// Resize the canvas
function resizeCanvas() {
  canvas.discardActiveObject();
  canvas.setHeight($('#canvas-area').height());
  canvas.setWidth($('#canvas-area').width());
  artboard.set({
    left: canvas.get('width') / 2 - artboard.get('width') / 2,
    top: canvas.get('height') / 2 - artboard.get('height') / 2,
  });
  canvas.renderAll();
  animate(false, currenttime);
  initLines();
}
window.addEventListener('resize', resizeCanvas, false);
resizeCanvas();

// Highlight layers when selecting objects in the canvas, scroll into view if needed
function updateSelection(e) {
  if (e.target.type == 'activeSelection') {
    $('.layer-selected').removeClass('layer-selected');
    canvas.getActiveObjects().forEach(function (object) {
      if (
        $('.layer').length > 0 &&
        $(".layer[data-object='" + object.get('id') + "']").length > 0
      ) {
        $(".layer[data-object='" + object.get('id') + "']").addClass(
          'layer-selected'
        );
        if (e.e != undefined && $('.layer-selected').length > 0) {
          document
            .getElementsByClassName('layer-selected')[0]
            .scrollIntoView();
        }
      }
    });
  } else {
    if (
      $('.layer').length > 0 &&
      $(".layer[data-object='" + e.target.get('id') + "']").length > 0
    ) {
      $('.layer-selected').removeClass('layer-selected');
      $(".layer[data-object='" + e.target.get('id') + "']").addClass(
        'layer-selected'
      );
      if (e.e != undefined && $('.layer-selected').length > 0) {
        document
          .getElementsByClassName('layer-selected')[0]
          .scrollIntoView();
      }
    }
  }
}

// Object has been modified, automatically add a keyframe
function autoKeyframe(object, e, multi) {
  if (e.action == 'drag') {
    newKeyframe(
      'left',
      object,
      currenttime,
      object.get('left'),
      true
    );
    newKeyframe('top', object, currenttime, object.get('top'), true);
  } else if (e.action == 'scale') {
    newKeyframe(
      'scaleX',
      object,
      currenttime,
      object.get('scaleX'),
      true
    );
    newKeyframe(
      'scaleY',
      object,
      currenttime,
      object.get('scaleY'),
      true
    );
    newKeyframe(
      'left',
      object,
      currenttime,
      object.get('left'),
      true
    );
    newKeyframe('top', object, currenttime, object.get('top'), true);
    newKeyframe(
      'width',
      object,
      currenttime,
      object.get('width'),
      true
    );
    newKeyframe(
      'height',
      object,
      currenttime,
      object.get('height'),
      true
    );
  } else if (e.action == 'rotate') {
    newKeyframe(
      'angle',
      object,
      currenttime,
      object.get('angle'),
      true
    );
    if (multi) {
      newKeyframe(
        'scaleX',
        object,
        currenttime,
        object.get('scaleX'),
        true
      );
      newKeyframe(
        'scaleY',
        object,
        currenttime,
        object.get('scaleY'),
        true
      );
      newKeyframe(
        'width',
        object,
        currenttime,
        object.get('width'),
        true
      );
      newKeyframe(
        'left',
        object,
        currenttime,
        object.get('left'),
        true
      );
      newKeyframe(
        'top',
        object,
        currenttime,
        object.get('top'),
        true
      );
    }
  } else if (
    e.action == 'resizing' ||
    e.action == 'scaleX' ||
    e.action == 'scaleY'
  ) {
    newKeyframe(
      'scaleX',
      object,
      currenttime,
      object.get('scaleX'),
      true
    );
    newKeyframe(
      'scaleY',
      object,
      currenttime,
      object.get('scaleY'),
      true
    );
    newKeyframe(
      'left',
      object,
      currenttime,
      object.get('left'),
      true
    );
    newKeyframe(
      'width',
      object,
      currenttime,
      object.get('width'),
      true
    );
    newKeyframe('top', object, currenttime, object.get('top'), true);
    newKeyframe(
      'height',
      object,
      currenttime,
      object.get('height'),
      true
    );
  }
}

// Reselect
function reselect(selection) {
  tempselection = false;
  if (selection.type == 'activeSelection') {
    var objs = [];
    for (let so of selection._objects) {
      for (let obj of canvas.getObjects()) {
        if (obj.get('id') === so.get('id')) {
          objs.push(obj);
          break;
        }
      }
    }
    canvas.setActiveObject(
      new fabric.ActiveSelection(objs, {
        canvas: canvas,
      })
    );
    canvas.renderAll();
  } else {
    if (selection.get('type') == 'group') {
      canvas.setActiveObject(canvas.getItemById(selection.get('id')));
    } else {
      canvas.setActiveObject(selection);
    }
    canvas.renderAll();
  }
}

// Group objects
function group() {
  var objects = canvas.getActiveObjects();
  var object_ids = [];
  var newgroup = new fabric.Group();
  objects.forEach(function (object) {
    newgroup.addWithUpdate(object);
    object.set({ inGroup: true });
    $(".layer[data-object='" + object.get('id') + "']").remove();
    object_ids.push(object.get('id'));
    $('#' + object.get('id')).remove();
    canvas.remove(object);
  });
  //var newgroup = canvas.getActiveObject().toGroup();
  newgroup.set({
    id: 'Group' + layer_count,
    objectCaching: false,
    isGroup: true,
    color: '#F1890E',
    type: 'group',
    stroke: '#000',
    strokeUniform: true,
    strokeWidth: 0,
    paintFirst: 'stroke',
    absolutePositioned: true,
    inGroup: false,
    strokeDashArray: false,
    shadow: {
      color: 'black',
      offsetX: 0,
      offsetY: 0,
      blur: 0,
      opacity: 0,
    },
  });
  groups.push({ id: newgroup.get('id'), objects: object_ids });
  canvas.renderAll();
  newLayer(newgroup);
  canvas.setActiveObject(newgroup);
  sortKeyframes();
  save();
}
$(document).on('click', '#group-objects', group);

// Ungroup SVG
function unGroup(group) {
  canvas.discardActiveObject();
  canvas.renderAll();
  tempgroup = group._objects;
  group._restoreObjectsState();
  $(".layer[data-object='" + group.get('id') + "']").remove();
  $('#' + group.get('id')).remove();
  canvas.remove(group);
  keyframes = $.grep(keyframes, function (e) {
    return e.id != group.get('id');
  });
  p_keyframes = $.grep(p_keyframes, function (e) {
    return e.id != group.get('id');
  });
  objects = $.grep(objects, function (e) {
    return e.id != group.get('id');
  });
  canvas.renderAll();
  for (var i = 0; i < tempgroup.length; i++) {
    if (tempgroup[i].inGroup) {
      canvas.add(tempgroup[i]);
      renderLayer(tempgroup[i]);
      props.forEach(function (prop) {
        if (
          prop != 'top' &&
          prop != 'scaleY' &&
          prop != 'width' &&
          prop != 'height' &&
          prop != 'shadow.offsetX' &&
          prop != 'shadow.offsetY' &&
          prop != 'shadow.opacity' &&
          prop != 'shadow.blur' &&
          prop != 'lineHeight'
        ) {
          renderProp(prop, tempgroup[i]);
        }
      });
      const keyarr = $.grep(keyframes, function (e) {
        return e.id == tempgroup[i].id;
      });
      keyarr.forEach(function (keyframe) {
        if (
          keyframe.name != 'top' &&
          keyframe.name != 'scaleY' &&
          keyframe.name != 'width' &&
          keyframe.name != 'height' &&
          keyframe.name != 'shadow.offsetX' &&
          keyframe.name != 'shadow.offsetY' &&
          keyframe.name != 'shadow.opacity' &&
          keyframe.name != 'shadow.blur' &&
          keyframe.name != 'lineHeight'
        ) {
          renderKeyframe(
            canvas.getItemById(keyframe.id),
            keyframe.name,
            keyframe.t
          );
        }
      });
    }
  }
  canvas.renderAll();
  save();
}
$(document).on('click', '#ungroup-objects', function () {
  if (canvas.getActiveObject()) {
    unGroup(canvas.getActiveObject());
  }
});

// Regroup SVG
function reGroup(id) {
  var group = [];
  var objects = [];
  const entry = groups.find((x) => x.id == id);
  if (!entry) {
    return;
  }
  entry.objects.forEach(function (object) {
    const item = canvas.getItemById(object);
    if (item) {
      objects.push(item);
    }
  });
  if (objects.length == 0) {
    return;
  }
  var activeselection = new fabric.ActiveSelection(objects);
  var newgroup = activeselection.toGroup();
  newgroup.set({
    id: id,
    objectCaching: false,
  });
  canvas.add(newgroup);
  canvas.renderAll();
}

// Keep record canvas up to date
async function updateRecordCanvas() {
  canvasrecord.setWidth(artboard.width);
  canvasrecord.setHeight(artboard.height);
  canvasrecord.width = artboard.width;
  canvasrecord.height = artboard.height;
  canvas.clipPath = null;
  // Sequential, not forEach(async): the snapshot below must not be taken
  // while filters are still being stripped off the objects.
  for (const object of objects) {
    var obj = canvas.getItemById(object.id);
    if (!obj) {
      object.filters = [];
      continue;
    }
    if (obj.filters) {
      if (obj.filters.length > 0) {
        object.filters = [];
        obj.filters.forEach(function (filter) {
          if (
            filter.type == 'BlackWhite' ||
            filter.type == 'Invert' ||
            filter.type == 'Sepia' ||
            filter.type == 'Kodachrome' ||
            filter.type == 'Polaroid' ||
            filter.type == 'Technicolor' ||
            filter.type == 'Brownie' ||
            filter.type == 'Vintage'
          ) {
            object.filters.push({ type: filter.type });
          } else if (filter.type == 'Brightness') {
            object.filters.push({
              type: filter.type,
              value: filter.brightness,
            });
          } else if (filter.type == 'Contrast') {
            object.filters.push({
              type: filter.type,
              value: filter.contrast,
            });
          } else if (filter.type == 'Vibrance') {
            object.filters.push({
              type: filter.type,
              value: filter.vibrance,
            });
          } else if (filter.type == 'Saturation') {
            object.filters.push({
              type: filter.type,
              value: filter.saturation,
            });
          } else if (filter.type == 'HueRotation') {
            object.filters.push({
              type: filter.type,
              value: filter.rotation,
            });
          } else if (filter.type == 'Blur') {
            object.filters.push({
              type: filter.type,
              value: filter.blur,
            });
          } else if (filter.type == 'Noise') {
            object.filters.push({
              type: filter.type,
              value: filter.noise,
            });
          } else if (filter.type == 'RemoveColor') {
            object.filters.push({
              type: filter.type,
              distance: filter.distance,
              color: filter.color,
            });
          }
        });
        obj.filters = [];
        obj.applyFilters();
        var backend = fabric.filterBackend;
        if (backend && backend.evictCachesForKey) {
          backend.evictCachesForKey(obj.cacheKey);
          backend.evictCachesForKey(obj.cacheKey + '_filtered');
        }
        if (
          obj.filters.length > 0 &&
          obj.get('id').indexOf('Video') >= 0
        ) {
          await obj.setElement(obj.saveElem);
        }
      } else {
        object.filters = [];
      }
    } else {
      object.filters = [];
    }
  }
  const canvassave = canvas.toJSON([
    'volume',
    'audioSrc',
    'defaultLeft',
    'defaultTop',
    'defaultScaleX',
    'defaultScaleY',
    'notnew',
    'starttime',
    'top',
    'left',
    'width',
    'height',
    'scaleX',
    'scaleY',
    'flipX',
    'flipY',
    'originX',
    'originY',
    'transformMatrix',
    'stroke',
    'strokeWidth',
    'strokeDashArray',
    'strokeLineCap',
    'strokeDashOffset',
    'strokeLineJoin',
    'strokeMiterLimit',
    'angle',
    'opacity',
    'fill',
    'globalCompositeOperation',
    'shadow',
    'clipTo',
    'visible',
    'backgroundColor',
    'skewX',
    'skewY',
    'fillRule',
    'paintFirst',
    'clipPath',
    'strokeUniform',
    'rx',
    'ry',
    'cornerRadius',
    'selectable',
    'hasControls',
    'subTargetCheck',
    'id',
    'hoverCursor',
    'defaultCursor',
    'isEditing',
    'source',
    'assetType',
    'duration',
    'inGroup',
  ]);
  canvas.clipPath = artboard;
  // loadFromJSON is asynchronous (it clears the canvas first, then revives the
  // objects), so callers that record straight afterwards must await this.
  return new Promise(function (resolve) {
    canvasrecord.loadFromJSON(canvassave, function () {
      if (canvasrecord.getItemById('center_h')) {
        canvasrecord.remove(canvasrecord.getItemById('center_h'));
        canvasrecord.remove(canvasrecord.getItemById('center_v'));
      }
      if (canvasrecord.getItemById('line_h')) {
        canvasrecord.remove(canvasrecord.getItemById('line_h'));
        canvasrecord.remove(canvasrecord.getItemById('line_v'));
      }
      canvasrecord.renderAll();
      canvasrecord.setWidth(artboard.width);
      canvasrecord.setHeight(artboard.height);
      canvasrecord.width = artboard.width;
      canvasrecord.height = artboard.height;
      canvasrecord.renderAll();
      objects.forEach(function (object) {
        replaceSource(
          canvasrecord.getItemById(object.id),
          canvasrecord
        );
        replaceSource(canvas.getItemById(object.id), canvas);
      });
      resolve();
    });
  });
}

// Frame rate the user picked in the download modal, used by every export path
const EXPORT_FPS_DEFAULT = 30;

function getExportFramerate() {
  const fps = parseInt($('#framerate').val(), 10);
  if (!(fps > 0)) {
    return EXPORT_FPS_DEFAULT;
  }
  return Math.min(120, fps);
}

// A still image has no frame rate
$('input[name=radio]').on('change', function () {
  $('#framerate-row').toggle(
    $('input[name=radio]:checked').val() != 'image'
  );
});

// Put the editor back into a usable state once a render has finished (or failed)
function resetRecordingUI() {
  recording = false;
  currenttime = 0;
  animate(false, 0);
  $('#seekbar').offset({
    left:
      offset_left +
      $('#inner-timeline').offset().left +
      currenttime / timelinetime,
  });
  canvas.renderAll();
  resizeCanvas();
  if (background_audio != false) {
    background_audio.pause();
    background_audio = new Audio(background_audio.src);
  }
  $('#download-real').html('Download');
  $('#download-real').removeClass('downloading');
  updateRecordCanvas();
}

// Hand a finished WebM recording to the user, converting it first if the
// chosen format is not webm.
function deliverRecording(blob) {
  $('#download-real').html('Downloading...');
  const format = $('input[name=radio]:checked').val();
  if (format == 'mp4' || format == 'gif') {
    convertStreams(blob, format);
    return;
  }
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.style.display = 'none';
  a.href = url;
  a.download = 'video.webm';
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  // Give the browser a tick to start the download before releasing the blob
  window.setTimeout(function () {
    URL.revokeObjectURL(url);
  }, 60000);
  resetRecordingUI();
}

// Download recording
function downloadRecording(chunks) {
  $('#download-real').html('Downloading...');
  const format = $('input[name=radio]:checked').val();
  if (format == 'webm') {
    var url = URL.createObjectURL(
      new Blob(chunks, {
        type: 'video/webm',
      })
    );
    const a = document.createElement('a');
    a.style.display = 'none';
    a.href = url;
    a.download = 'video.webm';
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    // Give the browser a tick to start the download before releasing the blob
    window.setTimeout(function () {
      URL.revokeObjectURL(url);
    }, 60000);
    resetRecordingUI();
  } else if (format == 'mp4' || format == 'gif') {
    // Both are transcoded from the captured webm by the ffmpeg worker
    convertStreams(
      new Blob(chunks, { type: 'video/webm' }),
      format
    );
  } else {
    console.warn('Unknown download format: ' + format);
    resetRecordingUI();
  }
}

$('#download-real').on('click', record);

// Save everything in the canvas (for undo/redo/autosave)
function save() {
  redo = [];
  redoarr = [];
  if (state) {
    undo.push(state);
    undoarr.push(statearr);
  }
  canvas.clipPath = null;
  state = canvas.toJSON([
    'volume',
    'audioSrc',
    'top',
    'left',
    'width',
    'height',
    'scaleX',
    'scaleY',
    'flipX',
    'flipY',
    'originX',
    'originY',
    'transformMatrix',
    'stroke',
    'strokeWidth',
    'strokeDashArray',
    'strokeLineCap',
    'strokeDashOffset',
    'strokeLineJoin',
    'strokeMiterLimit',
    'angle',
    'opacity',
    'fill',
    'globalCompositeOperation',
    'shadow',
    'clipTo',
    'visible',
    'backgroundColor',
    'skewX',
    'skewY',
    'fillRule',
    'paintFirst',
    'clipPath',
    'strokeUniform',
    'rx',
    'ry',
    'cornerRadius',
    'selectable',
    'hasControls',
    'subTargetCheck',
    'id',
    'hoverCursor',
    'defaultCursor',
    'isEditing',
    'source',
    'assetType',
    'duration',
    'inGroup',
    'filters',
  ]);
  canvas.clipPath = artboard;
  statearr = {
    keyframes: JSON.parse(JSON.stringify(keyframes)),
    p_keyframes: JSON.parse(JSON.stringify(p_keyframes)),
    objects: JSON.parse(JSON.stringify(objects)),
    colormode: colormode,
    duration: duration,
    currenttime: currenttime,
  };
  if (undo.length >= 1) {
    $('#undo').addClass('history-active');
  } else {
    $('#undo').removeClass('history-active');
  }
  if (redo.length >= 1) {
    $('#redo').addClass('history-active');
  } else {
    $('#redo').removeClass('history-active');
  }

  schedulePersist();
}

// Rebuilding the record canvas and writing to IndexedDB are both expensive
// and used to run on every single edit (including every keystroke). Coalesce
// them; record() awaits updateRecordCanvas() directly when it needs it fresh.
var persistTimer;
function schedulePersist() {
  window.clearTimeout(persistTimer);
  persistTimer = window.setTimeout(function () {
    updateRecordCanvas();
    autoSave();
  }, 400);
}

// Duplicate object
function copyObject() {
  if (clipboard) {
    if (cliptype == 'object') {
      if (clipboard.type == 'activeSelection') {
        clipboard._objects.forEach(function (clone) {
          clone.clone(function (cloned) {
            // layer_count only advances inside newLayer, so read it after
            // the clone resolves or every copy shares one id.
            cloned.set({
              id: 'Shape' + layer_count,
            });
            canvas.add(cloned);
            canvas.renderAll();
            newLayer(cloned);
            canvas.setActiveObject(cloned);
          });
        });
      } else {
        clipboard.clone(function (cloned) {
          cloned.set({
            id: 'Shape' + layer_count,
          });
          canvas.add(cloned);
          canvas.renderAll();
          newLayer(cloned);
          canvas.setActiveObject(cloned);
        });
      }
      save();
    } else {
      copyKeyframes();
    }
  }
}

