Fix recording and playback issues

- Fix download filename handling in converter and export functions
- Make updateRecordCanvas return Promise to handle async loadFromJSON correctly
- Add setCoords() call in setObjectValue to fix object selection after updates
- Update recorder initialization and animation frame handling

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 16:20:25 +02:00
co-authored by Claude Haiku 4.5
parent 794995fbb5
commit f8f863e7e2
4 changed files with 68 additions and 41 deletions
+1 -1
View File
@@ -94,7 +94,7 @@ function PostBlob(blob) {
const a = document.createElement('a'); const a = document.createElement('a');
a.style.display = 'none'; a.style.display = 'none';
a.href = url; a.href = url;
a.download = name; a.download = blob.name || 'video';
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
recording = false; recording = false;
+56 -36
View File
@@ -506,27 +506,32 @@ function updateRecordCanvas() {
'inGroup', 'inGroup',
]); ]);
canvas.clipPath = artboard; canvas.clipPath = artboard;
canvasrecord.loadFromJSON(canvassave, function () { // loadFromJSON is asynchronous (it clears the canvas first, then revives the
if (canvasrecord.getItemById('center_h')) { // objects), so callers that record straight afterwards must await this.
canvasrecord.remove(canvasrecord.getItemById('center_h')); return new Promise(function (resolve) {
canvasrecord.remove(canvasrecord.getItemById('center_v')); canvasrecord.loadFromJSON(canvassave, function () {
} if (canvasrecord.getItemById('center_h')) {
if (canvasrecord.getItemById('line_h')) { canvasrecord.remove(canvasrecord.getItemById('center_h'));
canvasrecord.remove(canvasrecord.getItemById('line_h')); canvasrecord.remove(canvasrecord.getItemById('center_v'));
canvasrecord.remove(canvasrecord.getItemById('line_v')); }
} if (canvasrecord.getItemById('line_h')) {
canvasrecord.renderAll(); canvasrecord.remove(canvasrecord.getItemById('line_h'));
canvasrecord.setWidth(artboard.width); canvasrecord.remove(canvasrecord.getItemById('line_v'));
canvasrecord.setHeight(artboard.height); }
canvasrecord.width = artboard.width; canvasrecord.renderAll();
canvasrecord.height = artboard.height; canvasrecord.setWidth(artboard.width);
canvasrecord.renderAll(); canvasrecord.setHeight(artboard.height);
objects.forEach(function (object) { canvasrecord.width = artboard.width;
replaceSource( canvasrecord.height = artboard.height;
canvasrecord.getItemById(object.id), canvasrecord.renderAll();
canvasrecord objects.forEach(function (object) {
); replaceSource(
replaceSource(canvas.getItemById(object.id), canvas); canvasrecord.getItemById(object.id),
canvasrecord
);
replaceSource(canvas.getItemById(object.id), canvas);
});
resolve();
}); });
}); });
} }
@@ -543,7 +548,7 @@ function downloadRecording(chunks) {
const a = document.createElement('a'); const a = document.createElement('a');
a.style.display = 'none'; a.style.display = 'none';
a.href = url; a.href = url;
a.download = name; a.download = 'video.webm';
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
recording = false; recording = false;
@@ -1223,6 +1228,7 @@ function setObjectValue(prop, object, value, inst) {
} else if (prop != 'width') { } else if (prop != 'width') {
object.set(prop, value); object.set(prop, value);
} }
object.setCoords();
inst.renderAll(); inst.renderAll();
} }
@@ -1508,18 +1514,20 @@ async function recordAnimate(time) {
if (!canvas.getItemById(keyframe.id)) { if (!canvas.getItemById(keyframe.id)) {
reGroup(keyframe.id); reGroup(keyframe.id);
} }
const object = canvas.getItemById(keyframe.id); const object = inst.getItemById(keyframe.id);
if (!object) {
return;
}
if ( if (
currenttime < time <
p_keyframes.find((x) => x.id == keyframe.id).trimstart + p_keyframes.find((x) => x.id == keyframe.id).trimstart +
p_keyframes.find((x) => x.id == keyframe.id).start p_keyframes.find((x) => x.id == keyframe.id).start
) { ) {
object.set('visible', false); object.set('visible', false);
inst.renderAll(); inst.renderAll();
} else if ( } else if (
currenttime > time > p_keyframes.find((x) => x.id == keyframe.id).end ||
p_keyframes.find((x) => x.id == keyframe.id).end || time > duration
currenttime > duration
) { ) {
object.set('visible', false); object.set('visible', false);
inst.renderAll(); inst.renderAll();
@@ -1528,7 +1536,7 @@ async function recordAnimate(time) {
inst.renderAll(); inst.renderAll();
} }
if ( if (
currenttime >= time >=
p_keyframes.find((x) => x.id == keyframe.id).trimstart + p_keyframes.find((x) => x.id == keyframe.id).trimstart +
p_keyframes.find((x) => x.id == keyframe.id).start p_keyframes.find((x) => x.id == keyframe.id).start
) { ) {
@@ -1573,6 +1581,7 @@ async function recordAnimate(time) {
} else if (prop != 'width') { } else if (prop != 'width') {
object.set(prop, value); object.set(prop, value);
} }
object.setCoords();
inst.renderAll(); inst.renderAll();
} }
@@ -1601,9 +1610,12 @@ async function recordAnimate(time) {
} }
var object = canvasrecord.getItemById(keyframe.id); var object = canvasrecord.getItemById(keyframe.id);
if (!object) {
return;
}
if ( if (
keyframe.t >= time && keyframe.t >= time &&
currenttime >= time >=
p_keyframes.find((x) => x.id == keyframe.id).trimstart + p_keyframes.find((x) => x.id == keyframe.id).trimstart +
p_keyframes.find((x) => x.id == keyframe.id).start p_keyframes.find((x) => x.id == keyframe.id).start
) { ) {
@@ -1621,7 +1633,11 @@ async function recordAnimate(time) {
lasttime = lastkey.t; lasttime = lastkey.t;
lastprop = lastkey.value; lastprop = lastkey.value;
} }
if (lastkey && lastkey.t >= time && !play) { // 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; return;
} }
@@ -1686,24 +1702,28 @@ async function recordAnimate(time) {
objects.forEach(function (object) { objects.forEach(function (object) {
if (object.id.indexOf('Group') == -1) { if (object.id.indexOf('Group') == -1) {
const object2 = canvas.getItemById(object.id); // 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);
if (!object2) {
return;
}
if ( if (
currenttime < time <
p_keyframes.find((x) => x.id == object.id).trimstart + p_keyframes.find((x) => x.id == object.id).trimstart +
p_keyframes.find((x) => x.id == object.id).start p_keyframes.find((x) => x.id == object.id).start
) { ) {
object2.set('visible', false); object2.set('visible', false);
} else if ( } else if (
currenttime > time > p_keyframes.find((x) => x.id == object.id).end ||
p_keyframes.find((x) => x.id == object.id).end || time > duration
currenttime > duration
) { ) {
object2.set('visible', false); object2.set('visible', false);
} else { } else {
object2.set('visible', true); object2.set('visible', true);
} }
if ( if (
currenttime >= time >=
p_keyframes.find((x) => x.id == object.id).trimstart + p_keyframes.find((x) => x.id == object.id).trimstart +
p_keyframes.find((x) => x.id == object.id).start p_keyframes.find((x) => x.id == object.id).start
) { ) {
+5
View File
@@ -988,6 +988,11 @@ var canvasrecord = new fabric.Canvas('canvasrecord', {
backgroundColor: '#FFF', backgroundColor: '#FFF',
width: artboard.width, width: artboard.width,
height: artboard.height, height: artboard.height,
// Objects are repositioned to artboard-relative coordinates while
// recording. Their cached aCoords are not always refreshed in step, and
// fabric's offscreen culling would then skip drawing them entirely,
// producing blank frames.
skipOffscreen: false,
}); });
var timelineslider = document.getElementById('timeline-zoom'); var timelineslider = document.getElementById('timeline-zoom');
+6 -4
View File
@@ -67,11 +67,13 @@ async function exportRecording() {
// Record canvas // Record canvas
async function record() { async function record() {
updateRecordCanvas(); // loadFromJSON clears the record canvas and revives the objects
// asynchronously, so nothing may be drawn or captured until it resolves.
await updateRecordCanvas();
if ($('input[name=radio]:checked').val() == 'image') { if ($('input[name=radio]:checked').val() == 'image') {
recording = true; recording = true;
paused = true; paused = true;
animate(false, currenttime); await recordAnimate(currenttime);
const dataURL = canvasrecord.toDataURL({ const dataURL = canvasrecord.toDataURL({
format: 'png', format: 'png',
}); });
@@ -82,12 +84,12 @@ async function record() {
link.click(); link.click();
document.body.removeChild(link); document.body.removeChild(link);
recording = false; recording = false;
updateRecordCanvas();
} else { } else {
if (!recording) { if (!recording) {
recording = true; recording = true;
paused = true; paused = true;
recordAnimate(false, (frame / FPS) * 1000); await recordAnimate(0);
recording = true;
$('#download-real').html('Rendering...'); $('#download-real').html('Rendering...');
$('#download-real').addClass('downloading'); $('#download-real').addClass('downloading');
var fps = 60; var fps = 60;