// Replace the source of an object when reloading the canvas (since Fabric needs a DOM reference for the objects)
function replaceSource(object, canvas) {
  if (object == null) {
    return false;
  }
  if (object.get('type') != 'group') {
    if (object.type) {
      if (object.type == 'image') {
        if (object.get('id').indexOf('Video') >= 0) {
          var vidObj = document.createElement('video');
          var vidSrc = document.createElement('source');
          vidSrc.src = object.get('source');
          vidObj.crossOrigin = 'anonymous';
          vidObj.appendChild(vidSrc);
          vidObj.addEventListener('loadeddata', function () {
            vidObj.width = this.videoWidth;
            vidObj.height = this.videoHeight;
            vidObj.currentTime = 0;
            vidObj.muted = false;
            async function waitLoad() {
              if (vidObj.readyState >= 3) {
                object.setElement(vidObj);
                object.saveElem = vidObj;
                await canvas.renderAll();
                await animate(false, currenttime);
                if (
                  objects.find((x) => x.id == object.get('id'))
                    .filters
                ) {
                  if (
                    objects.find((x) => x.id == object.get('id'))
                      .filters.length > 0
                  ) {
                    objects
                      .find((x) => x.id == object.get('id'))
                      .filters.forEach(function (filter) {
                        if (filter.type == 'Sepia') {
                          object.filters.push(new f.Sepia());
                        } else if (filter.type == 'Invert') {
                          object.filters.push(new f.Invert());
                        } else if (filter.type == 'BlackWhite') {
                          object.filters.push(new f.BlackWhite());
                        } else if (filter.type == 'Kodachrome') {
                          object.filters.push(new f.Kodachrome());
                        } else if (filter.type == 'Polaroid') {
                          object.filters.push(new f.Polaroid());
                        } else if (filter.type == 'Technicolor') {
                          object.filters.push(new f.Technicolor());
                        } else if (filter.type == 'Vintage') {
                          object.filters.push(new f.Vintage());
                        } else if (filter.type == 'Brownie') {
                          object.filters.push(new f.Brownie());
                        } else if (filter.type == 'Brightness') {
                          object.filters.push(
                            new f.Brightness({
                              brightness: filter.value,
                            })
                          );
                        } else if (filter.type == 'Contrast') {
                          object.filters.push(
                            new f.Contrast({ contrast: filter.value })
                          );
                        } else if (filter.type == 'Saturation') {
                          object.filters.push(
                            new f.Saturation({
                              saturation: filter.value,
                            })
                          );
                        } else if (filter.type == 'Vibrance') {
                          object.filters.push(
                            new f.Vibrance({ vibrance: filter.value })
                          );
                        } else if (filter.type == 'HueRotation') {
                          object.filters.push(
                            new f.HueRotation({
                              rotation: filter.value,
                            })
                          );
                        } else if (filter.type == 'Noise') {
                          object.filters.push(
                            new f.Noise({ noise: filter.value })
                          );
                        } else if (filter.type == 'Blur') {
                          object.filters.push(
                            new f.Blur({ blur: filter.value })
                          );
                        } else if (filter.type == 'RemoveColor') {
                          object.filters.push(
                            new f.RemoveColor({
                              distance: filter.distance,
                              color: filter.color,
                            })
                          );
                        }
                      });
                    object.applyFilters();
                    canvas.renderAll();
                  }
                }
              } else {
                window.setTimeout(function () {
                  waitLoad();
                }, 100);
              }
            }
            window.setTimeout(function () {
              waitLoad();
            }, 100);
          });
          vidObj.currentTime = 0;
        } else {
          //var img = new Image();
          //img.onload = function(){
          //   object.setElement(img);
          //    canvas.renderAll();
          if (objects.find((x) => x.id == object.get('id')).filters) {
            if (
              objects.find((x) => x.id == object.get('id')).filters
                .length > 0
            ) {
              objects
                .find((x) => x.id == object.get('id'))
                .filters.forEach(function (filter) {
                  if (filter.type == 'Sepia') {
                    object.filters.push(new f.Sepia());
                  } else if (filter.type == 'Invert') {
                    object.filters.push(new f.Invert());
                  } else if (filter.type == 'BlackWhite') {
                    object.filters.push(new f.BlackWhite());
                  } else if (filter.type == 'Kodachrome') {
                    object.filters.push(new f.Kodachrome());
                  } else if (filter.type == 'Polaroid') {
                    object.filters.push(new f.Polaroid());
                  } else if (filter.type == 'Technicolor') {
                    object.filters.push(new f.Technicolor());
                  } else if (filter.type == 'Vintage') {
                    object.filters.push(new f.Vintage());
                  } else if (filter.type == 'Brownie') {
                    object.filters.push(new f.Brownie());
                  } else if (filter.type == 'Brightness') {
                    object.filters.push(
                      new f.Brightness({ brightness: filter.value })
                    );
                  } else if (filter.type == 'Contrast') {
                    object.filters.push(
                      new f.Contrast({ contrast: filter.value })
                    );
                  } else if (filter.type == 'Saturation') {
                    object.filters.push(
                      new f.Saturation({ saturation: filter.value })
                    );
                  } else if (filter.type == 'Vibrance') {
                    object.filters.push(
                      new f.Vibrance({ vibrance: filter.value })
                    );
                  } else if (filter.type == 'HueRotation') {
                    object.filters.push(
                      new f.HueRotation({ rotation: filter.value })
                    );
                  } else if (filter.type == 'Noise') {
                    object.filters.push(
                      new f.Noise({ noise: filter.value })
                    );
                  } else if (filter.type == 'Blur') {
                    object.filters.push(
                      new f.Blur({ blur: filter.value })
                    );
                  } else if (filter.type == 'RemoveColor') {
                    object.filters.push(
                      new f.RemoveColor({
                        distance: filter.distance,
                        color: filter.color,
                      })
                    );
                  }
                });
              object.applyFilters();
              canvas.renderAll();
            } else {
              object.filters = [];
              object.applyFilters();
              canvas.renderAll();
            }
          }

          //}
          //img.src = window.URL.createObjectURL(new Blob(files.find(x => x.name == object.get("id")).file, {type:"image/png"}));
        }
      }
    }
  }
}

// Perform undo/redo
function undoRedo(newState, saveState, newArrState, saveArrState) {
  saveState.push(state);
  saveArrState.push(statearr);
  statearr = newArrState.pop();
  state = newState.pop();
  keyframes = statearr.keyframes;
  p_keyframes = statearr.p_keyframes;
  objects = statearr.objects;
  colormode = statearr.colormode;
  duration = statearr.duration;
  currenttime = statearr.currenttime;
  canvas.clipPath = null;
  canvas.loadFromJSON(state, function () {
    canvas.clipPath = artboard;
    hideGuides(canvas);
    canvas.renderAll();
    $('.object-props').remove();
    $('.layer').remove();
    objects.forEach(function (object) {
      if (!canvas.getItemById(object.id)) {
        return;
      }
      replaceSource(canvas.getItemById(object.id), canvas);
      renderLayer(canvas.getItemById(object.id));
      props.forEach(function (prop) {
        if (
          prop != 'top' &&
          prop != 'scaleY' &&
          prop != 'width' &&
          prop != 'height' &&
          prop != 'shadow.offsetX' &&
          prop != 'shadow.offsetY' &&
          prop != 'shadow.opacity' &&
          prop != 'shadow.blur' &&
          prop != 'lineHeight'
        ) {
          renderProp(prop, canvas.getItemById(object.id));
        }
      });
    });
    keyframes.forEach(function (keyframe) {
      if (!canvas.getItemById(keyframe.id)) {
        return;
      }
      if (
        keyframe.name != 'top' &&
        keyframe.name != 'scaleY' &&
        keyframe.name != 'width' &&
        keyframe.name != 'height' &&
        keyframe.name != 'shadow.offsetX' &&
        keyframe.name != 'shadow.offsetY' &&
        keyframe.name != 'shadow.opacity' &&
        keyframe.name != 'shadow.blur' &&
        keyframe.name != 'lineHeight'
      ) {
        renderKeyframe(
          canvas.getItemById(keyframe.id),
          keyframe.name,
          keyframe.t
        );
      }
    });
    animate(false, currenttime);
  });
  if (undo.length >= 1) {
    $('#undo').addClass('history-active');
  } else {
    $('#undo').removeClass('history-active');
  }
  if (redo.length >= 1) {
    $('#redo').addClass('history-active');
  } else {
    $('#redo').removeClass('history-active');
  }
}

// Undo/redo buttons
$(document).on('click', '#undo', function () {
  if (undo.length >= 1) {
    undoRedo(undo, redo, undoarr, redoarr);
  }
});
$(document).on('click', '#redo', function () {
  if (redo.length >= 1) {
    undoRedo(redo, undo, redoarr, undoarr);
  }
});

// Corner radius, in the canvas pixels the panel advertises.
//
// fabric applies rx/ry in the object's own coordinate space, before its scale,
// and shapes are resized by scaling rather than by changing width/height. The
// raw input was written straight to rx, so a radius typed as 20 drew at 60 on a
// rect scaled 3x: the value behaved like a proportion of the shape instead of
// pixels. `cornerRadius` stores what the user asked for and rx/ry are derived
// from it, so the same number means the same number of pixels at any size.
function getCornerRadius(object) {
  if (!object) {
    return 0;
  }
  if (typeof object.cornerRadius == 'number') {
    return object.cornerRadius;
  }
  // Projects saved before cornerRadius existed only carry the raw rx
  return (
    (object.get('rx') || 0) * Math.abs(object.get('scaleX') || 1)
  );
}

function setCornerRadius(object, radius) {
  if (!object) {
    return;
  }
  var px = radius > 0 ? radius : 0;
  object.cornerRadius = px;
  object.set({
    rx: px / Math.abs(object.get('scaleX') || 1),
    ry: px / Math.abs(object.get('scaleY') || 1),
  });
}

// Re-derive rx/ry after a resize so the drawn radius keeps the typed value
function syncCornerRadius(object) {
  if (!object || object.get('type') != 'rect') {
    return;
  }
  if (typeof object.cornerRadius != 'number') {
    return;
  }
  setCornerRadius(object, object.cornerRadius);
}

// Generate keyframes
function keyframeChanges(object, type, id, selection) {
  if (object.get('type') == 'rect') {
    setCornerRadius(
      object,
      parseFloat($('#object-corners input').val())
    );
  } else if (object.get('type') == 'textbox') {
    object.set({
      charSpacing: parseFloat($('#text-h input').val()) * 10,
      lineHeight: parseFloat($('#text-v input').val() / 100),
    });
    canvas.renderAll();
  }
  canvas.renderAll();
  if (type && type == 'opacity') {
    newKeyframe(
      'opacity',
      object,
      currenttime,
      object.get('opacity'),
      true
    );
  } else if (type && type == 'opacity3') {
    newKeyframe(
      'charSpacing',
      object,
      currenttime,
      object.get('charSpacing'),
      true
    );
    newKeyframe(
      'lineHeight',
      object,
      currenttime,
      object.get('lineHeight'),
      true
    );
  } else {
    if (id == 'object-x' || id == 'object-y') {
      newKeyframe(
        'left',
        object,
        currenttime,
        object.get('left'),
        true
      );
      newKeyframe(
        'top',
        object,
        currenttime,
        object.get('top'),
        true
      );
    } else if (id == 'object-w' || id == 'object-h') {
      newKeyframe(
        'scaleX',
        object,
        currenttime,
        object.get('scaleX'),
        true
      );
      newKeyframe(
        'scaleY',
        object,
        currenttime,
        object.get('scaleY'),
        true
      );
      newKeyframe(
        'width',
        object,
        currenttime,
        object.get('width'),
        true
      );
      newKeyframe(
        'height',
        object,
        currenttime,
        object.get('height'),
        true
      );
      if (selection) {
        newKeyframe(
          'left',
          object,
          currenttime,
          object.get('left'),
          true
        );
        newKeyframe(
          'top',
          object,
          currenttime,
          object.get('top'),
          true
        );
      }
    } else if (id == 'object-r') {
      newKeyframe(
        'angle',
        object,
        currenttime,
        object.get('angle'),
        true
      );
      if (selection) {
        newKeyframe(
          'left',
          object,
          currenttime,
          object.get('left'),
          true
        );
        newKeyframe(
          'top',
          object,
          currenttime,
          object.get('top'),
          true
        );
        newKeyframe(
          'scaleX',
          object,
          currenttime,
          object.get('scaleX'),
          true
        );
        newKeyframe(
          'scaleY',
          object,
          currenttime,
          object.get('scaleY'),
          true
        );
        newKeyframe(
          'width',
          object,
          currenttime,
          object.get('width'),
          true
        );
        newKeyframe(
          'height',
          object,
          currenttime,
          object.get('width'),
          true
        );
      }
    } else if (id == 'object-stroke') {
      newKeyframe(
        'strokeWidth',
        object,
        currenttime,
        object.get('strokeWidth'),
        true
      );
      newKeyframe(
        'stroke',
        object,
        currenttime,
        object.get('stroke'),
        true
      );
    } else if (
      id == 'object-shadow-x' ||
      id == 'object-shadow-y' ||
      id == 'object-blur' ||
      id == 'object-color-stroke-opacity'
    ) {
      newKeyframe(
        'shadow.color',
        object,
        currenttime,
        object.shadow.color,
        true
      );
      newKeyframe(
        'shadow.opacity',
        object,
        currenttime,
        object.shadow.opacity,
        true
      );
      newKeyframe(
        'shadow.offsetX',
        object,
        currenttime,
        object.shadow.offsetX,
        true
      );
      newKeyframe(
        'shadow.offsetY',
        object,
        currenttime,
        object.shadow.offsetY,
        true
      );
      newKeyframe(
        'shadow.blur',
        object,
        currenttime,
        object.shadow.blur,
        true
      );
    }
  }
  save();
}

// Play video
function play() {
  paused = false;
  animate(true, currenttime);
  $('#play-button').attr('src', 'assets/pause-button.svg');
}

// Pause video
function pause() {
  paused = true;
  $('#play-button').attr('src', 'assets/play-button.svg');
}

// Write an object's stored default for a property, creating the entry if the
// object was serialized before that property existed.
function setDefaultValue(object, prop, value) {
  const entry = objects.find((x) => x.id == object.get('id'));
  if (!entry) {
    return;
  }
  if (!entry.defaults) {
    entry.defaults = [];
  }
  const def = entry.defaults.find((x) => x.name == prop);
  if (def) {
    def.value = value;
  } else {
    entry.defaults.push({ name: prop, value: value });
  }
}

// Read an object's stored default for a property (undefined when absent)
function getDefaultValue(id, prop) {
  const entry = objects.find((x) => x.id == id);
  if (!entry || !entry.defaults) {
    return undefined;
  }
  const def = entry.defaults.find((x) => x.name == prop);
  return def ? def.value : undefined;
}

// Read a property that may be a "shadow.*" path.
// fabric's get() is flat, so object.get("shadow.blur") is always undefined.
function getPropValue(object, prop) {
  if (typeof prop === 'string' && prop.indexOf('shadow.') === 0) {
    return object.shadow ? object.shadow[prop.slice(7)] : undefined;
  }
  return object.get(prop);
}

// Set object value (while animating)
function setObjectValue(prop, object, value, inst) {
  if (object.get('type') != 'group') {
    if (object.group) {
      var group = object.group;
      tempgroup = group._objects;
      group._restoreObjectsState();
      canvas.setActiveObject(group);
      inst.remove(canvas.getActiveObject());
      canvas.discardActiveObject();
      inst.renderAll();
      for (var i = 0; i < tempgroup.length; i++) {
        inst.add(tempgroup[i]);
      }
    }
  }
  if (prop == 'left' && !recording) {
    object.set(prop, value + artboard.get('left'));
  } else if (prop == 'top' && !recording) {
    object.set(prop, value + artboard.get('top'));
  } else if (prop == 'shadow.blur') {
    object.shadow.blur = value;
  } else if (prop == 'shadow.color') {
    object.shadow.color = value;
  } else if (prop == 'shadow.offsetX') {
    object.shadow.offsetX = value;
  } else if (prop == 'shadow.offsetY') {
    object.shadow.offsetY = value;
  } else if (object.get('type') != 'group') {
    object.set(prop, value);
  } else if (prop != 'width') {
    object.set(prop, value);
  }
  object.setCoords();
  inst.renderAll();
}

// Group keyframes by "id|name", each group sorted by time.
// Built once per rendered frame: without it every keyframe lookup sorted a
// copy of the whole keyframe list, making playback O(n^2 log n).
function buildKeyframeIndex() {
  const idx = new Map();
  keyframes.forEach(function (keyframe) {
    const key = keyframe.id + '|' + keyframe.name;
    let arr = idx.get(key);
    if (!arr) {
      arr = [];
      idx.set(key, arr);
    }
    arr.push(keyframe);
  });
  idx.forEach(function (arr) {
    arr.sort(function (a, b) {
      return a.t - b.t;
    });
  });
  return idx;
}

// All keyframes of one object/property, sorted by time
function keyframeSiblings(keyframe, idx) {
  if (idx) {
    return idx.get(keyframe.id + '|' + keyframe.name) || [];
  }
  return keyframes
    .filter(function (e) {
      return e.id == keyframe.id && e.name == keyframe.name;
    })
    .sort(function (a, b) {
      return a.t - b.t;
    });
}

// Find last keyframe in time from same object & property
function lastKeyframe(keyframe, idx) {
  const arr = keyframeSiblings(keyframe, idx);
  const pos = arr.indexOf(keyframe);
  if (pos <= 0) {
    return false;
  }
  return arr[pos - 1];
}

// Find next keyframe in time from same object & property
function nextKeyframe(keyframe, idx) {
  const arr = keyframeSiblings(keyframe, idx);
  const pos = arr.indexOf(keyframe);
  if (pos == -1 || pos == arr.length - 1) {
    return false;
  }
  return arr[pos + 1];
}

// Check whether any keyframe exists for a certain property
function checkAnyKeyframe(id, prop, inst, idx) {
  const object = inst.getItemById(id);
  if (!object) {
    return false;
  }
  if (object.get('assetType') == 'audio') {
    return false;
  }
  if (
    object.get('type') != 'textbox' &&
    (prop == 'charSpacing' || prop == 'lineHeight')
  ) {
    return false;
  }
  if (
    object.get('type') == 'group' &&
    (prop == 'shadow.opacity' ||
      prop == 'shadow.color' ||
      prop == 'shadow.offsetX' ||
      prop == 'shadow.offsetY' ||
      prop == 'shadow.blur')
  ) {
    return false;
  }
  const hasKeyframe = idx
    ? idx.has(id + '|' + prop)
    : keyframes.some((e) => e.id == id && e.name == prop);
  if (!hasKeyframe) {
    const value = getDefaultValue(id, prop);
    if (value === undefined) {
      return false;
    }
    setObjectValue(prop, object, value, inst);
  }
}

// Check if parameter is a DOM element
function isDomElem(el) {
  return el instanceof HTMLElement || el[0] instanceof HTMLElement
    ? true
    : false;
}

// Play videos when seeking/playing
async function playVideos(time) {
  objects.forEach(async function (entry) {
    var inst = recording ? canvasrecord : canvas;
    var object = inst.getItemById(entry.id);
    var p_keyframe = p_keyframes.find((x) => x.id == entry.id);
    if (object == null || !p_keyframe) {
      return false;
    }
    var start = false;
    if (
      object.get('id').indexOf('Video') >= 0 &&
      p_keyframe.trimstart + p_keyframe.start <= time &&
      p_keyframe.end >= time
    ) {
      var tempfilters = object.filters;
      if (object.filters.length > 0) {
        object.filters = [];
        object.applyFilters();
        var image = object;
        var backend = fabric.filterBackend;
        if (backend && backend.evictCachesForKey) {
          backend.evictCachesForKey(image.cacheKey);
          backend.evictCachesForKey(image.cacheKey + '_filtered');
        }
        await object.setElement(object.saveElem);
      }
      object.set('visible', true);
      inst.renderAll();
      if ($(object.getElement())[0].paused == true) {
        $(object.getElement())[0].currentTime = parseFloat(
          (
            (time - p_keyframe.start + p_keyframe.trimstart) /
            1000
          ).toFixed(2)
        );
      }
      if (!recording) {
        var animation = {
          value: 0,
        };
        var instance = anime({
          targets: animation,
          value: [currenttime, duration],
          delay: 0,
          duration: duration,
          easing: 'linear',
          autoplay: true,
          update: async function () {
            if (!paused && start) {
              if (object.filters.length > 0) {
                object.filters = [];
                object.applyFilters();
                var image = object;
                var backend = fabric.filterBackend;
                if (backend && backend.evictCachesForKey) {
                  backend.evictCachesForKey(image.cacheKey);
                  backend.evictCachesForKey(
                    image.cacheKey + '_filtered'
                  );
                }
                await object.setElement(object.saveElem);
              }
              if ($(object.getElement())[0].tagName == 'VIDEO') {
                $(object.getElement())[0].play();
              }
              await inst.renderAll();
              if (tempfilters.length > 0) {
                object.filters = tempfilters;
                object.applyFilters();
                inst.renderAll();
              }
            } else if (paused) {
              if (
                isDomElem($(object.getElement())[0]) &&
                $(object.getElement())[0].tagName == 'VIDEO'
              ) {
                $(object.getElement())[0].pause();
              }
              animation.value = duration + 1;
              anime.remove(animation);
            }
          },
          changeBegin: function () {
            start = true;
          },
        });
        if (paused) {
          $(object.getElement())[0].currentTime = parseFloat(
            (
              (time - p_keyframe.start + p_keyframe.trimstart) /
              1000
            ).toFixed(2)
          );
          await inst.renderAll();
          if (tempfilters.length > 0) {
            object.filters = tempfilters;
            object.applyFilters();
            inst.renderAll();
          }
        }
      } else {
        if ($(object.getElement())[0].paused == true) {
          $(object.getElement())[0].play();
          inst.renderAll();
        }
      }
    } else if (object.get('id').indexOf('Video') >= 0) {
      $(object.getElement())[0].pause();
      object.set('visible', false);
      inst.renderAll();
    }
  });
}

// Play background audio
function playAudio(time) {
  objects.forEach(function (object) {
    var start = false;
    var obj = canvas.getItemById(object.id);
    var p_keyframe = p_keyframes.find((x) => x.id == object.id);
    if (obj && p_keyframe && obj.get('assetType') == 'audio') {
      var flag = false;
      var animation = {
        value: 0,
      };
      var instance = anime({
        targets: animation,
        value: [currenttime, duration],
        delay: 0,
        duration: duration,
        easing: 'linear',
        autoplay: true,
        update: function () {
          // `paused` is the only playback flag here; `play` used to be read
          // as a variable but resolves to the global play() function, so the
          // condition was always true.
          if (start && !paused) {
            if (
              !flag &&
              p_keyframe.start <= currenttime &&
              p_keyframe.end >= currenttime
            ) {
              if (obj.get('src')) {
                obj.get('src').currentTime =
                  (p_keyframe.trimstart -
                    p_keyframe.start +
                    currenttime) /
                  1000;
                obj.get('src').volume = obj.get('volume');
                obj.get('src').play();
                flag = true;
              } else {
                var audio = new Audio(obj.get('audioSrc'));
                obj.set('src', audio);
                audio.volume = obj.get('volume');
                audio.crossOrigin = 'anonymous';
                audio.currentTime =
                  (p_keyframe.trimstart -
                    p_keyframe.start +
                    currenttime) /
                  1000;
                audio.play();
                flag = true;
              }
            } else if (
              p_keyframe.start >= currenttime ||
              p_keyframe.end <= currenttime
            ) {
              if (obj.get('src')) {
                obj.get('src').pause();
              }
            }
          } else if (paused) {
            if (obj.get('src')) {
              obj.get('src').pause();
              anime.remove(animation);
            }
          }
        },
        changeBegin: function () {
          start = true;
        },
      });
    }
  });
}

// Temp animate with render callback
async function recordAnimate(time) {
  anime.speed = 1;
  //return new Promise(function(resolve){
  var inst = canvasrecord;
  const idx = buildKeyframeIndex();
  if (animatedtext.length > 0) {
    animatedtext.forEach(function (text) {
      text.seek(time, inst);
      inst.renderAll();
    });
  }
  keyframes.forEach(function (keyframe, index) {
    // Regroup if needed (groups break to animate their children, then regroup after children have animated)
    if (groups.find((x) => x.id == keyframe.id)) {
      if (!canvas.getItemById(keyframe.id)) {
        reGroup(keyframe.id);
      }
      const object = inst.getItemById(keyframe.id);
      const p_keyframe = p_keyframes.find((x) => x.id == keyframe.id);
      if (!object || !p_keyframe) {
        return;
      }
      if (time < p_keyframe.trimstart + p_keyframe.start) {
        object.set('visible', false);
        inst.renderAll();
      } else if (time > p_keyframe.end || time > duration) {
        object.set('visible', false);
        inst.renderAll();
      } else {
        object.set('visible', true);
        inst.renderAll();
      }
      if (time >= p_keyframe.trimstart + p_keyframe.start) {
        props.forEach(function (prop) {
          checkAnyKeyframe(keyframe.id, prop, inst, idx);
        });
      }
    }

    // Copy of setObjectValue function, seems to perform better inside the function
    function setValue(prop, object, value, inst) {
      if (object.get('type') != 'group') {
        if (object.group) {
          var group = object.group;
          tempgroup = group._objects;
          group._restoreObjectsState();
          canvas.setActiveObject(group);
          inst.remove(canvas.getActiveObject());
          canvas.discardActiveObject();
          inst.renderAll();
          for (var i = 0; i < tempgroup.length; i++) {
            inst.add(tempgroup[i]);
          }
        }
      }
      if (prop == 'left' && !recording) {
        object.set(prop, value + artboard.get('left'));
      } else if (prop == 'top' && !recording) {
        object.set(prop, value + artboard.get('top'));
      } else if (prop == 'shadow.blur') {
        object.shadow.blur = value;
      } else if (prop == 'shadow.color') {
        object.shadow.color = value;
      } else if (prop == 'shadow.offsetX') {
        object.shadow.offsetX = value;
      } else if (prop == 'shadow.offsetY') {
        object.shadow.offsetY = value;
      } else if (object.get('type') != 'group') {
        object.set(prop, value);
      } else if (prop != 'width') {
        object.set(prop, value);
      }
      object.setCoords();
      inst.renderAll();
    }

    var object = canvasrecord.getItemById(keyframe.id);
    if (!object) {
      return;
    }
    if (
      keyframe.t >= time &&
      time >=
        p_keyframes.find((x) => x.id == keyframe.id).trimstart +
          p_keyframes.find((x) => x.id == keyframe.id).start
    ) {
      var delay = 0;
      var start = false;
      var lasttime, lastprop;
      // Find last keyframe in time from same object & property
      var lastkey = lastKeyframe(keyframe, idx);
      if (!lastkey) {
        lasttime = 0;
        lastprop = getDefaultValue(keyframe.id, keyframe.name);
        if (lastprop === undefined) {
          return;
        }
      } else {
        lasttime = lastkey.t;
        lastprop = lastkey.value;
      }
      // Every recorded frame is rendered like a seek, so a keyframe whose
      // preceding keyframe is still in the future has not started yet.
      // (This used to read an undeclared `play`, throwing a ReferenceError
      // and aborting the rest of the frame.)
      if (lastkey && lastkey.t >= time) {
        return;
      }

      // Initiate the animation
      var animation = {
        value: lastprop,
      };
      var instance = anime({
        targets: animation,
        delay: delay,
        value: keyframe.value,
        duration: keyframe.t - lasttime,
        easing: keyframe.easing,
        autoplay: false,
        update: function () {
          if (start && paused) {
            anime.remove(animation);
          }
        },
        changeBegin: function () {
          start = true;
        },
      });

      if (time - lasttime <= 0) {
        instance.seek(0);
      } else {
        instance.seek(time - lasttime);
      }

      if (
        parseFloat(lasttime) <= parseFloat(time) &&
        parseFloat(keyframe.t) >= parseFloat(time)
      ) {
        setValue(keyframe.name, object, animation.value, inst);
      }
    } else if (keyframe.t < time && !nextKeyframe(keyframe, idx)) {
      var prop = keyframe.name;
      if (prop == 'shadow.blur') {
        if (object.shadow.blur != keyframe.value) {
          setValue(keyframe.name, object, keyframe.value, inst);
        }
      } else if (prop == 'shadow.color') {
        if (object.shadow.color != keyframe.value) {
          setValue(keyframe.name, object, keyframe.value, inst);
        }
      } else if (prop == 'shadow.offsetX') {
        if (object.shadow.offsetX != keyframe.value) {
          setValue(keyframe.name, object, keyframe.value, inst);
        }
      } else if (prop == 'shadow.offsetY') {
        if (object.shadow.offsetY != keyframe.value) {
          setValue(keyframe.name, object, keyframe.value, inst);
        }
      } else {
        if (object.get(prop) != keyframe.value) {
          setValue(keyframe.name, object, keyframe.value, inst);
        }
      }
    }
  });

  objects.forEach(function (object) {
    if (object.id.indexOf('Group') == -1) {
      // Visibility has to be applied to the object on the recording canvas,
      // at the time of the frame being rendered (not the editor playhead).
      const object2 = inst.getItemById(object.id);
      const p_keyframe = p_keyframes.find((x) => x.id == object.id);
      if (!object2 || !p_keyframe) {
        return;
      }
      if (time < p_keyframe.trimstart + p_keyframe.start) {
        object2.set('visible', false);
      } else if (time > p_keyframe.end || time > duration) {
        object2.set('visible', false);
      } else {
        object2.set('visible', true);
      }
      if (time >= p_keyframe.trimstart + p_keyframe.start) {
        props.forEach(function (prop) {
          checkAnyKeyframe(object.id, prop, inst, idx);
        });
      }
    }
  });
  inst.renderAll();

  // The offline renderer seeks each video layer to the exact frame time
  // itself; playVideos() would fight it by starting real-time playback.
  if (!offlinerender) {
    playVideos(time);
  }
  //});
}

// Animate timeline (or seek to specific point in time)
async function animate(play, time) {
  anime.speed = speed;
  if (!draggingPanel) {
    var offset = time;
    var inst = canvas;
    const idx = buildKeyframeIndex();
    keyframes.forEach(function (keyframe, index) {
      // Regroup if needed (groups break to animate their children, then regroup after children have animated)
      if (groups.find((x) => x.id == keyframe.id)) {
        if (!canvas.getItemById(keyframe.id)) {
          reGroup(keyframe.id);
        }
        const object = canvas.getItemById(keyframe.id);
        const p_keyframe = p_keyframes.find(
          (x) => x.id == keyframe.id
        );
        if (!object || !p_keyframe) {
          return;
        }
        if (currenttime < p_keyframe.trimstart + p_keyframe.start) {
          object.set('visible', false);
          inst.renderAll();
        } else if (
          currenttime > p_keyframe.end ||
          currenttime > duration
        ) {
          object.set('visible', false);
          inst.renderAll();
        } else {
          object.set('visible', true);
          inst.renderAll();
        }
        if (currenttime >= p_keyframe.trimstart + p_keyframe.start) {
          props.forEach(function (prop) {
            checkAnyKeyframe(keyframe.id, prop, inst, idx);
          });
        }
      }

      // Copy of setObjectValue function, seems to perform better inside the function
      function setValue(prop, object, value, inst) {
        if (object.get('assetType') == 'audio' && play) {
          if (object.get('src')) {
            object.get('src').volume = value;
            object.set('volume', value);
          }
          return false;
        }
        if (object.get('type') != 'group') {
          if (object.group) {
            /*
                        var group = object.group;
                        tempgroup = group._objects;
                        group._restoreObjectsState();
                        canvas.setActiveObject(group);
                        inst.remove(canvas.getActiveObject());
                        canvas.discardActiveObject();
                        inst.renderAll();
                        for (var i = 0; i < tempgroup.length; i++) {
                            inst.add(tempgroup[i]);
                        }
												*/
          }
        }
        if (prop == 'left' && !recording) {
          object.set(prop, value + artboard.get('left'));
        } else if (prop == 'top' && !recording) {
          object.set(prop, value + artboard.get('top'));
        } else if (prop == 'shadow.blur') {
          object.shadow.blur = value;
        } else if (prop == 'shadow.color') {
          object.shadow.color = value;
        } else if (prop == 'shadow.offsetX') {
          object.shadow.offsetX = value;
        } else if (prop == 'shadow.offsetY') {
          object.shadow.offsetY = value;
        } else if (object.get('type') != 'group') {
          object.set(prop, value);
        } else if (prop != 'width') {
          object.set(prop, value);
        }
        inst.renderAll();
      }

      var object = canvas.getItemById(keyframe.id);
      var kf_p_keyframe = p_keyframes.find(
        (x) => x.id == keyframe.id
      );
      if (!object || !kf_p_keyframe) {
        return;
      }
      if (
        keyframe.t >= time &&
        currenttime >=
          kf_p_keyframe.trimstart + kf_p_keyframe.start
      ) {
        var delay = 0;
        var start = false;
        var lasttime, lastprop;
        // Find last keyframe in time from same object & property
        var lastkey = lastKeyframe(keyframe, idx);
        if (!lastkey) {
          lasttime = 0;
          lastprop = getDefaultValue(keyframe.id, keyframe.name);
          if (lastprop === undefined) {
            return;
          }
        } else {
          lasttime = lastkey.t;
          lastprop = lastkey.value;
        }
        if (lastkey && lastkey.t >= time && !play) {
          return;
        }
        // Set delay for the animation if playing
        if (play) {
          if (lasttime > currenttime) {
            delay = lasttime - time;
          }
        }
        // Initiate the animation
        var animation = {
          value: lastprop,
        };
        var instance = anime({
          targets: animation,
          delay: delay,
          value: keyframe.value,
          duration: keyframe.t - lasttime,
          easing: keyframe.easing,
          autoplay: false,
          update: function () {
            if (start && !paused) {
              if (
                currenttime <
                  kf_p_keyframe.trimstart + kf_p_keyframe.start ||
                currenttime > kf_p_keyframe.end ||
                currenttime > duration
              ) {
                object.set('visible', false);
                inst.renderAll();
              } else {
                setValue(
                  keyframe.name,
                  object,
                  animation.value,
                  inst
                );
                object.set('visible', true);
                inst.renderAll();
              }
            } else if (start && paused) {
              anime.remove(animation);
            }
          },
          changeBegin: function () {
            start = true;
          },
        });

        if (time - lasttime <= 0) {
          instance.seek(0);
        } else {
          instance.seek(time - lasttime);
        }

        if (play) {
          instance.play();
        } else if (
          parseFloat(lasttime) <= parseFloat(time) &&
          parseFloat(keyframe.t) >= parseFloat(time)
        ) {
          setValue(keyframe.name, object, animation.value, inst);
        }
      } else if (
        keyframe.t < time &&
        !nextKeyframe(keyframe, idx)
      ) {
        var prop = keyframe.name;
        if (prop == 'left' && !recording) {
          if (
            object.get('left') - artboard.get('left') !=
            keyframe.value
          ) {
            setValue(keyframe.name, object, keyframe.value, inst);
          }
        } else if (prop == 'top' && !recording) {
          if (
            object.get('top') - artboard.get('top') !=
            keyframe.value
          ) {
            setValue(keyframe.name, object, keyframe.value, inst);
          }
        } else if (prop == 'shadow.blur') {
          if (object.shadow.blur != keyframe.value) {
            setValue(keyframe.name, object, keyframe.value, inst);
          }
        } else if (prop == 'shadow.color') {
          if (object.shadow.color != keyframe.value) {
            setValue(keyframe.name, object, keyframe.value, inst);
          }
        } else if (prop == 'shadow.offsetX') {
          if (object.shadow.offsetX != keyframe.value) {
            setValue(keyframe.name, object, keyframe.value, inst);
          }
        } else if (prop == 'shadow.offsetY') {
          if (object.shadow.offsetY != keyframe.value) {
            setValue(keyframe.name, object, keyframe.value, inst);
          }
        } else {
          if (object.get(prop) != keyframe.value) {
            setValue(keyframe.name, object, keyframe.value, inst);
          }
        }
      }
    });
    /*
        if (play) {
            p_keyframes.forEach(function(keyframe){
                inst.getItemById(keyframe.id).set("visible", false);
                window.setTimeout(function(){
                    if (!paused) {
                        inst.getItemById(keyframe.id).set("visible", true);
                        inst.renderAll();
                    }
                }, keyframe.start-time)
                window.setTimeout(function(){
                    if (!paused) {
                        inst.getItemById(keyframe.id).set("visible", false);
                        inst.renderAll();
                    }
                }, keyframe.end-time)
            })
        }
        */
    objects.forEach(function (object) {
      if (object.id.indexOf('Group') == -1) {
        const object2 = canvas.getItemById(object.id);
        const p_keyframe = p_keyframes.find((x) => x.id == object.id);
        if (!object2 || !p_keyframe) {
          return;
        }
        if (currenttime < p_keyframe.trimstart + p_keyframe.start) {
          object2.set('visible', false);
        } else if (
          currenttime > p_keyframe.end ||
          currenttime > duration
        ) {
          object2.set('visible', false);
        } else {
          object2.set('visible', true);
        }
        if (currenttime >= p_keyframe.trimstart + p_keyframe.start) {
          props.forEach(function (prop) {
            checkAnyKeyframe(object.id, prop, inst, idx);
          });
        }
      }
      var obj = canvas.getItemById(object.id);
      if (obj && obj.type == 'lottie') {
        obj.goToSeconds(currenttime);
        inst.renderAll();
      }
    });
    inst.renderAll();

    if (animatedtext.length > 0) {
      animatedtext.forEach(function (text) {
        text.seek(currenttime, canvas);
        inst.renderAll();
      });
    }

    playVideos(time);
    if (play) {
      playAudio(time);
    }
    if (play && !paused) {
      var animation = {
        value: 0,
      };
      var main_instance = anime({
        targets: animation,
        value: [currenttime, duration],
        duration: duration - currenttime,
        easing: 'linear',
        autoplay: true,
        update: function () {
          if (!paused) {
            currenttime = animation.value;
            if (animatedtext.length > 0) {
              animatedtext.forEach(function (text) {
                text.seek(currenttime, canvas);
                inst.renderAll();
              });
            }
            objects.forEach(function (object) {
              if (object.id.indexOf('Group') == -1) {
                const object2 = inst.getItemById(object.id);
                const p_keyframe = p_keyframes.find(
                  (x) => x.id == object.id
                );
                if (!object2 || !p_keyframe) {
                  return;
                }
                if (
                  currenttime <
                  p_keyframe.trimstart + p_keyframe.start
                ) {
                  object2.set('visible', false);
                } else if (
                  currenttime > p_keyframe.end ||
                  currenttime > duration
                ) {
                  object2.set('visible', false);
                } else {
                  object2.set('visible', true);
                }
                if (
                  currenttime >=
                  p_keyframe.trimstart + p_keyframe.start
                ) {
                  props.forEach(function (prop) {
                    checkAnyKeyframe(object.id, prop, inst, idx);
                  });
                }
              }
              var obj = canvas.getItemById(object.id);
              if (obj && obj.type == 'lottie') {
                obj.goToSeconds(currenttime);
                inst.renderAll();
              }
            });
            inst.renderAll();
            if (!recording) {
              renderTime();
              $('#seekbar').css({
                left: currenttime / timelinetime + offset_left,
              });
            }
          } else {
            pause();
            animation.value = duration + 1;
            anime.remove(animation);
          }
        },
        complete: function () {
          pause();
        },
      });
    } else if (paused) {
      currenttime = time;
    }
  }
}

// Keep group keyframes last so children animate before their group regroups
function sortKeyframes() {
  keyframes.sort(function (a, b) {
    if (a.id.indexOf('Group') >= 0 && b.id.indexOf('Group') == -1) {
      return 1;
    } else if (
      b.id.indexOf('Group') >= 0 &&
      a.id.indexOf('Group') == -1
    ) {
      return -1;
    } else {
      return 0;
    }
  });
}

// Render a keyframe.
// data-time is ALWAYS the absolute timeline time, because every lookup keys
// off it. The CSS offset is relative, since the row it lives in is already
// shifted by the layer start.
function renderKeyframe(object, prop, time) {
  const entry = objects.find((x) => x.id == object.get('id'));
  const p_keyframe = p_keyframes.find(
    (x) => x.id == object.get('id')
  );
  if (!entry || !p_keyframe) {
    return;
  }
  const color = entry.color;
  const rowclass = prop == 'shadow.color' ? 'shadowcolor' : prop;
  const row = $('#' + object.get('id')).find('.' + rowclass);
  if (row.length == 0) {
    return;
  }
  const displaytime = time - parseFloat(p_keyframe.start);
  row.prepend(
    "<div class='keyframe' data-time='" +
      time +
      "' data-object='" +
      object.get('id') +
      "' data-property='" +
      prop +
      "'></div>"
  );
  row
    .find("[data-time='" + time + "'][data-property='" + prop + "']")
    .css({ left: displaytime / timelinetime, background: color });
}

// Create a keyframe
function newKeyframe(property, object, time, value, render) {
  // Check if property can be animated
  if (
    $.inArray(
      property,
      objects.find((x) => x.id == object.get('id')).animate
    ) != -1
  ) {
    const keyarr = $.grep(keyframes, function (e) {
      return (
        e.t == parseFloat(time) &&
        e.id == object.get('id') &&
        e.name == property
      );
    });
    const keyarr2 = $.grep(keyframes, function (e) {
      return e.id == object.get('id') && e.name == property;
    });
    if (keyarr2.length == 0) {
      if (property == 'left') {
        setDefaultValue(
          object,
          property,
          object.get(property) - artboard.get('left')
        );
      } else if (property == 'top') {
        setDefaultValue(
          object,
          property,
          object.get(property) - artboard.get('top')
        );
      } else {
        setDefaultValue(object, property, value);
      }
    }
    if (keyarr.length == 0) {
      if (property == 'left') {
        keyframes.push({
          t: time,
          name: property,
          value: value - artboard.get('left'),
          id: object.get('id'),
          easing: 'linear',
        });
      } else if (property == 'top') {
        keyframes.push({
          t: time,
          name: property,
          value: value - artboard.get('top'),
          id: object.get('id'),
          easing: 'linear',
        });
      } else {
        keyframes.push({
          t: time,
          name: property,
          value: value,
          id: object.get('id'),
          easing: 'linear',
        });
      }
      if (
        render &&
        property != 'top' &&
        property != 'scaleY' &&
        property != 'width' &&
        property != 'height' &&
        property != 'stroke' &&
        property != 'shadow.opacity' &&
        property != 'shadow.offsetX' &&
        property != 'shadow.offsetY' &&
        property != 'shadow.blur' &&
        property != 'lineHeight'
      ) {
        renderKeyframe(object, property, time);
      }
      sortKeyframes();
    } else if (render) {
      if (
        property != 'top' &&
        property != 'scaleY' &&
        property != 'width' &&
        property != 'height' &&
        property != 'stroke' &&
        property != 'shadow.opacity' &&
        property != 'shadow.offsetX' &&
        property != 'shadow.offsetY' &&
        property != 'shadow.blur' &&
        property != 'lineHeight'
      ) {
        updateKeyframe(
          $('#' + object.get('id')).find(
            ".keyframe[data-time='" +
              time +
              "'][data-property='" +
              property +
              "']"
          ),
          true
        );
      }
    }
  } else {
    if (property == 'left') {
      setDefaultValue(
        object,
        property,
        object.get(property) - artboard.get('left')
      );
    } else if (property == 'top') {
      setDefaultValue(
        object,
        property,
        object.get(property) - artboard.get('top')
      );
    } else {
      setDefaultValue(object, property, value);
    }
  }
}

// Create a keyframe (manually)
function manualKeyframe() {
  var prop = $(this).parent().attr('data-property');
  const object = canvas.getItemById(
    $(this).parent().parent().parent().attr('data-object')
  );
  if (prop == 'position') {
    prop = 'left';
    newKeyframe('top', object, currenttime, object.get('top'), true);
  } else if (prop == 'scale') {
    prop = 'scaleX';
    newKeyframe(
      'scaleY',
      object,
      currenttime,
      object.get('scaleY'),
      true
    );
    newKeyframe(
      'width',
      object,
      currenttime,
      object.get('width'),
      true
    );
    newKeyframe(
      'height',
      object,
      currenttime,
      object.get('height'),
      true
    );
  } else if (prop == 'stroke') {
    prop = 'strokeWidth';
    newKeyframe(
      'stroke',
      object,
      currenttime,
      object.get('stroke'),
      true
    );
  } else if (prop == 'shadow') {
    prop = 'shadow.color';
    // fabric's get() is not a path getter - read the shadow object directly
    newKeyframe(
      'shadow.opacity',
      object,
      currenttime,
      object.shadow.opacity,
      true
    );
    newKeyframe(
      'shadow.offsetX',
      object,
      currenttime,
      object.shadow.offsetX,
      true
    );
    newKeyframe(
      'shadow.offsetY',
      object,
      currenttime,
      object.shadow.offsetY,
      true
    );
    newKeyframe(
      'shadow.blur',
      object,
      currenttime,
      object.shadow.blur,
      true
    );
  } else if (prop == 'text') {
    prop = 'charSpacing';
    newKeyframe(
      'lineHeight',
      object,
      currenttime,
      object.get('lineHeight'),
      true
    );
  }
  newKeyframe(prop, object, currenttime, getPropValue(object, prop), true);
  save();
}
$(document).on('click', '.property-keyframe', manualKeyframe);

// Freeze all properties (this is counterintuitve because I initially programmed it to work the other way around)
function toggleAnimate(e) {
  e.stopPropagation();
  const object = canvas.getItemById(
    $(this).parent().parent().parent().attr('data-object')
  );
  // Turn off clock -> Stop animation
  if ($(this).hasClass('frozen')) {
    $(this).removeClass('frozen');
    $(this).attr('src', 'assets/freeze.svg');
    objects.find((x) => x.id == object.get('id')).animate = [];
    // Turn on clock -> Start animation
  } else {
    $(this).addClass('frozen');
    $(this).attr('src', 'assets/frozen.svg');
    objects.find((x) => x.id == object.get('id')).animate = [];
    if (object.get('assetType') == 'audio') {
      objects
        .find((x) => x.id == object.get('id'))
        .animate.push('volume');
      keyframes = $.grep(keyframes, function (e) {
        return e.id != object.get('id');
      });
      $(".keyframe[data-object='" + object.get('id') + "']").remove();
      $(this)
        .parent()
        .parent()
        .parent()
        .find('.freeze-prop')
        .removeClass('frozen');
      newKeyframe('volume', object, 0, 0.5, true);
    } else {
      props.forEach(function (prop) {
        objects
          .find((x) => x.id == object.get('id'))
          .animate.push(prop);
      });
      keyframes = $.grep(keyframes, function (e) {
        return e.id != object.get('id');
      });
      $(".keyframe[data-object='" + object.get('id') + "']").remove();
      $(this)
        .parent()
        .parent()
        .parent()
        .find('.freeze-prop')
        .removeClass('frozen');

      props.forEach(function (prop) {
        if (prop == 'lineHeight' || prop == 'charSpacing') {
          if (object.get('type') == 'textbox') {
            newKeyframe(prop, object, 0, object.get(prop), true);
          }
        } else if (
          prop == 'shadow.opacity' ||
          prop == 'shadow.blur' ||
          prop == 'shadow.offsetX' ||
          prop == 'shadow.offsetY' ||
          prop == 'shadow.color'
        ) {
          if (object.get('type') != 'group') {
            if (prop == 'shadow.color') {
              newKeyframe(prop, object, 0, object.shadow.color, true);
            } else if (prop == 'shadow.opacity') {
              newKeyframe(
                prop,
                object,
                0,
                object.shadow.opacity,
                true
              );
            } else if (prop == 'shadow.offsetX') {
              newKeyframe(
                prop,
                object,
                0,
                object.shadow.offsetX,
                true
              );
            } else if (prop == 'shadow.offsetY') {
              newKeyframe(
                prop,
                object,
                0,
                object.shadow.offsetY,
                true
              );
            } else if (prop == 'shadow.blur') {
              newKeyframe(prop, object, 0, object.shadow.blur, true);
            }
          }
        } else {
          newKeyframe(prop, object, 0, object.get(prop), true);
        }
      });
    }
  }
  save();
}
$(document).on('click', '.freeze', toggleAnimate);

function animateProp(prop, object) {
  objects.find((x) => x.id == object.get('id')).animate.push(prop);

  // Prop counterparts
  if (prop == 'left') {
    objects.find((x) => x.id == object.get('id')).animate.push('top');
    newKeyframe(
      'left',
      object,
      currenttime,
      object.get('left'),
      true
    );
    newKeyframe('top', object, currenttime, object.get('top'), true);
    setDefaultValue(object, 'left', object.get('left') - artboard.get('left'));
    setDefaultValue(object, 'top', object.get('top') - artboard.get('top'));
  } else if (prop == 'scaleX') {
    newKeyframe(
      'scaleY',
      object,
      currenttime,
      object.get('scaleY'),
      true
    );
    newKeyframe(
      'width',
      object,
      currenttime,
      object.get('width'),
      true
    );
    newKeyframe(
      'height',
      object,
      currenttime,
      object.get('height'),
      true
    );
    objects
      .find((x) => x.id == object.get('id'))
      .animate.push('scaleY');
    objects
      .find((x) => x.id == object.get('id'))
      .animate.push('width');
    objects
      .find((x) => x.id == object.get('id'))
      .animate.push('height');
    setDefaultValue(object, 'height', object.get('height'));
    setDefaultValue(object, 'width', object.get('width'));
    setDefaultValue(object, 'scaleY', object.get('scaleY'));
  } else if (prop == 'strokeWidth') {
    newKeyframe(
      'stroke',
      object,
      currenttime,
      object.get('stroke'),
      true
    );
    setDefaultValue(object, 'stroke', object.get('stroke'));
    objects
      .find((x) => x.id == object.get('id'))
      .animate.push('stroke');
  } else if (prop == 'shadow.color') {
    newKeyframe(
      'shadow.color',
      object,
      currenttime,
      object.shadow.color,
      true
    );
    newKeyframe(
      'shadow.opacity',
      object,
      currenttime,
      object.shadow.opacity,
      true
    );
    newKeyframe(
      'shadow.offsetX',
      object,
      currenttime,
      object.shadow.offsetX,
      true
    );
    newKeyframe(
      'shadow.offsetY',
      object,
      currenttime,
      object.shadow.offsetY,
      true
    );
    newKeyframe(
      'shadow.blur',
      object,
      currenttime,
      object.shadow.blur,
      true
    );
    setDefaultValue(object, 'shadow.color', object.shadow.color);
    setDefaultValue(object, 'shadow.opacity', object.shadow.opacity);
    setDefaultValue(object, 'shadow.offsetX', object.shadow.offsetX);
    setDefaultValue(object, 'shadow.offsetY', object.shadow.offsetY);
    setDefaultValue(object, 'shadow.blur', object.shadow.blur);
    objects
      .find((x) => x.id == object.get('id'))
      .animate.push('shadow.opacity');
    objects
      .find((x) => x.id == object.get('id'))
      .animate.push('shadow.offsetX');
    objects
      .find((x) => x.id == object.get('id'))
      .animate.push('shadow.offsetY');
    objects
      .find((x) => x.id == object.get('id'))
      .animate.push('shadow.blur');
  } else if (prop == 'charSpacing') {
    newKeyframe(
      'lineHeight',
      object,
      currenttime,
      object.get('lineHeight'),
      true
    );
    setDefaultValue(object, 'lineHeight', object.get('lineHeight'));
    objects
      .find((x) => x.id == object.get('id'))
      .animate.push('lineHeight');
  }

  // Exception
  if (prop != 'left' && prop != 'shadow.color') {
    newKeyframe(prop, object, currenttime, object.get(prop), true);
    setDefaultValue(object, prop, object.get(prop));
  }
}

function freezeProp(prop, object) {
  objects.find((x) => x.id == object.get('id')).animate = $.grep(
    objects.find((x) => x.id == object.get('id')).animate,
    function (e) {
      return e != prop;
    }
  );
  // Also add prop counterparts (should probably have done in a better way)
  if (prop == 'left') {
    objects.find((x) => x.id == object.get('id')).animate = $.grep(
      objects.find((x) => x.id == object.get('id')).animate,
      function (e) {
        return e != 'top';
      }
    );
    keyframes = $.grep(keyframes, function (e) {
      return e.id != object.get('id') || e.name != 'top';
    });
    setDefaultValue(object, 'left', object.get('left') - artboard.get('left'));
    setDefaultValue(object, 'top', object.get('top') - artboard.get('top'));
  } else if (prop == 'scaleX') {
    setDefaultValue(object, 'height', object.get('height'));
    setDefaultValue(object, 'width', object.get('width'));
    setDefaultValue(object, 'scaleY', object.get('scaleY'));
    objects.find((x) => x.id == object.get('id')).animate = $.grep(
      objects.find((x) => x.id == object.get('id')).animate,
      function (e) {
        return e != 'scaleY';
      }
    );
    keyframes = $.grep(keyframes, function (e) {
      return e.id != object.get('id') || e.name != 'scaleY';
    });
    objects.find((x) => x.id == object.get('id')).animate = $.grep(
      objects.find((x) => x.id == object.get('id')).animate,
      function (e) {
        return e != 'width';
      }
    );
    keyframes = $.grep(keyframes, function (e) {
      return e.id != object.get('id') || e.name != 'width';
    });
    objects.find((x) => x.id == object.get('id')).animate = $.grep(
      objects.find((x) => x.id == object.get('id')).animate,
      function (e) {
        return e != 'height';
      }
    );
    keyframes = $.grep(keyframes, function (e) {
      return e.id != object.get('id') || e.name != 'height';
    });
  } else if (prop == 'strokeWidth') {
    setDefaultValue(object, 'stroke', object.get('stroke'));
    objects.find((x) => x.id == object.get('id')).animate = $.grep(
      objects.find((x) => x.id == object.get('id')).animate,
      function (e) {
        return e != 'stroke';
      }
    );
    keyframes = $.grep(keyframes, function (e) {
      return e.id != object.get('id') || e.name != 'stroke';
    });
  } else if (prop == 'shadow.color') {
    setDefaultValue(object, 'shadow.opacity', object.shadow.opacity);
    setDefaultValue(object, 'shadow.offsetX', object.shadow.offsetX);
    setDefaultValue(object, 'shadow.offsetY', object.shadow.offsetY);
    setDefaultValue(object, 'shadow.blur', object.shadow.blur);
    keyframes = $.grep(keyframes, function (e) {
      return e.id != object.get('id') || e.name != 'shadow.opacity';
    });
    objects.find((x) => x.id == object.get('id')).animate = $.grep(
      objects.find((x) => x.id == object.get('id')).animate,
      function (e) {
        return e != 'shadow.opacity';
      }
    );
    keyframes = $.grep(keyframes, function (e) {
      return e.id != object.get('id') || e.name != 'shadow.offsetX';
    });
    objects.find((x) => x.id == object.get('id')).animate = $.grep(
      objects.find((x) => x.id == object.get('id')).animate,
      function (e) {
        return e != 'shadow.offsetX';
      }
    );
    keyframes = $.grep(keyframes, function (e) {
      return e.id != object.get('id') || e.name != 'shadow.offsetY';
    });
    objects.find((x) => x.id == object.get('id')).animate = $.grep(
      objects.find((x) => x.id == object.get('id')).animate,
      function (e) {
        return e != 'shadow.offsetY';
      }
    );
    keyframes = $.grep(keyframes, function (e) {
      return e.id != object.get('id') || e.name != 'shadow.blur';
    });
    objects.find((x) => x.id == object.get('id')).animate = $.grep(
      objects.find((x) => x.id == object.get('id')).animate,
      function (e) {
        return e != 'shadow.blur';
      }
    );
  } else if (prop == 'charSpacing') {
    setDefaultValue(object, 'lineHeight', object.get('lineHeight'));
    objects.find((x) => x.id == object.get('id')).animate = $.grep(
      objects.find((x) => x.id == object.get('id')).animate,
      function (e) {
        return e != 'lineHeight';
      }
    );
    keyframes = $.grep(keyframes, function (e) {
      return e.id != object.get('id') || e.name != 'lineHeight';
    });
  }

  keyframes = $.grep(keyframes, function (e) {
    return e.id != object.get('id') || e.name != prop;
  });

  // Exception
  if (prop != 'left' && prop != 'shadow.color') {
    setDefaultValue(object, prop, object.get(prop));
  }

  $(
    ".keyframe[data-object='" +
      object.get('id') +
      "'][data-property='" +
      prop +
      "']"
  ).remove();
}

// Toggle animation mode for property
function toggleAnimateProp(e) {
  e.stopPropagation();
  var prop = $(this).parent().attr('data-property');
  const object = canvas.getItemById(
    $(this).parent().parent().parent().attr('data-object')
  );
  if (prop == 'position') {
    prop = 'left';
  } else if (prop == 'scale') {
    prop = 'scaleX';
  } else if (prop == 'stroke') {
    prop = 'strokeWidth';
  } else if (prop == 'shadow') {
    prop = 'shadow.color';
  } else if (prop == 'text') {
    prop = 'charSpacing';
  }

  // Check layer global "freezing" state
  if (
    $(this)
      .parent()
      .parent()
      .parent()
      .find('.freeze')
      .hasClass('frozen')
  ) {
    objects.find((x) => x.id == object.get('id')).animate = [];
    $(this)
      .parent()
      .parent()
      .parent()
      .find('.freeze')
      .removeClass('frozen');

    // Stop animating all props except selected
    var propmatch = [
      prop,
      'scaleY',
      'width',
      'height',
      'top',
      'stroke',
      'shadow.opacity',
      'shadow.offsetX',
      'shadow.offsetY',
      'shadow.blur',
      'lineHeight',
    ];
    props.forEach(function (p) {
      if ($.inArray(p, propmatch) == -1) {
        if (
          (object.get('type') == 'textbox' && p == 'charSpacing') ||
          p == 'lineHeight'
        ) {
          freezeProp(p, object);
        } else if (p != 'charSpacing' && p != 'lineHeight') {
          freezeProp(p, object);
        }
      }
    });
  }

  // Turn off clock -> Stop animating
  if ($(this).hasClass('frozen')) {
    $(this).removeClass('frozen');
    $(this).attr('src', 'assets/freeze.svg');
    freezeProp(prop, object);
    // Turn on clock -> Animate
  } else {
    $(this).addClass('frozen');
    $(this).attr('src', 'assets/frozen.svg');
    animateProp(prop, object);
  }
  save();
}
$(document).on('click', '.freeze-prop', toggleAnimateProp);

// Lock layer
function lockLayer(e) {
  e.stopPropagation();
  const object = canvas.getItemById(
    $(this).parent().parent().parent().attr('data-object')
  );
  if ($(this).hasClass('locked')) {
    $(this).removeClass('locked');
    $(this).attr('src', 'assets/lock.svg');
    object.selectable = true;
    $(this)
      .parent()
      .parent()
      .find('.layer-handle')
      .attr('draggable', true);
  } else {
    $(this).addClass('locked');
    $(this).attr('src', 'assets/locked.svg');
    object.selectable = false;
    if (canvas.getActiveObject() == object) {
      canvas.discardActiveObject();
      canvas.renderAll();
    }
    $(this)
      .parent()
      .parent()
      .find('.layer-handle')
      .attr('draggable', false);
  }
  save();
}
$(document).on('click', '.lock', lockLayer);

// Center an object in the canvas
function centerObject(object) {
  object.set('top', artboard.get('top') + artboard.get('height') / 2);
  object.set(
    'left',
    artboard.get('left') + artboard.get('width') / 2
  );
  canvas.renderAll();
  save();
}

// Render a layer
function renderLayer(object, animate = false) {
  const entry = objects.find((x) => x.id == object.get('id'));
  const p_keyframe = p_keyframes.find(
    (x) => x.id == object.get('id')
  );
  if (!entry || !p_keyframe) {
    return;
  }
  $('#nolayers').addClass('yaylayers');
  const color = entry.color;
  var src = '';
  var classlock = '';
  var srclock = 'lock';
  var freeze = 'freeze';
  if (object.get('type') == 'textbox') {
    src = 'assets/text.svg';
  } else if (
    object.get('type') == 'rect' ||
    object.get('type') == 'group' ||
    object.get('type') == 'circle' ||
    object.get('type') == 'path'
  ) {
    src = 'assets/star.svg';
    if (object.get('assetType') == 'animatedText') {
      src = 'assets/text.svg';
    }
    if (object.get('assetType') == 'audio') {
      src = 'assets/audio.svg';
    }
  } else if (object.get('type') == 'image') {
    if (
      object.get('assetType') &&
      object.get('assetType') == 'video'
    ) {
      src = 'assets/video.svg';
    } else {
      src = 'assets/image.svg';
    }
  } else if (object.get('type') == 'lottie') {
    src = 'assets/zappy.svg';
  } else if (object.get('assetType') == 'audio') {
    src = 'assets/audio.svg';
  }
  if (object.selectable == false) {
    classlock = 'locked';
    srclock = 'locked';
  }
  if (animate != false) {
    freeze = 'frozen';
  }
  const leftoffset = p_keyframe.trimstart / timelinetime;
  const width =
    (p_keyframe.end - p_keyframe.trimstart) / timelinetime;
  $('#inner-timeline').prepend(
    "<div class='object-props' id='" +
      object.get('id') +
      "' style='width:" +
      (p_keyframe.end - p_keyframe.start) / timelinetime +
      "px'><div class='row main-row'><div class='row-el' style='background-color:" +
      color +
      "'><div class='trim-row' style='left:" +
      leftoffset +
      'px;width:' +
      width +
      "px'></div></div></div></div>"
  );
  if (object.get('assetType') == 'audio') {
    object.setControlsVisibility({
      mt: false,
      mb: false,
      ml: false,
      mr: false,
    });
    $('#layer-inner-list').prepend(
      "<div class='layer' data-object='" +
        object.get('id') +
        "'><div class='layer-name'><div class='layer-handle' title='Drag to reorder'></div><img class='droparrow' src='assets/drop-arrow.svg' ><img class='layer-icon' src=" +
        src +
        "><input class='layer-custom-name' value='" +
        entry.label +
        "' readonly></span><div class='layer-options'><img class='" +
        freeze +
        "' src='assets/" +
        freeze +
        ".svg' title='Toggle animation'></div></div><div class='properties'></div></div>"
    );
  } else {
    $('#layer-inner-list').prepend(
      "<div class='layer' data-object='" +
        object.get('id') +
        "'><div class='layer-name'><div class='layer-handle' title='Drag to reorder'></div><img class='droparrow' src='assets/drop-arrow.svg' ><img class='layer-icon' src=" +
        src +
        "><input class='layer-custom-name' value='" +
        entry.label +
        "' readonly></span><div class='layer-options'><img class='lock " +
        classlock +
        "' src='assets/" +
        srclock +
        ".svg' title='Lock layer'><img class='" +
        freeze +
        "' src='assets/" +
        freeze +
        ".svg' title='Toggle animation'></div></div><div class='properties'></div></div>"
    );
  }
  $(".layer[data-object='" + object.get('id') + "']")
    .find('.properties')
    .toggle();
  setTimelineZoom(timelinetime);
  // Make the new layer draggable (html5sortable only wires up the
  // children that exist when it is initialized)
  if (typeof initLayerSortable == 'function') {
    initLayerSortable();
  }
  if (object.selectable == false) {
    $(".layer[data-object='" + object.get('id') + "']")
      .find('.layer-handle')
      .attr('draggable', false);
  }
}

// Render a property
function renderProp(prop, object) {
  var classfreeze = '';
  var srcfreeze = 'freeze';
  const entry = objects.find((x) => x.id == object.get('id'));
  if (!entry) {
    return;
  }
  if ($.inArray(prop, entry.animate) != -1) {
    classfreeze = 'frozen';
    srcfreeze = 'frozen';
  }
  if (prop == 'shadow.color') {
    prop = 'shadowcolor';
  }
  $('#' + object.get('id')).append(
    "<div class='row " +
      prop +
      " keyframe-row' data-object='" +
      object.get('id') +
      "'><div class='row-el'></div></div>"
  );
  if (prop == 'left') {
    $(".layer[data-object='" + object.get('id') + "']")
      .find('.properties')
      .append(
        "<div class='property-name' data-property='position'><span class='property-keyframe' title='Create a new keyframe'></span>Position<img class='freeze-prop " +
          classfreeze +
          "' src='assets/" +
          srcfreeze +
          ".svg' title='Toggle animation'></div>"
      );
  } else if (prop == 'scaleX') {
    $(".layer[data-object='" + object.get('id') + "']")
      .find('.properties')
      .append(
        "<div class='property-name' data-property='scale'><span class='property-keyframe' title='Create a new keyframe'></span>Scale<img class='freeze-prop " +
          classfreeze +
          "' src='assets/" +
          srcfreeze +
          ".svg' title='Toggle animation'></div>"
      );
  } else if (prop == 'strokeWidth') {
    $(".layer[data-object='" + object.get('id') + "']")
      .find('.properties')
      .append(
        "<div class='property-name' data-property='stroke'><span class='property-keyframe' title='Create a new keyframe'></span>Stroke<img class='freeze-prop " +
          classfreeze +
          "' src='assets/" +
          srcfreeze +
          ".svg' title='Toggle animation'></div>"
      );
  } else if (prop == 'shadowcolor') {
    $(".layer[data-object='" + object.get('id') + "']")
      .find('.properties')
      .append(
        "<div class='property-name' data-property='shadow'><span class='property-keyframe' title='Create a new keyframe'></span>Shadow<img class='freeze-prop " +
          classfreeze +
          "' src='assets/" +
          srcfreeze +
          ".svg' title='Toggle animation'></div>"
      );
  } else if (prop == 'charSpacing') {
    $(".layer[data-object='" + object.get('id') + "']")
      .find('.properties')
      .append(
        "<div class='property-name' data-property='text'><span class='property-keyframe' title='Create a new keyframe'></span>Text<img class='freeze-prop " +
          classfreeze +
          "' src='assets/" +
          srcfreeze +
          ".svg' title='Toggle animation'></div>"
      );
  } else {
    $(".layer[data-object='" + object.get('id') + "']")
      .find('.properties')
      .append(
        "<div class='property-name' data-property='" +
          prop +
          "'><span class='property-keyframe' title='Create a new keyframe'></span>" +
          prop +
          "<img class='freeze-prop " +
          classfreeze +
          "' src='assets/" +
          srcfreeze +
          ".svg' title='Toggle animation'></div>"
      );
  }
  $('#' + object.get('id'))
    .find('.keyframe-row' + '.' + prop)
    .toggle();
}

// Create a layer
function newLayer(object) {
  layer_count++;
  var color;
  if (object.get('type') == 'image') {
    if (
      object.get('assetType') &&
      object.get('assetType') == 'video'
    ) {
      color = '#106CF6';
    } else {
      color = '#92F711';
    }
  } else if (object.get('type') == 'textbox') {
    color = '#F7119B';
  } else if (
    object.get('type') == 'rect' ||
    object.get('type') == 'group' ||
    object.get('type') == 'circle' ||
    object.get('type') == 'path'
  ) {
    color = '#9211F7';
    if (object.get('assetType') == 'animatedText') {
      color = '#F7119B';
    } else if (object.get('assetType') == 'audio') {
      color = '#11C0F7';
    }
  } else if (object.get('type') == 'lottie') {
    color = '#F1890E';
  } else {
    color = '#9211F7';
  }
  if (
    (object.get('assetType') && object.get('assetType') == 'video') ||
    object.get('type') == 'lottie' ||
    object.get('assetType') == 'audio'
  ) {
    objects.push({
      object: object,
      id: object.get('id'),
      label: object.get('id'),
      color: color,
      defaults: [],
      locked: [],
      mask: 'none',
      start: 0,
      end: object.get('duration'),
    });
    // Clamp to the project end. This used to be `duration - currenttime`,
    // which shortened every layer added after t=0.
    const mediaend = Math.min(
      object.get('duration') + currenttime,
      duration
    );
    p_keyframes.push({
      start: currenttime,
      end: mediaend,
      trimstart: 0,
      trimend: mediaend,
      object: object,
      id: object.get('id'),
    });
  } else {
    objects.push({
      object: object,
      id: object.get('id'),
      label: object.get('id'),
      color: color,
      defaults: [],
      locked: [],
      mask: 'none',
    });
    p_keyframes.push({
      start: object.get('notnew')
        ? object.get('starttime')
        : currenttime,
      end: duration,
      trimstart: 0,
      trimend: duration,
      object: object,
      id: object.get('id'),
    });
  }
  renderLayer(object);
  if (
    !object.get('assetType') ||
    object.get('assetType') != 'audio'
  ) {
    props.forEach(function (prop) {
      if (prop == 'lineHeight' || prop == 'charSpacing') {
        if (object.get('type') == 'textbox') {
          if (prop != 'lineHeight') {
            renderProp(prop, object);
          }
          objects
            .find((x) => x.id == object.id)
            .defaults.push({ name: prop, value: object.get(prop) });
        }
      } else if (
        prop == 'shadow.opacity' ||
        prop == 'shadow.blur' ||
        prop == 'shadow.offsetX' ||
        prop == 'shadow.offsetY' ||
        prop == 'shadow.color'
      ) {
        if (object.get('type') != 'group') {
          if (prop == 'shadow.color') {
            renderProp(prop, object);
            objects
              .find((x) => x.id == object.id)
              .defaults.push({
                name: prop,
                value: object.shadow.color,
              });
          } else if (prop == 'shadow.blur') {
            objects
              .find((x) => x.id == object.id)
              .defaults.push({
                name: prop,
                value: object.shadow.blur,
              });
          } else if (prop == 'shadow.offsetX') {
            objects
              .find((x) => x.id == object.id)
              .defaults.push({
                name: prop,
                value: object.shadow.offsetX,
              });
          } else if (prop == 'shadow.offsetY') {
            objects
              .find((x) => x.id == object.id)
              .defaults.push({
                name: prop,
                value: object.shadow.offsetY,
              });
          } else if (prop == 'shadow.opacity') {
            objects
              .find((x) => x.id == object.id)
              .defaults.push({
                name: prop,
                value: object.shadow.opacity,
              });
          }
        }
      } else {
        if (
          prop != 'top' &&
          prop != 'scaleY' &&
          prop != 'stroke' &&
          prop != 'width' &&
          prop != 'height'
        ) {
          renderProp(prop, object);
        }
        objects
          .find((x) => x.id == object.id)
          .defaults.push({ name: prop, value: object.get(prop) });
      }
    });
  } else {
    renderProp('volume', object);
    objects
      .find((x) => x.id == object.id)
      .defaults.push({ name: 'volume', value: 0 });
  }
  $('.layer-selected').removeClass('layer-selected');
  $(".layer[data-object='" + object.get('id') + "']").addClass(
    'layer-selected'
  );
  const selectedLayer =
    document.getElementsByClassName('layer-selected')[0];
  if (selectedLayer) {
    selectedLayer.scrollIntoView();
  }
  objects.find((x) => x.id == object.id).animate = [];
  animate(false, currenttime);
  save();
  checkFilter();
}

// Add a (complex) SVG shape to the canvas
function newSVG(svg, x, y, width, center) {
  var svggroup = [];
  fabric.loadSVGFromURL(svg, function (objects, options) {
    var newsvg = objects[0];
    if (objects.length > 1) {
      newsvg = fabric.util.groupSVGElements(objects, options);
    }
    newsvg.set({
      id: 'Shape' + layer_count,
      stroke: '#000',
      left: x,
      top: y,
      strokeWidth: 0,
      strokeUniform: true,
      originX: 'center',
      originY: 'center',
      strokeDashArray: false,
      absolutePositioned: true,
      paintFirst: 'stroke',
      objectCaching: true,
      sourcePath: svg,
      inGroup: false,
      shadow: {
        color: '#000',
        offsetX: 0,
        offsetY: 0,
        blur: 0,
        opacity: 0,
      },
    });
    newsvg.scaleToWidth(width);
    newsvg.set({
      scaleX: parseFloat(newsvg.get('scaleX').toFixed(2)),
      scaleY: parseFloat(newsvg.get('scaleY').toFixed(2)),
    });
    canvas.add(newsvg);
    newLayer(newsvg);
    canvas.setActiveObject(newsvg);
    canvas.bringToFront(newsvg);
    canvas.renderAll();
    if (center) {
      newsvg.set(
        'left',
        artboard.get('left') + artboard.get('width') / 2
      );
      newsvg.set(
        'top',
        artboard.get('top') + artboard.get('height') / 2
      );
      canvas.renderAll();
    }
  });
}

// Add a video to the canvas
function newVideo(file, src, x, y, duration, center) {
  var newvid = new fabric.Image(file, {
    left: x,
    top: y,
    width: file.width,
    height: file.height,
    originX: 'center',
    originY: 'center',
    backgroundColor: 'rgba(255,255,255,0)',
    cursorWidth: 1,
    stroke: '#000',
    strokeUniform: true,
    paintFirst: 'stroke',
    strokeWidth: 0,
    cursorDuration: 1,
    cursorDelay: 250,
    source: src,
    duration: duration * 1000,
    assetType: 'video',
    id: 'Video' + layer_count,
    objectCaching: false,
    strokeDashArray: false,
    inGroup: false,
    shadow: {
      color: '#000',
      offsetX: 0,
      offsetY: 0,
      blur: 0,
      opacity: 0,
    },
  });
  files.push({ name: newvid.get('id'), file: src });
  newvid.saveElem = newvid.getElement();
  canvas.add(newvid);
  if (newvid.get('width') > artboard.get('width')) {
    newvid.scaleToWidth(artboard.get('width'));
  }
  newvid.scaleToWidth(150);
  canvas.renderAll();
  if (window.duration < newvid.duration + currenttime) {
    window.duration =
      ((newvid.duration + currenttime) / 1000).toFixed(2) * 1000;
  }
  newLayer(newvid);
  canvas.setActiveObject(newvid);
  canvas.bringToFront(newvid);
  if (center) {
    newvid.set(
      'left',
      artboard.get('left') + artboard.get('width') / 2
    );
    newvid.set(
      'top',
      artboard.get('top') + artboard.get('height') / 2
    );
    canvas.renderAll();
  }
  $('#load-video').removeClass('loading-active');
}

// Load a video
function loadVideo(src, x, y, center) {
  var vidObj = document.createElement('video');
  var vidSrc = document.createElement('source');
  vidSrc.src = src;
  vidObj.crossOrigin = 'anonymous';
  vidObj.appendChild(vidSrc);
  vidObj.addEventListener('error', function () {
    console.error('Could not load video', src);
    $('#load-video').removeClass('loading-active');
  });
  vidObj.addEventListener('loadeddata', function () {
    vidObj.width = this.videoWidth;
    vidObj.height = this.videoHeight;
    vidObj.currentTime = 0;
    vidObj.muted = false;
    function waitLoad() {
      if (vidObj.readyState >= 3) {
        newVideo(vidObj, src, x, y, vidObj.duration, center);
      } else {
        window.setTimeout(function () {
          waitLoad();
        }, 100);
      }
    }
    window.setTimeout(function () {
      waitLoad();
    }, 100);
  });
  vidObj.currentTime = 0;
}

// Check that crop controls are inside image
function checkCrop(obj) {
  if (obj.isContainedWithinObject(cropobj)) {
    croptop = obj.get('top');
    cropleft = obj.get('left');
    cropscalex = obj.get('scaleX');
    cropscaley = obj.get('scaleY');
  } else {
    obj.top = croptop;
    obj.left = cropleft;
    obj.scaleX = cropscalex;
    obj.scaleY = cropscaley;
    obj.setCoords();
    obj.saveState();
  }
  obj.set({
    borderColor: '#51B9F9',
  });
  canvas.renderAll();
  crop(canvas.getItemById('cropped'));
}

// Rotate a vector by `radians` around the origin
function rotateVector(x, y, radians) {
  if (!radians) {
    return { x: x, y: y };
  }
  var cos = Math.cos(radians);
  var sin = Math.sin(radians);
  return { x: x * cos - y * sin, y: x * sin + y * cos };
}

// Perform a crop
//
// Everything is computed in the image's own frame rather than along the canvas
// axes, so a rotated image crops the region the crop window actually covers.
// The previous version compared canvas-space edges and only handled three of
// the four quadrants, which cropped the wrong area of any rotated image (and
// nothing at all when the crop window was centred on it).
function crop(obj) {
  var cropUI = canvas.getItemById('crop');
  if (!obj || !cropUI || !cropobj) {
    return;
  }
  cropobj.setCoords();
  cropUI.setCoords();
  // Read everything before writing: the final call passes cropobj as `obj`
  var scaleX = cropobj.get('scaleX') || 1;
  var scaleY = cropobj.get('scaleY') || 1;
  var ogWidth = cropobj.get('width');
  var ogHeight = cropobj.get('height');
  var centerX = cropobj.get('left');
  var centerY = cropobj.get('top');
  var radians = fabric.util.degreesToRadians(
    cropobj.get('angle') || 0
  );

  // The crop window carries the image's angle, so its own width and height are
  // already axis-aligned with the image
  var width = (cropUI.get('width') * cropUI.get('scaleX')) / scaleX;
  var height = (cropUI.get('height') * cropUI.get('scaleY')) / scaleY;

  // Where the crop window sits relative to the image centre, un-rotated
  var offset = rotateVector(
    cropUI.get('left') - centerX,
    cropUI.get('top') - centerY,
    -radians
  );
  var cropX = offset.x / scaleX + ogWidth / 2 - width / 2;
  var cropY = offset.y / scaleY + ogHeight / 2 - height / 2;

  // Never leave the source bitmap: fabric draws nothing for a negative crop
  width = Math.max(1, Math.min(width, ogWidth));
  height = Math.max(1, Math.min(height, ogHeight));
  cropX = Math.max(0, Math.min(cropX, ogWidth - width));
  cropY = Math.max(0, Math.min(cropY, ogHeight - height));

  // Put the cropped object back under the (clamped) region it represents
  var center = rotateVector(
    (cropX + width / 2 - ogWidth / 2) * scaleX,
    (cropY + height / 2 - ogHeight / 2) * scaleY,
    radians
  );
  obj
    .set({
      cropX: cropX,
      cropY: cropY,
      width: width,
      height: height,
      left: centerX + center.x,
      top: centerY + center.y,
    })
    .setCoords();
  canvas.renderAll();
  if (obj.get('id') != 'cropped') {
    canvas.remove(cropUI);
    canvas.remove(canvas.getItemById('crop-overlay'));
    canvas.remove(canvas.getItemById('cropped'));
    cropping = false;
    resetControls();
    canvas.uniformScaling = true;
    canvas.renderAll();
    newKeyframe('scaleX', obj, currenttime, obj.get('scaleX'), true);
    newKeyframe('scaleY', obj, currenttime, obj.get('scaleY'), true);
    newKeyframe('width', obj, currenttime, obj.get('width'), true);
    newKeyframe('height', obj, currenttime, obj.get('height'), true);
    newKeyframe('left', obj, currenttime, obj.get('left'), true);
    newKeyframe('top', obj, currenttime, obj.get('top'), true);
    $('#properties-overlay').removeClass('properties-disabled');
    save();
  }
  canvas.renderAll();
}

var tlcrop = new Image();
tlcrop.src = 'assets/tlcrop.svg';
var trcrop = new Image();
trcrop.src = 'assets/trcrop.svg';
var blcrop = new Image();
blcrop.src = 'assets/blcrop.svg';
var brcrop = new Image();
brcrop.src = 'assets/brcrop.svg';

function overlay() {
  canvas.add(
    new fabric.Rect({
      left: artboard.left,
      top: artboard.top,
      originX: 'left',
      originY: 'top',
      width: artboard.width,
      height: artboard.height,
      fill: 'rgba(0,0,0,0.5)',
      selectable: false,
      // Not "overlay": that is the artboard's id
      id: 'crop-overlay',
    })
  );
}

// Start cropping an image
function cropImage(object) {
  if (!cropping) {
    $('#properties-overlay').addClass('properties-disabled');
    cropping = true;
    cropobj = object;
    canvas.uniformScaling = false;
    cropobj.setCoords();
    // The visible region, in the source bitmap's own pixels, before the image
    // is expanded back to its full size below
    var cropx = cropobj.get('cropX');
    var cropy = cropobj.get('cropY');
    var shownWidth = cropobj.get('width');
    var shownHeight = cropobj.get('height');
    var shownLeft = cropobj.get('left');
    var shownTop = cropobj.get('top');
    overlay();
    var cropUI = new fabric.Rect({
      left: object.get('left'),
      top: object.get('top'),
      width: object.get('width') * object.get('scaleX') - 5,
      height: object.get('height') * object.get('scaleY') - 5,
      // The crop window has to turn with the image, otherwise the region it
      // covers cannot be expressed as a crop at all
      angle: object.get('angle') || 0,
      originX: 'center',
      originY: 'center',
      id: 'crop',
      fill: 'rgba(0,0,0,0)',
      shadow: {
        color: 'black',
        offsetX: 0,
        offsetY: 0,
        blur: 0,
        opacity: 0,
      },
    });
    cropobj.clone(function (cloned) {
      cloned.set({
        id: 'cropped',
        selectable: false,
        originX: 'center',
        originY: 'center',
      });
      canvas.add(cloned);
      canvas.bringToFront(cloned);
      canvas.bringToFront(cropUI);
      canvas.renderAll();
      cropobj = object;
    });
    cropobj
      .set({
        cropX: 0,
        cropY: 0,
        width: cropobj.get('ogWidth'),
        height: cropobj.get('ogHeight'),
      })
      .setCoords();
    canvas.renderAll();
    // Growing back to the full bitmap moves the centre, so shift the image so
    // the region that was on screen stays exactly where it was. The offset is
    // in the image's own frame, hence the rotation.
    var shift = rotateVector(
      (cropx + shownWidth / 2 - cropobj.get('width') / 2) *
        cropobj.get('scaleX'),
      (cropy + shownHeight / 2 - cropobj.get('height') / 2) *
        cropobj.get('scaleY'),
      fabric.util.degreesToRadians(cropobj.get('angle') || 0)
    );
    cropobj.set({
      left: shownLeft - shift.x,
      top: shownTop - shift.y,
    });
    cropUI.setControlsVisibility({
      mt: false,
      mb: false,
      mr: false,
      ml: false,
      mtr: false,
    });
    cropUI.controls.tl = new fabric.Control({
      x: -0.5,
      y: -0.5,
      offsetX: 3,
      offsetY: 3,
      cursorStyleHandler:
        fabric.controlsUtils.scaleCursorStyleHandler,
      actionHandler: fabric.controlsUtils.scalingEqually,
      render: function (ctx, left, top, styleOverride, fabricObject) {
        const wsize = 27;
        const hsize = 27;
        ctx.save();
        ctx.translate(left, top);
        ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
        ctx.drawImage(tlcrop, -wsize / 2, -hsize / 2, wsize, hsize);
        ctx.restore();
      },
    });
    cropUI.controls.tr = new fabric.Control({
      x: 0.5,
      y: -0.5,
      offsetX: -3,
      offsetY: 3,
      cursorStyleHandler:
        fabric.controlsUtils.scaleCursorStyleHandler,
      actionHandler: fabric.controlsUtils.scalingEqually,
      render: function (ctx, left, top, styleOverride, fabricObject) {
        const wsize = 27;
        const hsize = 27;
        ctx.save();
        ctx.translate(left, top);
        ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
        ctx.drawImage(trcrop, -wsize / 2, -hsize / 2, wsize, hsize);
        ctx.restore();
      },
    });
    cropUI.controls.bl = new fabric.Control({
      x: -0.5,
      y: 0.5,
      offsetX: 3,
      offsetY: -3,
      cursorStyleHandler:
        fabric.controlsUtils.scaleCursorStyleHandler,
      actionHandler: fabric.controlsUtils.scalingEqually,
      render: function (ctx, left, top, styleOverride, fabricObject) {
        const wsize = 27;
        const hsize = 27;
        ctx.save();
        ctx.translate(left, top);
        ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
        ctx.drawImage(blcrop, -wsize / 2, -hsize / 2, wsize, hsize);
        ctx.restore();
      },
    });
    cropUI.controls.br = new fabric.Control({
      x: 0.5,
      y: 0.5,
      offsetX: -3,
      offsetY: -3,
      cursorStyleHandler:
        fabric.controlsUtils.scaleCursorStyleHandler,
      actionHandler: fabric.controlsUtils.scalingEqually,
      render: function (ctx, left, top, styleOverride, fabricObject) {
        const wsize = 27;
        const hsize = 27;
        ctx.save();
        ctx.translate(left, top);
        ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
        ctx.drawImage(brcrop, -wsize / 2, -hsize / 2, wsize, hsize);
        ctx.restore();
      },
    });
    canvas.add(cropUI);
    canvas.setActiveObject(cropUI);
    canvas.renderAll();
    cropleft = cropUI.get('left');
    croptop = cropUI.get('top');
    cropscalex = cropUI.get('scaleX') - 0.03;
    cropscaley = cropUI.get('scaleY') - 0.03;
  }
}
$(document).on('click', '#crop-image', function () {
  if (canvas.getActiveObject()) {
    cropImage(canvas.getActiveObject());
  }
});

// Add an image to the canvas
function newImage(file, x, y, width, center) {
  var newimg = new fabric.Image(file, {
    left: x,
    top: y,
    originX: 'center',
    originY: 'center',
    stroke: '#000',
    strokeUniform: true,
    strokeWidth: 0,
    paintFirst: 'stroke',
    absolutePositioned: true,
    id: 'Image' + layer_count,
    inGroup: false,
    strokeDashArray: false,
    objectCaching: true,
    shadow: {
      color: 'black',
      offsetX: 0,
      offsetY: 0,
      blur: 0,
      opacity: 0,
    },
  });
  files.push({ name: newimg.get('id'), file: file.src });
  canvas.add(newimg);
  newimg.scaleToWidth(width);
  newimg.set({
    scaleX: parseFloat(newimg.get('scaleX').toFixed(2)),
    scaleY: parseFloat(newimg.get('scaleY').toFixed(2)),
    ogWidth: newimg.get('width'),
    ogHeight: newimg.get('height'),
  });
  canvas.bringToFront(newimg);
  canvas.renderAll();
  newLayer(newimg);
  canvas.setActiveObject(newimg);
  if (center) {
    newimg.set(
      'left',
      artboard.get('left') + artboard.get('width') / 2
    );
    newimg.set(
      'top',
      artboard.get('top') + artboard.get('height') / 2
    );
    canvas.renderAll();
  }
  $('#load-image').removeClass('loading-active');
}

function loadImage(src, x, y, width, center) {
  var image = new Image();
  image.onload = function (img) {
    newImage(image, x, y, width, center);
  };
  image.onerror = function () {
    console.error('Could not load image', src);
    $('#load-image').removeClass('loading-active');
  };
  image.src = src;
}

function createVideoThumbnail(file, max, seekTo = 0.0, isURL) {
  return new Promise((resolve, reject) => {
    const videoPlayer = document.createElement('video');
    if (isURL) {
      videoPlayer.setAttribute('src', file);
    } else {
      videoPlayer.setAttribute('src', URL.createObjectURL(file));
    }
    videoPlayer.setAttribute('crossorigin', 'anonymous');
    videoPlayer.load();
    videoPlayer.addEventListener('error', (ex) => {
      reject('error when loading video file', ex);
    });
    videoPlayer.addEventListener('loadedmetadata', () => {
      if (videoPlayer.duration < seekTo) {
        reject('video is too short.');
        return;
      }
      setTimeout(() => {
        videoPlayer.currentTime = seekTo;
      }, 200);
      videoPlayer.addEventListener('seeked', () => {
        var oc = document.createElement('canvas');
        var octx = oc.getContext('2d');
        if (videoPlayer.videoWidth > videoPlayer.videoHeight) {
          oc.width = max;
          oc.height =
            (videoPlayer.videoHeight / videoPlayer.videoWidth) * max;
        } else {
          oc.height = max;
          oc.width =
            (videoPlayer.videoWidth / videoPlayer.videoHeight) * max;
        }
        octx.drawImage(videoPlayer, 0, 0, oc.width, oc.height);
        resolve(oc.toDataURL());
      });
    });
  });
}

function createThumbnail(file, max) {
  return new Promise(function (resolve, reject) {
    var reader = new FileReader();
    reader.onload = function (event) {
      var img = new Image();
      img.onload = function () {
        if (img.width > max) {
          var oc = document.createElement('canvas');
          var octx = oc.getContext('2d');
          if (img.width > img.height) {
            oc.width = max;
            oc.height = (img.height / img.width) * max;
          } else {
            oc.height = max;
            oc.width = (img.width / img.height) * max;
          }
          octx.drawImage(img, 0, 0, oc.width, oc.height);
          resolve(oc.toDataURL());
        } else {
          resolve(img.src);
        }
      };
      img.src = event.target.result;
    };
    reader.readAsDataURL(file);
  });
}

function dataURItoBlob(dataURI) {
  var byteString = atob(dataURI.split(',')[1]);
  var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
  var ab = new ArrayBuffer(byteString.length);
  var ia = new Uint8Array(ab);
  for (var i = 0; i < byteString.length; i++) {
    ia[i] = byteString.charCodeAt(i);
  }
  var blob = new Blob([ab], { type: mimeString });
  return blob;
}

async function uploadFromURL() {
  var url = $('#upload-link-input').val();
  let file = await fetch(url).then((r) => r.blob());
  if (file.type.split('/')[0] === 'image') {
    $('#upload-link-input').val('');
    $('.upload-show').removeClass('upload-show');
    createThumbnail(file, 250).then(function (data) {
      saveFile(
        dataURItoBlob(data),
        file,
        file.type.split('/')[0],
        'temp',
        false,
        false
      );
    });
  } else if (file.type.split('/')[0] === 'video') {
    $('.upload-show').removeClass('upload-show');
    createVideoThumbnail(file, 250, 0, false).then(function (data) {
      saveFile(
        dataURItoBlob(data),
        file,
        file.type.split('/')[0],
        'temp',
        false,
        false
      );
    });
    $('#upload-link-input').val('');
  } else {
    alert('File type not accepted');
  }
}
$(document).on('click', '#upload-link-add', uploadFromURL);

function handleUpload(custom = false) {
  var files2;
  if (custom == false) {
    files2 = $('#filepick').get(0).files;
  } else {
    files2 = custom.originalEvent.dataTransfer.files;
  }
  if (files2) {
    Array.from(files2).forEach((file) => {
      uploading = true;
      if (file.size / 1024 / 1024 <= 10) {
        $('#upload-button').html(
          "<img src='assets/upload.svg'> Uploading..."
        );
        $('#upload-button').addClass('uploading');
        if (file['type'].split('/')[0] === 'image') {
          $('.upload-show').removeClass('upload-show');
          createThumbnail(file, 250).then(function (data) {
            saveFile(
              dataURItoBlob(data),
              file,
              file['type'].split('/')[0],
              'temp',
              false,
              false
            );
          });
        } else if (file['type'].split('/')[0] === 'video') {
          $('.upload-show').removeClass('upload-show');
          createVideoThumbnail(file, 250, 0, false).then(function (
            data
          ) {
            saveFile(
              dataURItoBlob(data),
              file,
              file['type'].split('/')[0],
              'temp',
              false,
              false
            );
          });
        } else {
          alert('File type not accepted');
        }
      } else {
        alert('File is too big');
      }
    });
    if (files2.length == 1) {
      if (files2[0]['type'].split('/')[0] === 'image') {
        $('.upload-tab-active').removeClass('upload-tab-active');
        $('#images-tab').addClass('upload-tab-active');
      } else if (files2[0]['type'].split('/')[0] === 'video') {
        $('.upload-tab-active').removeClass('upload-tab-active');
        $('#videos-tab').addClass('upload-tab-active');
      } else if (files2[0]['type'].split('/')[0] === 'audio') {
        $('.upload-tab-active').removeClass('upload-tab-active');
        $('#audio-tab').addClass('upload-tab-active');
      }
    }
  }
}
$(document).on('change', '#filepick', function () {
  handleUpload(false);
});

// Upload audio
function audioUpload() {
  const files = $('#filepick2').get(0).files;
  if (files) {
    if (files.length == 1) {
      if (files[0]['type'].split('/')[0] === 'audio') {
        if (files[0].size / 1024 / 1024 <= 10) {
          $('#audio-upload-button').html('Uploading...');
          $('#audio-upload-button').addClass('uploading');
          saveAudio(files[0]);
        } else {
          alert('File is too big');
        }
      } else {
        alert('Wrong file type');
      }
    }
  }
}
$(document).on('change', '#filepick2', audioUpload);

// Create a rectangle
function newRectangle(color) {
  var newrect = new fabric.Rect({
    left: 0,
    top: 0,
    originX: 'center',
    originY: 'center',
    width: 200,
    height: 200,
    stroke: '#000',
    strokeWidth: 0,
    strokeUniform: true,
    backgroundColor: 'rgba(255,255,255,0)',
    rx: 0,
    ry: 0,
    cornerRadius: 0,
    fill: color,
    cursorWidth: 1,
    cursorDuration: 1,
    paintFirst: 'stroke',
    cursorDelay: 250,
    strokeDashArray: false,
    inGroup: false,
    id: 'Shape' + layer_count,
    shadow: {
      color: '#000',
      offsetX: 0,
      offsetY: 0,
      blur: 0,
      opacity: 0,
    },
  });
  canvas.add(newrect);
  newLayer(newrect);
  canvas.setActiveObject(newrect);
  canvas.bringToFront(newrect);
  canvas.renderAll();
}

// Change text format
function formatText() {
  var isselected = false;
  if (!canvas.getActiveObject().isEditing) {
    canvas.getActiveObject().enterEditing();
    canvas.getActiveObject().selectAll();
    isselected = true;
  }
  if ($(this).hasClass('format-text-active')) {
    if ($(this).attr('id') == 'format-bold') {
      $(this).find('img').attr('src', 'assets/bold.svg');
      canvas
        .getActiveObject()
        .setSelectionStyles({ fontWeight: 'normal' });
    } else if ($(this).attr('id') == 'format-italic') {
      $(this).find('img').attr('src', 'assets/italic.svg');
      canvas
        .getActiveObject()
        .setSelectionStyles({ fontStyle: 'normal' });
    } else if ($(this).attr('id') == 'format-underline') {
      $(this).find('img').attr('src', 'assets/underline.svg');
      canvas
        .getActiveObject()
        .setSelectionStyles({ underline: false });
    } else {
      $(this).find('img').attr('src', 'assets/strike.svg');
      canvas
        .getActiveObject()
        .setSelectionStyles({ linethrough: false });
    }
    $(this).removeClass('format-text-active');
  } else {
    $(this).addClass('format-text-active');
    if ($(this).attr('id') == 'format-bold') {
      $(this).find('img').attr('src', 'assets/bold-active.svg');
      canvas
        .getActiveObject()
        .setSelectionStyles({ fontWeight: 'bold' });
    } else if ($(this).attr('id') == 'format-italic') {
      $(this).find('img').attr('src', 'assets/italic-active.svg');
      canvas
        .getActiveObject()
        .setSelectionStyles({ fontStyle: 'italic' });
    } else if ($(this).attr('id') == 'format-underline') {
      $(this).find('img').attr('src', 'assets/underline-active.svg');
      canvas
        .getActiveObject()
        .setSelectionStyles({ underline: true });
    } else {
      $(this).find('img').attr('src', 'assets/strike-active.svg');
      canvas
        .getActiveObject()
        .setSelectionStyles({ linethrough: true });
    }
  }
  if (isselected) {
    canvas.getActiveObject().exitEditing();
  }
  canvas.renderAll();
  save();
}
$(document).on('click', '.format-text', formatText);

// Change stroke type (e.g. dashed), ignore naming (used to be for line join)
function lineJoin() {
  if ($('.line-join-active').attr('id') == 'miter') {
    $('.line-join-active')
      .find('img')
      .attr('src', 'assets/miter.svg');
  } else if ($('.line-join-active').attr('id') == 'bevel') {
    $('.line-join-active')
      .find('img')
      .attr('src', 'assets/bevel.svg');
  } else if ($('.line-join-active').attr('id') == 'round') {
    $('.line-join-active')
      .find('img')
      .attr('src', 'assets/round.svg');
  } else if ($('.line-join-active').attr('id') == 'small-dash') {
    $('.line-join-active')
      .find('img')
      .attr('src', 'assets/dash2.svg');
  }
  $('.line-join-active').removeClass('line-join-active');
  $(this).addClass('line-join-active');
  if ($(this).attr('id') == 'miter') {
    $(this).find('img').attr('src', 'assets/miter-active.svg');
    canvas
      .getActiveObject()
      .set({ strokeWidth: 0, strokeDashArray: false });
    canvas.renderAll();
    updatePanelValues();
  } else if ($(this).attr('id') == 'bevel') {
    $(this).find('img').attr('src', 'assets/bevel-active.svg');
    canvas.getActiveObject().set({ strokeDashArray: false });
    if (canvas.getActiveObject().get('strokeWidth') == 0) {
      canvas.getActiveObject().set({ strokeWidth: 1 });
      canvas.renderAll();
      updatePanelValues();
    }
  } else if ($(this).attr('id') == 'round') {
    $(this).find('img').attr('src', 'assets/round-active.svg');
    canvas.getActiveObject().set({ strokeDashArray: [10, 5] });
    if (canvas.getActiveObject().get('strokeWidth') == 0) {
      canvas.getActiveObject().set({ strokeWidth: 1 });
      canvas.renderAll();
      updatePanelValues();
    }
  } else {
    $(this).find('img').attr('src', 'assets/dash2-active.svg');
    canvas.getActiveObject().set({ strokeDashArray: [3, 3] });
    if (canvas.getActiveObject().get('strokeWidth') == 0) {
      canvas.getActiveObject().set({ strokeWidth: 1 });
      canvas.renderAll();
      updatePanelValues();
    }
  }
  canvas.renderAll();
  save();
}
$(document).on('click', '.line-join', lineJoin);

// Change text alignment
function alignText() {
  var textalign;
  if ($('.align-text-active').attr('id') == 'align-text-left') {
    $('.align-text-active')
      .find('img')
      .attr('src', 'assets/align-text-left.svg');
  } else if (
    $('.align-text-active').attr('id') == 'align-text-center'
  ) {
    $('.align-text-active')
      .find('img')
      .attr('src', 'assets/align-text-center.svg');
  } else if (
    $('.align-text-active').attr('id') == 'align-text-right'
  ) {
    $('.align-text-active')
      .find('img')
      .attr('src', 'assets/align-text-right.svg');
  } else {
    $('.align-text-active')
      .find('img')
      .attr('src', 'assets/align-text-justify.svg');
  }
  $('.align-text-active').removeClass('align-text-active');
  $(this).addClass('align-text-active');
  if ($(this).attr('id') == 'align-text-left') {
    textalign = 'left';
    $(this)
      .find('img')
      .attr('src', 'assets/align-text-left-active.svg');
  } else if ($(this).attr('id') == 'align-text-center') {
    textalign = 'center';
    $(this)
      .find('img')
      .attr('src', 'assets/align-text-center-active.svg');
  } else if ($(this).attr('id') == 'align-text-right') {
    textalign = 'right';
    $(this)
      .find('img')
      .attr('src', 'assets/align-text-right-active.svg');
  } else {
    textalign = 'justify';
    $(this)
      .find('img')
      .attr('src', 'assets/align-text-justify-active.svg');
  }
  canvas.getActiveObject().set({ textAlign: textalign });
  canvas.renderAll();
  save();
}
$(document).on('click', '.align-text', alignText);

// Change font
function changeFont() {
  var font = $('#font-picker').val();
  const active = canvas.getActiveObject();
  if (!active || !font) {
    return;
  }
  if (active.get('assetType')) {
    WebFont.load({
      google: {
        families: [font],
      },
      active: () => {
        const text = animatedtext.find((x) => x.id == active.id);
        if (!text) {
          return;
        }
        text.reset(
          text.text,
          $.extend(text.props, { fontFamily: font }),
          canvas
        );
        save();
      },
    });
  } else {
    WebFont.load({
      google: {
        families: [font],
      },
      active: () => {
        active.set('fontFamily', font);
        canvas.renderAll();
        save();
      },
    });
  }
}
$(document).on('change', '#font-picker', changeFont);

// Calculate text width to display it in 1 line
function calculateTextWidth(text, font) {
  var ctx = canvas.getContext('2d');
  ctx.font = font;
  return ctx.measureText(text).width + 10;
}

// Create an audio layer
function newAudioLayer(src) {
  var audio = new Audio(src);
  audio.crossOrigin = 'anonymous';
  audio.addEventListener('loadeddata', () => {
    var nullobject = new fabric.Rect({
      id: 'Audio' + layer_count,
      width: 10,
      height: 10,
      audioSrc: src,
      duration: audio.duration * 1000,
      opacity: 0,
      selectable: false,
      // Audio layers are invisible placeholders. Selecting them (from the
      // layer list) must not paint controls / a bounding box on the canvas.
      hasControls: false,
      hasBorders: false,
      evented: false,
      volume: 0.5,
      assetType: 'audio',
      shadow: {
        color: '#000',
        offsetX: 0,
        offsetY: 0,
        blur: 0,
        opacity: 0,
      },
    });
    canvas.add(nullobject);
    newLayer(nullobject);
  });
}

// Create a textbox
function newTextbox(
  fontsize,
  fontweight,
  text,
  x,
  y,
  width,
  center,
  font
) {
  var newtext = new fabric.Textbox(text, {
    left: x,
    top: y,
    originX: 'center',
    originY: 'center',
    fontFamily: 'Inter',
    fill: '#000',
    fontSize: fontsize,
    fontWeight: fontweight,
    textAlign: 'center',
    cursorWidth: 1,
    stroke: '#000',
    strokeWidth: 0,
    cursorDuration: 1,
    paintFirst: 'stroke',
    objectCaching: false,
    absolutePositioned: true,
    strokeUniform: true,
    inGroup: false,
    cursorDelay: 250,
    strokeDashArray: false,
    width: calculateTextWidth(
      text,
      fontweight + ' ' + fontsize + 'px ' + (font || 'Inter')
    ),
    id: 'Text' + layer_count,
    shadow: {
      color: '#000',
      offsetX: 0,
      offsetY: 0,
      blur: 0,
      opacity: 0,
    },
  });
  newtext.setControlsVisibility({
    mt: false,
    mb: false,
  });
  canvas.add(newtext);
  newLayer(newtext);
  canvas.setActiveObject(newtext);
  canvas.bringToFront(newtext);
  newtext.enterEditing();
  newtext.selectAll();
  canvas.renderAll();
  if (center) {
    newtext.set(
      'left',
      artboard.get('left') + artboard.get('width') / 2
    );
    newtext.set(
      'top',
      artboard.get('top') + artboard.get('height') / 2
    );
    canvas.renderAll();
  }
  canvas.getActiveObject().set('fontFamily', font);
  canvas.renderAll();
}

function deleteObject(object, def = true) {
  if (object.get('assetType') == 'animatedText' && def) {
    animatedtext = $.grep(animatedtext, function (a) {
      return a.id != object.id;
    });
  }
  if (object.type == 'image') {
    files = $.grep(files, function (a) {
      return a.name != object.get('id');
    });
  }
  // Release any media element the object was holding on to
  if (typeof object.getElement === 'function') {
    const el = object.getElement();
    if (el && el.tagName == 'VIDEO') {
      el.pause();
      el.removeAttribute('src');
      el.load();
    }
  }
  if (object.get('src') && typeof object.get('src').pause === 'function') {
    object.get('src').pause();
  }
  $(".layer[data-object='" + object.get('id') + "']").remove();
  $('#' + object.get('id')).remove();
  keyframes = $.grep(keyframes, function (e) {
    return e.id != object.get('id');
  });
  p_keyframes = $.grep(p_keyframes, function (e) {
    return e.id != object.get('id');
  });
  objects = $.grep(objects, function (e) {
    return e.id != object.get('id');
  });
  canvas.remove(object);
  canvas.renderAll();
  canvas.discardActiveObject();
  save();
  if (objects.length == 0) {
    $('#nolayers').removeClass('yaylayers');
  }
}

// Delete selected object
function deleteSelection() {
  if (
    canvas.getActiveObject() &&
    !canvas.getActiveObject().isEditing
  ) {
    const selection = canvas.getActiveObject();
    if (selection.type == 'activeSelection') {
      canvas.discardActiveObject();
      selection._objects.forEach(function (object) {
        deleteObject(object);
      });
    } else {
      deleteObject(canvas.getActiveObject());
    }
  }
}

// Expand / collapse layer
function toggleLayer() {
  const layerid = $(this).parent().parent().attr('data-object');
  $(this).parent().parent().find('.properties').toggle();
  $(this).parent().parent().find('.droparrow').toggleClass('layeron');
  $(".keyframe-row[data-object='" + layerid + "']").toggle();
  setTimelineZoom(timelinetime);
}
$(document).on('click', '.droparrow', toggleLayer);

// Select layer
function selectLayer(e) {
  if (!$(e.target).hasClass('droparrow')) {
    const layerid = $(this).parent().attr('data-object');
    $('.layer-selected').removeClass('layer-selected');
    $(this).parent().addClass('layer-selected');
    canvas.setActiveObject(canvas.getItemById(layerid));
  }
}
$(document).on('click', '.layer-name', selectLayer);

// Set video duration
function setDuration(length) {
  $('#inner-timeline').css('width', length / timelinetime + 50);
  $('#inner-seekarea').css('width', length / timelinetime + 50);
  duration = length;
  var minutes = Math.floor(duration / 1000 / 60);
  var seconds = (duration / 1000 - minutes * 60).toFixed(2);
  $('#total-time input').val(
    ('0' + minutes).slice(-2) +
      ':' +
      ('0' + Math.floor(seconds)).slice(-2) +
      ':' +
      ('0' + Math.floor((seconds % 1) * 100)).slice(-2)
  );
  $('.object-props').each(function () {
    const p_keyframe = p_keyframes.find(
      (x) => x.id == $(this).attr('id')
    );
    if (!p_keyframe) {
      return;
    }
    $(this).css(
      'width',
      duration / timelinetime - p_keyframe.start / timelinetime + 'px'
    );
    p_keyframe.end = duration;
    if (p_keyframe.trimend > p_keyframe.end) {
      p_keyframe.trimend = duration;
      $(this)
        .find('.trim-row')
        .css(
          'width',
          duration / timelinetime -
            p_keyframe.trimstart / timelinetime +
            'px'
        );
    }
  });
  setTimelineZoom(timelinetime);
  save();
}

// Render time markers
function renderTimeMarkers() {
  var renderoffset = 1000 / timelinetime - 20;
  var timenumber = 0;
  var modulo = 1;
  if (timelinetime > 18) {
    modulo = 5;
  } else if (timelinetime > 12) {
    modulo = 2;
  }
  $('#time-numbers').html('');
  $('#time-numbers').append(
    "<div class='time-number' style='margin-left:" +
      offset_left +
      "px'>" +
      timenumber +
      's<span></span></div>'
  );
  timenumber++;
  while (timenumber * 1000 <= duration) {
    $('#time-numbers').append(
      "<div class='time-number' style='margin-left:" +
        renderoffset +
        "px'>" +
        timenumber +
        's<span></span></div>'
    );
    if (timenumber % modulo != 0) {
      $('#time-numbers .time-number:last-child').css('opacity', '0');
    }
    timenumber++;
  }
}

// Change timeline zoom level
function setTimelineZoom(time) {
  $('.object-props').each(function () {
    const p_keyframe = p_keyframes.find(
      (x) => x.id == $(this).attr('id')
    );
    if (!p_keyframe) {
      return;
    }
    $(this).offset({
      left:
        p_keyframe.start / time +
        $('#inner-timeline').offset().left +
        offset_left,
    });
    $(this).css({ width: ($(this).width() * timelinetime) / time });
    $(this)
      .find('.trim-row')
      .css({ left: p_keyframe.trimstart / time });
    $(this)
      .find('.trim-row')
      .css({
        width:
          ($(this).find('.trim-row').width() * timelinetime) / time,
      });
  });
  timelinetime = time;
  $('.keyframe').each(function () {
    $(this).offset({
      left:
        $(this).attr('data-time') / timelinetime +
        $('#inner-timeline').offset().left +
        offset_left,
    });
  });
  $('#seekbar').offset({
    left:
      $('#inner-timeline').offset().left +
      currenttime / timelinetime +
      offset_left,
  });
  $('#inner-timeline').css({ width: duration / timelinetime + 50 });
  $('#inner-seekarea').css({
    minWidth: duration / timelinetime + 50,
  });
  renderTimeMarkers();
}
$(document).on('input', '#timeline-zoom', function () {
  setTimelineZoom($('#timeline-zoom').val());
});

function removeKeyframe() {
  if (!selectedkeyframe) {
    return;
  }
  const time = selectedkeyframe.attr('data-time');
  const id = selectedkeyframe.attr('data-object');
  const prop = selectedkeyframe.attr('data-property');
  const names = [prop].concat(KEYFRAME_COUNTERPARTS[prop] || []);
  keyframes = $.grep(keyframes, function (e) {
    return (
      e.t != time || e.id != id || names.indexOf(e.name) == -1
    );
  });
  selectedkeyframe.remove();
  $('#keyframe-properties').removeClass('show-properties');
}

// Delete a keyframe
function deleteKeyframe() {
  if (shiftkeys.length > 0) {
    shiftkeys.forEach(function (key) {
      selectedkeyframe = $(key.keyframe);
      removeKeyframe();
    });
    shiftkeys = [];
  } else {
    removeKeyframe();
  }
  animate(false, currenttime);
  save();
}
$(document).on('click', '#delete-keyframe', deleteKeyframe);

// Properties that are always keyframed together with a leading property
const KEYFRAME_COUNTERPARTS = {
  left: ['top'],
  scaleX: ['scaleY', 'width', 'height'],
  strokeWidth: ['stroke'],
  'shadow.color': [
    'shadow.opacity',
    'shadow.offsetX',
    'shadow.offsetY',
    'shadow.blur',
  ],
  charSpacing: ['lineHeight'],
};

// Copy keyframes
function copyKeyframes() {
  if (!Array.isArray(clipboard) || clipboard.length == 0) {
    return;
  }
  clipboard.sort(function (a, b) {
    return a.t - b.t;
  });
  var inittime = clipboard[0].t;
  clipboard.forEach(function (keyframe) {
    const object = canvas.getItemById(keyframe.id);
    if (!object) {
      return;
    }
    var newtime = keyframe.t - inittime + currenttime;
    newKeyframe(
      keyframe.name,
      object,
      newtime,
      keyframe.value,
      true
    );
    // Counterpart properties are stored as separate keyframes at the same
    // time; copy whichever of them actually exist.
    const counterparts = KEYFRAME_COUNTERPARTS[keyframe.name] || [];
    counterparts.forEach(function (name) {
      const keyarr2 = $.grep(keyframes, function (e) {
        return (
          e.t == keyframe.t &&
          e.id == keyframe.id &&
          e.name == name
        );
      });
      if (keyarr2.length > 0) {
        newKeyframe(name, object, newtime, keyarr2[0].value, true);
      }
    });
  });
  save();
}

// Update keyframe (after dragging, or when re-stamping it at the playhead)
function updateKeyframe(drag, newval) {
  const keyprop = drag.attr('data-property');
  const keytime = drag.attr('data-time');
  const objectid = drag.attr('data-object');
  const keyarr = $.grep(keyframes, function (e) {
    return (
      e.t == parseFloat(keytime) &&
      e.id == objectid &&
      e.name == keyprop
    );
  });
  if (keyarr.length == 0) {
    return;
  }
  const keyobj = canvas.getItemById(keyarr[0].id);
  const p_keyframe = p_keyframes.find((x) => x.id == objectid);
  if (!keyobj || !p_keyframe) {
    return;
  }
  // data-time is always absolute; drag.position() is relative to the layer
  // row, which is itself offset by the layer start.
  const time = newval
    ? currenttime
    : parseFloat(p_keyframe.start) +
      parseFloat((drag.position().left * timelinetime).toFixed(1));

  const names = [keyprop].concat(
    KEYFRAME_COUNTERPARTS[keyprop] || []
  );
  names.forEach(function (name) {
    const arr = $.grep(keyframes, function (e) {
      return (
        e.t == parseFloat(keytime) &&
        e.id == objectid &&
        e.name == name
      );
    });
    if (arr.length == 0) {
      return;
    }
    let value = arr[0].value;
    if (newval) {
      if (name == 'volume') {
        value = parseFloat($('#object-volume input').val()) / 200;
      } else {
        value = getPropValue(keyobj, name);
      }
    } else if (name == 'left') {
      // newKeyframe stores left/top relative to the artboard
      value = value + artboard.get('left');
    } else if (name == 'top') {
      value = value + artboard.get('top');
    }
    keyframes = $.grep(keyframes, function (e) {
      return (
        e.t != parseFloat(keytime) ||
        e.id != objectid ||
        e.name != name
      );
    });
    newKeyframe(name, keyobj, time, value, false);
  });
  drag.attr('data-time', time);
  sortKeyframes();
}

function keyframeSnap(drag) {
  if (shiftkeys.length == 0) {
    if (
      drag.offset().left > $('#seekbar').offset().left - 5 &&
      drag.offset().left < $('#seekbar').offset().left + 5
    ) {
      drag.offset({ left: $('#seekbar').offset().left });
      $('#line-snap').offset({
        left: $('#seekbar').offset().left,
        top: drag.parent().parent().offset().top,
      });
      $('#line-snap').css({
        height: drag.parent().parent().height(),
      });
      $('#line-snap').addClass('line-active');
    } else {
      let snapped = false;
      drag
        .parent()
        .parent()
        .find('.keyframe')
        .each(function () {
          if (!drag.is($(this))) {
            if (
              drag.offset().left > $(this).offset().left - 5 &&
              drag.offset().left < $(this).offset().left + 5
            ) {
              drag.offset({ left: $(this).offset().left });
              $('#line-snap').offset({
                left: $(this).offset().left,
                top: drag.parent().parent().offset().top,
              });
              $('#line-snap').css({
                height: drag.parent().parent().height(),
              });
              $('#line-snap').addClass('line-active');
              snapped = true;
              return false;
            }
          }
        });
      if (!snapped) {
        $('#line-snap').removeClass('line-active');
      }
    }
  }
}

// Shared plumbing for the timeline drags (seekbar, keyframes, layer bars,
// timeline resize handle). Captures the pointer so the matching pointerup
// always comes back to us, and tears the drag down on pointercancel, on a
// stray native dragstart, or when the window loses focus. Without this a
// swallowed mouseup leaves the dragged element glued to the cursor.
function bindPointerDrag(e, el, onMove, onEnd) {
  var pointerId =
    e.originalEvent && e.originalEvent.pointerId != undefined
      ? e.originalEvent.pointerId
      : null;
  if (pointerId != null && el && el.setPointerCapture) {
    try {
      el.setPointerCapture(pointerId);
    } catch (err) {}
  }
  function end(ev) {
    // Ignore another pointer going up while this one is still dragging
    if (
      ev &&
      ev.originalEvent &&
      pointerId != null &&
      ev.originalEvent.pointerId != undefined &&
      ev.originalEvent.pointerId != pointerId
    ) {
      return;
    }
    $(document)
      .off('pointermove', onMove)
      .off('pointerup', end)
      .off('pointercancel', end)
      .off('dragstart', end);
    window.removeEventListener('blur', end);
    if (pointerId != null && el && el.releasePointerCapture) {
      try {
        el.releasePointerCapture(pointerId);
      } catch (err) {}
    }
    onEnd(ev);
  }
  $(document)
    .on('pointermove', onMove)
    .on('pointerup', end)
    .on('pointercancel', end)
    .on('dragstart', end);
  // Native listener on purpose: jQuery routes blur through its focus special
  // event, which defers the handler and can drop it entirely
  window.addEventListener('blur', end);
}

// A modal covers the timeline but does not cover pointer events, so every
// timeline drag has to opt out while one is open - otherwise the seekbar,
// keyframes, layer bars and the resize handle all still react to a drag
// happening "behind" the dialog.
function modalOpen() {
  return $('.modal-open').length > 0;
}

// Dragging a keyframe
function dragKeyframe(e) {
  if (e.which == 3 || modalOpen()) {
    return false;
  }
  e.stopPropagation();
  e.preventDefault();
  var inst = this;
  var drag = $(this);
  var pageX = e.pageX;
  var offset = $(this).offset();
  var move = false;
  if (e.shiftKey) {
    if (!$(this).hasClass('keyframe-selected')) {
      shiftkeys.push({
        keyframe: this,
        offset: $(this).offset().left,
      });
      $(this).addClass('keyframe-selected');
    } else {
      const el = this;
      shiftkeys = $.grep(shiftkeys, function (e) {
        return e.keyframe !== el;
      });
      $(this).removeClass('keyframe-selected');
    }
  }
  if (shiftkeys.length > 0) {
    shiftkeys.forEach(function (key) {
      key.offset = $(key.keyframe).offset().left;
    });
  }
  function draggingKeyframe(e) {
    move = true;
    var left = offset.left + (e.pageX - pageX);
    if (shiftkeys.length == 0) {
      if (left > $('#timearea').offset().left + offset_left) {
        drag.offset({ left: left });
      } else {
        drag.offset({
          left: $('#timearea').offset().left + offset_left,
        });
      }
      keyframeSnap(drag);
    } else {
      shiftkeys.forEach(function (key) {
        if (key.keyframe != inst) {
          $(key.keyframe).offset({
            left: key.offset + (e.pageX - pageX),
          });
          keyframeSnap($(key.keyframe));
        } else {
          drag.offset({ left: left });
          keyframeSnap(drag);
        }
      });
    }
  }
  function releasedKeyframe(e) {
    $('#line-snap').removeClass('line-active');
    if (move) {
      if (shiftkeys.length == 0) {
        // Check for 60FPS playback, 16ms "slots"
        var time = parseFloat(
          (drag.position().left * timelinetime).toFixed(1)
        );
        if (time % 16.666 != 0) {
          drag.offset({
            left:
              (Math.ceil(time / 16.666) * 16.666) / timelinetime +
              drag.parent().offset().left,
          });
          updateKeyframe(drag, false);
        } else {
          updateKeyframe(drag, false);
        }
      } else {
        shiftkeys.forEach(function (key) {
          // Check for 60FPS playback, 16ms "slots"
          var time = parseFloat(
            ($(key.keyframe).position().left * timelinetime).toFixed(
              1
            )
          );
          if (time % 16.666 != 0) {
            $(key.keyframe).offset({
              left:
                (Math.ceil(time / 16.666) * 16.666) / timelinetime +
                $(key.keyframe).parent().offset().left,
            });
            updateKeyframe($(key.keyframe), false);
          } else {
            updateKeyframe($(key.keyframe), false);
          }
        });
      }
    } else if (e && e.type == 'pointerup' && !e.shiftKey) {
      keyframeProperties(inst);
    }
    move = false;
    $('.line-active').removeClass('line-active');
    save();
  }
  bindPointerDrag(e, this, draggingKeyframe, releasedKeyframe);
}
$(document).on('pointerdown', '.keyframe', dragKeyframe);

// Render current time in the playback area
function renderTime() {
  var minutes = Math.floor(currenttime / 1000 / 60);
  var seconds = (currenttime / 1000 - minutes * 60).toFixed(2);
  $('#current-time input').val(
    ('0' + minutes).slice(-2) +
      ':' +
      ('0' + Math.floor(seconds)).slice(-2) +
      ':' +
      ('0' + Math.floor((seconds % 1) * 100)).slice(-2)
  );
}

// Update current time (and account for frame "slots")
function updateTime(drag, check) {
  if ($('#timeline').scrollLeft() > offset_left) {
    currenttime = parseFloat(
      (
        (drag.position().left +
          $('#timeline').scrollLeft() -
          offset_left) *
        timelinetime
      ).toFixed(1)
    );
  } else {
    currenttime = parseFloat(
      (
        (drag.position().left +
          $('#timeline').scrollLeft() -
          offset_left) *
        timelinetime
      ).toFixed(1)
    );
  }
  // Check for 60FPS playback, 16ms "slots"
  if (currenttime % 16.666 != 0 && !check) {
    currenttime = Math.ceil(currenttime / 16.666) * 16.666;
  }
  renderTime();
  pause();
  animate(false, currenttime);
}

// Dragging the seekbar
function dragSeekBar(e) {
  if (e.which == 3 || modalOpen()) {
    return false;
  }
  // Stop the browser from turning the press into a native drag-and-drop.
  // When it does, the mouseup is swallowed, released() never runs and the
  // seekbar stays glued to the pointer.
  e.preventDefault();
  var drag = $(this);
  var pageX = e.pageX;
  var offset = $(this).offset();
  tempselection = canvas.getActiveObject();
  canvas.discardActiveObject();
  function dragging(e) {
    paused = true;
    var left = offset.left + (e.pageX - pageX);
    if (
      left > $('#timearea').offset().left + offset_left &&
      left - $('#timearea').offset().left <
        duration / timelinetime + offset_left
    ) {
      drag.offset({ left: left });
    } else if (left < $('#timearea').offset().left + offset_left) {
      drag.offset({
        left: offset_left + $('#timearea').offset().left,
      });
    }
    if ($('#timeline').scrollLeft() > offset_left) {
      currenttime = parseFloat(
        (
          (drag.position().left +
            $('#timeline').scrollLeft() -
            offset_left) *
          timelinetime
        ).toFixed(1)
      );
    } else {
      currenttime = parseFloat(
        (
          (drag.position().left +
            $('#timeline').scrollLeft() -
            offset_left) *
          timelinetime
        ).toFixed(1)
      );
    }
    animate(false, currenttime);
    seeking = true;
    renderTime();
  }
  function released(e) {
    updateTime(drag, false);
    seeking = false;
    if (tempselection && tempselection.type != 'activeSelection') {
      reselect(tempselection);
    }
    updatePanelValues();
  }
  bindPointerDrag(e, this, dragging, released);
}
$(document).on('pointerdown', '#seekbar', dragSeekBar);
// The timeline drag targets must never become native drag sources
$(document).on(
  'dragstart',
  '#seekbar, .keyframe, .main-row, .row-el, .trim-row, #timeline-handle',
  function (e) {
    e.preventDefault();
  }
);

// Dragging layer horizontally
function dragObjectProps(e) {
  if (e.which == 3 || modalOpen()) {
    return false;
  }
  var drag = $(this).parent();
  var drag2 = $(this).find('.trim-row');
  var target = e.target;
  var pageX = e.pageX;
  var offset = drag.offset();
  var offset2 = drag2.offset();
  var initwidth = drag2.width();
  var initpos = drag2.position().left;
  var opened = false;
  var trim = 'no';
  // Trim layer to hovered area
  if (e.metaKey) {
    if (e.shiftKey) {
      if (drag2.position().left + e.pageX >= 0) {
        drag2.offset({
          left:
            hovertime / timelinetime -
            p_keyframes.find((x) => x.id == drag.attr('id'))
              .trimstart /
              timelinetime +
            offset2.left,
        });
        const leftval = parseFloat(
          (drag2.position().left * timelinetime).toFixed(1)
        );
        p_keyframes.find((x) => x.id == drag.attr('id')).trimstart =
          leftval;
        drag2.css({
          width:
            (p_keyframes.find((x) => x.id == drag.attr('id'))
              .trimend -
              p_keyframes.find((x) => x.id == drag.attr('id'))
                .trimstart) /
            timelinetime,
        });
        return false;
      }
    } else {
      if (
        hovertime +
          p_keyframes.find((x) => x.id == drag.attr('id')).start <
        duration
      ) {
        drag2.css({
          width:
            hovertime / timelinetime -
            p_keyframes.find((x) => x.id == drag.attr('id'))
              .trimstart /
              timelinetime,
        });
        save();
        p_keyframes.find((x) => x.id == drag.attr('id')).end =
          hovertime;
        p_keyframes.find((x) => x.id == drag.attr('id')).trimend =
          hovertime;
      }
      return false;
    }
  }
  if (pageX - $(this).find('.trim-row').offset().left < 7) {
    trim = 'left';
  } else if (
    pageX - $(this).find('.trim-row').offset().left >
    $(this).find('.trim-row').width() - 7
  ) {
    trim = 'right';
  }
  function dragging(e) {
    if (trim == 'no') {
      var left = offset.left + (e.pageX - pageX);
      if (
        left >
        $('#timearea').offset().left +
          offset_left -
          $('#timeline').scrollLeft()
      ) {
        drag.offset({ left: left });
      } else if (
        left + $('#timeline').scrollLeft() <
        $('#timearea').offset().left + offset_left
      ) {
        drag.css({ left: offset_left });
      }
      p_keyframes.find((x) => x.id == drag.attr('id')).start =
        parseFloat(
          (
            (drag.position().left -
              offset_left +
              $('#timeline').scrollLeft()) *
            timelinetime
          ).toFixed(1)
        );
      p_keyframes.find((x) => x.id == drag.attr('id')).end =
        parseFloat(
          (
            (drag.position().left +
              drag.width() -
              offset_left +
              $('#timeline').scrollLeft()) *
            timelinetime
          ).toFixed(1)
        );
      if (
        $(".keyframe-row[data-object='" + drag.attr('id') + "']").is(
          ':hidden'
        )
      ) {
        opened = true;
        $(".layer[data-object='" + drag.attr('id') + "']")
          .find('.properties')
          .toggle();
        $(".layer[data-object='" + drag.attr('id') + "']")
          .find('.properties')
          .toggleClass('layeron');
        $(
          ".keyframe-row[data-object='" + drag.attr('id') + "']"
        ).toggle();
        setTimelineZoom(timelinetime);
      }
      drag.find('.keyframe').each(function () {
        updateKeyframe($(this), false);
      });
      animate(false, currenttime);
    } else if (trim == 'left') {
      if (drag2.position().left + (e.pageX - pageX) >= 0) {
        drag2.offset({
          left: offset2.left + (e.pageX - pageX),
        });
        drag2.css({
          width: initwidth - (-initpos + drag2.position().left),
        });
        const leftval = parseFloat(
          (drag2.position().left * timelinetime).toFixed(1)
        );
        p_keyframes.find((x) => x.id == drag.attr('id')).trimstart =
          leftval;
      }
    } else if (trim == 'right') {
      if (initwidth + (e.pageX - pageX) < duration / timelinetime) {
        drag2.css({
          width: initwidth + (e.pageX - pageX),
        });
      } else {
        drag2.css({
          width:
            duration / timelinetime -
            drag.position().left -
            $('#timeline').scrollLeft() +
            offset_left,
        });
      }
      const rightval = parseFloat(
        (
          (drag2.position().left + drag2.width()) *
          timelinetime
        ).toFixed(1)
      );
      p_keyframes.find((x) => x.id == drag.attr('id')).end = rightval;
      p_keyframes.find((x) => x.id == drag.attr('id')).trimend =
        rightval;
    }
  }
  function released(e) {
    if (opened) {
      $(".layer[data-object='" + drag.attr('id') + "']")
        .find('.properties')
        .toggle();
      $(".layer[data-object='" + drag.attr('id') + "']")
        .find('.properties')
        .toggleClass('layeron');
      $(
        ".keyframe-row[data-object='" + drag.attr('id') + "']"
      ).toggle();
      setTimelineZoom(timelinetime);
    }
    animate(false, currenttime);
    save();
  }
  bindPointerDrag(e, this, dragging, released);
}
$(document).on('pointerdown', '.main-row', dragObjectProps);

function resetHeight() {
  var top = $(window).height() - oldtimelinepos - 92;
  if ($('#upload-tool').hasClass('tool-active')) {
    $('#browser').css('top', '150px');
    $('#browser').css(
      'height',
      'calc(100% - ' + (top + 97 + 150) + 'px)'
    );
  } else {
    $('#browser').css('top', '110px');
    $('#browser').css(
      'height',
      'calc(100% - ' + (top + 97 + 100) + 'px)'
    );
  }
  $('#timearea').css('height', top);
  $('#layer-list').css('height', top);
  $('#toolbar').css('height', 'calc(100% - ' + (top + 97) + 'px)');
  $('#canvas-area').css(
    'height',
    'calc(100% - ' + (top + 97) + 'px)'
  );
  $('#properties').css('height', 'calc(100% - ' + (top + 97) + 'px)');
  $('.panel-handle').css(
    'height',
    'calc(100% - ' + (top + 97) + 'px)'
  );
  $('#timeline-handle').css('bottom', top + 95);
  resizeCanvas();
}

// Dragging timeline vertically
function dragTimeline(e) {
  if (e.which == 3 || modalOpen()) {
    return false;
  }
  // Suppress text selection for the duration of the drag only - leaving these
  // handlers installed kills mousedown for the whole document.
  const disableselect = function () {
    return false;
  };
  const previousSelectStart = document.onselectstart;
  document.onselectstart = disableselect;

  oldtimelinepos = e.pageY;
  function draggingKeyframe(e) {
    oldtimelinepos = e.pageY;
    resetHeight(e);
  }
  function releasedKeyframe(e) {
    document.onselectstart = previousSelectStart || null;
  }
  bindPointerDrag(e, this, draggingKeyframe, releasedKeyframe);
}

$(document).on('pointerdown', '#timeline-handle', dragTimeline);

oldtimelinepos = $(window).height() - 92 - $('#timearea').height();

// Side panels: horizontal resize and hide/show
// The widths live as CSS variables on :root - every panel rule is a calc() of
// them, so moving a handle only has to write one number.
const RAIL_WIDTH = 76;
const BROWSER_MIN_WIDTH = 210;
const BROWSER_MAX_WIDTH = 640;
const PROPS_MIN_WIDTH = 300;
const PROPS_MAX_WIDTH = 640;
// Canvas width kept free while dragging, whatever the window size
const CANVAS_MIN_WIDTH = 240;
const PANEL_LAYOUT_KEY = 'motionity-panel-layout';

var browserwidth = 299;
var propswidth = 300;
var propshidden = false;
// Tool to reopen the library with, set when it gets collapsed
var lasttool = $('.tool-active').attr('id') || 'shape-tool';

function clampWidth(value, min, max) {
  return Math.max(min, Math.min(max, value));
}

function browserHidden() {
  return $('#browser').hasClass('collapsed');
}

function applyPanelWidths() {
  const root = document.documentElement.style;
  const hidden = browserHidden();
  var browser = hidden ? 0 : browserwidth;
  var properties = propshidden ? 0 : propswidth;
  // Shrink to fit rather than squeeze the canvas out of a narrow window. The
  // stored widths are left alone, so the panels grow back on a wider one.
  var over =
    RAIL_WIDTH +
    browser +
    properties +
    CANVAS_MIN_WIDTH -
    $(window).width();
  if (over > 0 && properties > PROPS_MIN_WIDTH) {
    const cut = Math.min(over, properties - PROPS_MIN_WIDTH);
    properties -= cut;
    over -= cut;
  }
  if (over > 0 && browser > BROWSER_MIN_WIDTH) {
    browser -= Math.min(over, browser - BROWSER_MIN_WIDTH);
  }
  root.setProperty('--browser-w', browser + 'px');
  root.setProperty('--props-w', properties + 'px');
  // The layer list and the timeline keep their column width while the library
  // is hidden, otherwise layer names would get squashed into the tool rail.
  if (!hidden) {
    root.setProperty('--layers-w', RAIL_WIDTH + browser + 'px');
  }
  $('#browser-handle').toggleClass('panel-hidden', hidden);
  $('#properties-handle').toggleClass('panel-hidden', propshidden);
  $('#properties').toggleClass('collapsed', propshidden);
  $('#browser-toggle').attr(
    'title',
    hidden ? 'Show the library' : 'Hide the library'
  );
  $('#properties-toggle').attr(
    'title',
    propshidden ? 'Show the properties' : 'Hide the properties'
  );
}

function savePanelLayout() {
  try {
    localStorage.setItem(
      PANEL_LAYOUT_KEY,
      JSON.stringify({
        browser: browserwidth,
        properties: propswidth,
        propertieshidden: propshidden,
      })
    );
  } catch (err) {
    // Private browsing or a full quota - the layout just won't persist
  }
}

function restorePanelLayout() {
  var stored;
  try {
    stored = JSON.parse(localStorage.getItem(PANEL_LAYOUT_KEY));
  } catch (err) {
    stored = null;
  }
  if (stored) {
    if (typeof stored.browser == 'number') {
      browserwidth = clampWidth(
        stored.browser,
        BROWSER_MIN_WIDTH,
        BROWSER_MAX_WIDTH
      );
    }
    if (typeof stored.properties == 'number') {
      propswidth = clampWidth(
        stored.properties,
        PROPS_MIN_WIDTH,
        PROPS_MAX_WIDTH
      );
    }
    propshidden = stored.propertieshidden === true;
  }
  applyPanelWidths();
}

// Dragging a side panel border
function dragPanel(e) {
  if (e.which == 3 || modalOpen() || $(this).hasClass('panel-hidden')) {
    return false;
  }
  e.preventDefault();
  const handle = $(this);
  const isbrowser = handle.attr('id') == 'browser-handle';
  const startx = e.pageX;
  const startwidth = isbrowser ? browserwidth : propswidth;
  // Suppress text selection for the duration of the drag only
  const previousSelectStart = document.onselectstart;
  document.onselectstart = function () {
    return false;
  };
  handle.addClass('handle-dragging');

  function draggingPanelBorder(ev) {
    // The properties panel is anchored to the right, so its width grows the
    // other way round
    const delta = isbrowser ? ev.pageX - startx : startx - ev.pageX;
    const otherwidth = isbrowser
      ? propshidden
        ? 0
        : propswidth
      : browserHidden()
      ? 0
      : browserwidth;
    const room =
      $(window).width() - RAIL_WIDTH - CANVAS_MIN_WIDTH - otherwidth;
    if (isbrowser) {
      browserwidth = clampWidth(
        startwidth + delta,
        BROWSER_MIN_WIDTH,
        Math.min(BROWSER_MAX_WIDTH, room)
      );
    } else {
      propswidth = clampWidth(
        startwidth + delta,
        PROPS_MIN_WIDTH,
        Math.min(PROPS_MAX_WIDTH, room)
      );
    }
    applyPanelWidths();
    resizeCanvas();
  }
  function releasedPanelBorder() {
    document.onselectstart = previousSelectStart || null;
    handle.removeClass('handle-dragging');
    savePanelLayout();
  }
  bindPointerDrag(e, this, draggingPanelBorder, releasedPanelBorder);
}
$(document).on('pointerdown', '.panel-handle', dragPanel);

// Keep a click on a hide/show button from starting a resize drag
$(document).on('pointerdown', '.panel-toggle', function (e) {
  e.stopPropagation();
});

$(document).on('click', '#browser-toggle', function (e) {
  e.stopPropagation();
  if (browserHidden()) {
    // Reopening goes through the tool click so the rail icons stay in sync
    $('#' + lasttool).trigger('click');
  } else {
    collapsePanel();
  }
});

$(document).on('click', '#properties-toggle', function (e) {
  e.stopPropagation();
  propshidden = !propshidden;
  applyPanelWidths();
  savePanelLayout();
  resizeCanvas();
});

// Re-fits the panels after the window itself changed size. Runs after the
// resizeCanvas listener registered at the top of this file, so the canvas is
// measured again once the new widths are in.
window.addEventListener(
  'resize',
  function () {
    applyPanelWidths();
    resizeCanvas();
  },
  false
);

restorePanelLayout();
resizeCanvas();

// Sync scrolling (vertical)
function syncScroll(el1, el2) {
  var $el1 = $(el1);
  var $el2 = $(el2);
  var forcedScroll = false;
  $el1.scroll(function () {
    performScroll($el1, $el2);
  });
  $el2.scroll(function () {
    performScroll($el2, $el1);
  });

  function performScroll($scrolled, $toScroll) {
    if (forcedScroll) return (forcedScroll = false);
    var percent =
      ($scrolled.scrollTop() /
        ($scrolled[0].scrollHeight - $scrolled.outerHeight())) *
      100;
    setScrollTopFromPercent($toScroll, percent);
  }

  function setScrollTopFromPercent($el, percent) {
    var scrollTopPos =
      (percent / 100) * ($el[0].scrollHeight - $el.outerHeight());
    forcedScroll = true;
    $el.scrollTop(scrollTopPos);
  }
}

// Sync scrolling (horizontal)
function syncScrollHoz(el1, el2) {
  var $el1 = $(el1);
  var $el2 = $(el2);
  var forcedScroll = false;
  $el1.scroll(function () {
    performScroll($el1, $el2);
  });
  $el2.scroll(function () {
    performScroll($el2, $el1);
  });

  function performScroll($scrolled, $toScroll) {
    if (forcedScroll) return (forcedScroll = false);
    var percent =
      ($scrolled.scrollLeft() / $scrolled.outerWidth()) * 100;
    setScrollLeftFromPercent($toScroll, percent);
  }

  function setScrollLeftFromPercent($el, percent) {
    var scrollLeftPos = (percent / 100) * $el.outerWidth();
    forcedScroll = true;
    $el.scrollLeft(scrollLeftPos);
  }
}

// Show keyframe properties
function keyframeProperties(inst) {
  if (!shiftdown) {
    selectedkeyframe = $(inst);
    const popup = $('#keyframe-properties');
    var keyarr = keyframes.filter(function (e) {
      return (
        e.t == selectedkeyframe.attr('data-time') &&
        e.id == selectedkeyframe.attr('data-object') &&
        e.name == selectedkeyframe.attr('data-property')
      );
    });
    if (keyarr.length == 0) {
      return;
    }
    $('#easing select').val(keyarr[0].easing);
    $('#easing select').niceSelect('update');
    popup.css({
      left: $(inst).offset().left - popup.width() / 2,
      top: $(inst).offset().top - popup.height() - 20,
    });
    popup.addClass('show-properties');
    $(inst).addClass('keyframe-selected');
  }
}

// Apply easing to keyframe
function applyEasing() {
  if (!selectedkeyframe) {
    return;
  }
  // Read the clicked item, not the underlying <select>: nice-select has not
  // necessarily written the value back yet on mouseup.
  const easing = $(this).attr('data-value');
  const time = selectedkeyframe.attr('data-time');
  const id = selectedkeyframe.attr('data-object');
  const prop = selectedkeyframe.attr('data-property');
  const names = [prop].concat(KEYFRAME_COUNTERPARTS[prop] || []);
  names.forEach(function (name) {
    keyframes
      .filter(function (e) {
        return e.t == time && e.id == id && e.name == name;
      })
      .forEach(function (e) {
        e.easing = easing;
      });
  });
  $('#keyframe-properties').removeClass('show-properties');
  selectedkeyframe.removeClass('keyframe-selected');
  animate(false, currenttime);
  save();
}
$(document).on('mouseup', '#easing li', applyEasing);

// Click on seek area to seek (still not working properly)
function seekTo(e) {
  if ($(e.target).hasClass('keyframe')) {
    return false;
  }
  paused = true;
  if ($('#seekarea').scrollLeft() > offset_left) {
    currenttime = parseFloat(
      (
        (e.pageX +
          $('#seekarea').scrollLeft() -
          $('#timearea').offset().left -
          offset_left) *
        timelinetime
      ).toFixed(1)
    );
  } else {
    currenttime = parseFloat(
      (
        (e.pageX +
          $('#seekarea').scrollLeft() -
          $('#timearea').offset().left -
          offset_left) *
        timelinetime
      ).toFixed(1)
    );
  }
  if (currenttime < 0) {
    currenttime = 0;
  }
  // Check for 60FPS playback, 16ms "slots"
  if (currenttime % 16.666 != 0) {
    currenttime = Math.ceil(currenttime / 16.666) * 16.666;
  }
  renderTime();
  $('#seekbar').offset({
    left:
      offset_left +
      $('#inner-timeline').offset().left +
      currenttime / timelinetime,
  });
  animate(false, currenttime);
  updatePanelValues();
}
$(document).on('click', '#seekevents', seekTo);
$(document).on('click', '#timearea', seekTo);

function hideSeekbar() {
  $('#seek-hover').css({ opacity: 0 });
}
function followCursor(e) {
  $('#seek-hover').css({ opacity: 0.3 });
  if ($('#seekarea').scrollLeft() > offset_left) {
    hovertime = parseFloat(
      (
        (e.pageX +
          $('#seekarea').scrollLeft() -
          $('#timearea').offset().left -
          offset_left) *
        timelinetime
      ).toFixed(1)
    );
  } else {
    hovertime = parseFloat(
      (
        (e.pageX +
          $('#seekarea').scrollLeft() -
          $('#timearea').offset().left -
          offset_left) *
        timelinetime
      ).toFixed(1)
    );
  }
  if (e.pageX >= offset_left + $('#inner-timeline').offset().left) {
    $('#seek-hover').offset({ left: e.pageX });
  }
}
$(document).on('mousemove', '#timearea', followCursor);
$(document).on('mousemove', '#seekevents', followCursor);
$(document).on('mousemove', '#toolbar', hideSeekbar);
$(document).on('mousemove', '#canvas-area', hideSeekbar);
$(document).on('mousemove', '#browser', hideSeekbar);
$(document).on('mousemove', '#properties', hideSeekbar);
$(document).on('mousemove', '#controls', hideSeekbar);

function orderLayers() {
  $('.layer').each(function (index) {
    const object = canvas.getItemById($(this).attr('data-object'));
    canvas.sendToBack(object);
    canvas.renderAll();
    objects.splice(
      $('.layer').length - index - 1,
      0,
      objects.splice(
        objects.findIndex((x) => x.id == object.get('id')),
        1
      )[0]
    );
  });
  save();
}

function handTool() {
  if ($(this).hasClass('hand-active')) {
    $(this).removeClass('hand-active');
    $(this).find('img').attr('src', 'assets/hand-tool.svg');
    handtool = false;
    canvas.defaultCursor = 'default';
    canvas.renderAll();
  } else {
    $(this).addClass('hand-active');
    $(this).find('img').attr('src', 'assets/hand-tool-active.svg');
    handtool = true;
    canvas.defaultCursor = 'grab';
    canvas.renderAll();
  }
}
$(document).on('click', '#hand-tool', handTool);
// Set defaults
setDuration(10000);
checkDB();
