Merge pull request 'Fix/upstream open bugs' (#2) from fix/upstream-open-bugs into main
Sync Gitea releases to GitHub / sync-releases (push) Waiting to run

Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
2026-08-14 10:22:45 +02:00
7 changed files with 593 additions and 104 deletions
+53
View File
@@ -315,3 +315,56 @@ held the Playwright profile lock), so the following still needs a manual pass:
console shows `Rendering n%` (frame-accurate path) and not console shows `Rendering n%` (frame-accurate path) and not
`Falling back to the real-time encoder` `Falling back to the real-time encoder`
4. Play the result: check audio is in sync and the video layer is not smeared 4. Play the result: check audio is in sync and the video layer is not smeared
---
## Upstream open issues (alyssaxuu/motionity)
The 13 bug reports still open upstream were replayed in Chromium against this
fork. Nine were already fixed by the audit above — mp4 export (#16), blank
render (#25), image download (#10), audio on download (#30), filters not
retained (#24), `EyeDropper is not defined` (#15, #18), the endless
*Loading video…* (#28) and text selection while resizing the timeline (#5).
GIF export (#21) works through the same ffmpeg path. #8 (video from the search
tab) needs a Pixabay API key, so it stays untested; #29 and #4 carry no
reproducer. The remaining three are fixed here:
- [x] **#23 — Border radius behaved like a percentage** — `src/js/functions.js`,
`src/js/ui.js`, `src/js/events.js`, `src/js/database.js`
fabric applies `rx`/`ry` before the object's scale, and shapes are resized by
scaling, so a radius typed as 20 drew at 60 px on a rect scaled 3x while the
panel — which read `rx` back raw — still said 20. A new `cornerRadius` property
stores the pixel value the user asked for; `rx`/`ry` are derived from it
(`setCornerRadius`) and re-derived on `object:scaling` / `object:modified`
(`syncCornerRadius`), so the same number means the same pixels at any size.
`cornerRadius` was added to the four serialised property lists, and
`getCornerRadius` falls back to `rx * scaleX` for projects saved before it
existed. Known limit, unchanged from before: if `scaleX` is *keyframed*, the
drawn radius still varies over the animation.
Verified: typed 20 px draws a 20 px corner at scale 1 and at scale 3, the
panel keeps reading 20, and the value survives save, JSON round-trip and a
page reload.
- [x] **#27 — Cropping a rotated image** — `src/js/functions.js`
`crop()` compared canvas-space edges and covered only three of the four
quadrants, so any rotated image cropped the wrong region — and nothing at all
when the crop window was centred on it, since all three branches need a
strictly positive offset. The guards added in the audit stopped the crash but
left the geometry wrong. `crop()` now works in the image's own frame
(`rotateVector`), clamps the region to the bitmap, and re-centres the object on
the region it kept; the crop window is created with the image's `angle`, and
the "expand back to the full bitmap" shift in `cropImage` is rotated the same
way. The four-branch soup is gone.
Verified: at 0°, 30° and 45°, the pixels under the crop window are byte
identical before and after the crop, everything outside it is dropped, and
crop mode exits cleanly.
- [x] **#1 — A modal did not block the timeline** — `src/js/functions.js`
Modals are painted over the timeline but never took its pointer events, so the
resize handle, the seekbar, keyframes and layer bars all still reacted to a
drag behind the dialog. The onboarding modal from the report does not exist in
the open-source build; the export, import/export and credits modals all had
the bug. `dragTimeline`, `dragSeekBar`, `dragKeyframe` and `dragObjectProps`
now bail while `.modal-open` is present.
Verified: with the export modal open a real mouse drag on the handle leaves
the timeline height untouched, and dragging works again once it closes.
+6
View File
@@ -311,6 +311,12 @@
<img src="assets/loading-video.svg" id="load-video" class="load-media"> <img src="assets/loading-video.svg" id="load-video" class="load-media">
<canvas id="canvas"></canvas> <canvas id="canvas"></canvas>
</div> </div>
<div id="browser-handle" class="panel-handle noselect" title="Drag to resize">
<div class="panel-toggle" id="browser-toggle"></div>
</div>
<div id="properties-handle" class="panel-handle noselect" title="Drag to resize">
<div class="panel-toggle" id="properties-toggle"></div>
</div>
<div id="timeline-handle"></div> <div id="timeline-handle"></div>
<div id="bottom-area" class="noselect"> <div id="bottom-area" class="noselect">
<div id="keyframe-properties"> <div id="keyframe-properties">
+13
View File
@@ -50,8 +50,11 @@ function checkDB() {
'strokeUniform', 'strokeUniform',
'rx', 'rx',
'ry', 'ry',
'cornerRadius',
'selectable', 'selectable',
'hasControls', 'hasControls',
'hasBorders',
'evented',
'subTargetCheck', 'subTargetCheck',
'id', 'id',
'hoverCursor', 'hoverCursor',
@@ -226,8 +229,11 @@ async function autoSave() {
'strokeUniform', 'strokeUniform',
'rx', 'rx',
'ry', 'ry',
'cornerRadius',
'selectable', 'selectable',
'hasControls', 'hasControls',
'hasBorders',
'evented',
'subTargetCheck', 'subTargetCheck',
'id', 'id',
'hoverCursor', 'hoverCursor',
@@ -346,6 +352,13 @@ function loadProject() {
}); });
replaceSource(canvas.getItemById(object.id), canvas); replaceSource(canvas.getItemById(object.id), canvas);
} else { } else {
// Projects saved before audio layers were flagged still carry
// controls / borders, which show up as a phantom box at 0,0.
canvas.getItemById(object.id).set({
hasControls: false,
hasBorders: false,
evented: false,
});
renderProp('volume', canvas.getItemById(object.id)); renderProp('volume', canvas.getItemById(object.id));
} }
}); });
+6
View File
@@ -100,6 +100,8 @@ $(document).ready(function () {
canvas.on('object:scaling', function (e) { canvas.on('object:scaling', function (e) {
e.target.hasControls = false; e.target.hasControls = false;
centerLines(e); centerLines(e);
// Keep the corner radius at its pixel value while the handle is dragged
syncCornerRadius(e.target);
if (cropping) { if (cropping) {
updateCropBounds(); updateCropBounds();
crop(canvas.getItemById('cropped')); crop(canvas.getItemById('cropped'));
@@ -135,12 +137,16 @@ $(document).ready(function () {
canvas.renderAll(); canvas.renderAll();
if (e.target.type == 'activeSelection') { if (e.target.type == 'activeSelection') {
const tempselection = canvas.getActiveObject(); const tempselection = canvas.getActiveObject();
// Discarding first bakes the group transform into the children, so
// their scale is final by the time the radius is re-derived
canvas.discardActiveObject(); canvas.discardActiveObject();
e.target._objects.forEach(function (object) { e.target._objects.forEach(function (object) {
syncCornerRadius(object);
autoKeyframe(object, e, true); autoKeyframe(object, e, true);
}); });
reselect(tempselection); reselect(tempselection);
} else { } else {
syncCornerRadius(e.target);
autoKeyframe(e.target, e, false); autoKeyframe(e.target, e, false);
} }
updatePanelValues(); updatePanelValues();
+353 -68
View File
@@ -495,6 +495,7 @@ async function updateRecordCanvas() {
'strokeUniform', 'strokeUniform',
'rx', 'rx',
'ry', 'ry',
'cornerRadius',
'selectable', 'selectable',
'hasControls', 'hasControls',
'subTargetCheck', 'subTargetCheck',
@@ -684,6 +685,7 @@ function save() {
'strokeUniform', 'strokeUniform',
'rx', 'rx',
'ry', 'ry',
'cornerRadius',
'selectable', 'selectable',
'hasControls', 'hasControls',
'subTargetCheck', 'subTargetCheck',
@@ -1050,13 +1052,57 @@ $(document).on('click', '#redo', function () {
} }
}); });
// 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 // Generate keyframes
function keyframeChanges(object, type, id, selection) { function keyframeChanges(object, type, id, selection) {
if (object.get('type') == 'rect') { if (object.get('type') == 'rect') {
object.set({ setCornerRadius(
rx: parseFloat($('#object-corners input').val()), object,
ry: parseFloat($('#object-corners input').val()), parseFloat($('#object-corners input').val())
}); );
} else if (object.get('type') == 'textbox') { } else if (object.get('type') == 'textbox') {
object.set({ object.set({
charSpacing: parseFloat($('#text-h input').val()) * 10, charSpacing: parseFloat($('#text-h input').val()) * 10,
@@ -3512,7 +3558,23 @@ function checkCrop(obj) {
crop(canvas.getItemById('cropped')); 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 // 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) { function crop(obj) {
var cropUI = canvas.getItemById('crop'); var cropUI = canvas.getItemById('crop');
if (!obj || !cropUI || !cropobj) { if (!obj || !cropUI || !cropobj) {
@@ -3520,58 +3582,54 @@ function crop(obj) {
} }
cropobj.setCoords(); cropobj.setCoords();
cropUI.setCoords(); cropUI.setCoords();
var cleft = // Read everything before writing: the final call passes cropobj as `obj`
cropUI.get('left') - (cropUI.get('width') * cropUI.get('scaleX')) / 2; var scaleX = cropobj.get('scaleX') || 1;
var ctop = var scaleY = cropobj.get('scaleY') || 1;
cropUI.get('top') - (cropUI.get('height') * cropUI.get('scaleY')) / 2; var ogWidth = cropobj.get('width');
var height = var ogHeight = cropobj.get('height');
(cropUI.get('height') / cropobj.get('scaleY')) * cropUI.get('scaleY'); var centerX = cropobj.get('left');
var width = var centerY = cropobj.get('top');
(cropUI.get('width') / cropobj.get('scaleX')) * cropUI.get('scaleX'); var radians = fabric.util.degreesToRadians(
var img_height = cropobj.get('height') * cropobj.get('scaleY'); cropobj.get('angle') || 0
var img_width = cropobj.get('width') * cropobj.get('scaleX'); );
var left =
cleft - // The crop window carries the image's angle, so its own width and height are
(cropobj.get('left') - // already axis-aligned with the image
(cropobj.get('width') * cropobj.get('scaleX')) / 2); var width = (cropUI.get('width') * cropUI.get('scaleX')) / scaleX;
var top = var height = (cropUI.get('height') * cropUI.get('scaleY')) / scaleY;
ctop -
(cropobj.get('top') - // Where the crop window sits relative to the image centre, un-rotated
(cropobj.get('height') * cropobj.get('scaleY')) / 2); var offset = rotateVector(
if (left < 0 && top > 0) { cropUI.get('left') - centerX,
obj cropUI.get('top') - centerY,
.set({ cropY: top / cropobj.get('scaleY'), height: height }) -radians
.setCoords(); );
canvas.renderAll(); var cropX = offset.x / scaleX + ogWidth / 2 - width / 2;
obj.set({ var cropY = offset.y / scaleY + ogHeight / 2 - height / 2;
top: ctop + (obj.get('height') * obj.get('scaleY')) / 2,
}); // Never leave the source bitmap: fabric draws nothing for a negative crop
canvas.renderAll(); width = Math.max(1, Math.min(width, ogWidth));
} else if (top < 0 && left > 0) { height = Math.max(1, Math.min(height, ogHeight));
obj cropX = Math.max(0, Math.min(cropX, ogWidth - width));
.set({ cropX: left / cropobj.get('scaleX'), width: width }) cropY = Math.max(0, Math.min(cropY, ogHeight - height));
.setCoords();
canvas.renderAll(); // Put the cropped object back under the (clamped) region it represents
obj.set({ var center = rotateVector(
left: cleft + (obj.get('width') * obj.get('scaleX')) / 2, (cropX + width / 2 - ogWidth / 2) * scaleX,
}); (cropY + height / 2 - ogHeight / 2) * scaleY,
canvas.renderAll(); radians
} else if (top > 0 && left > 0) { );
obj obj
.set({ .set({
cropX: left / cropobj.get('scaleX'), cropX: cropX,
cropY: top / cropobj.get('scaleY'), cropY: cropY,
height: height,
width: width, width: width,
height: height,
left: centerX + center.x,
top: centerY + center.y,
}) })
.setCoords(); .setCoords();
canvas.renderAll(); canvas.renderAll();
obj.set({
left: cleft + (obj.get('width') * obj.get('scaleX')) / 2,
top: ctop + (obj.get('height') * obj.get('scaleY')) / 2,
});
canvas.renderAll();
}
if (obj.get('id') != 'cropped') { if (obj.get('id') != 'cropped') {
canvas.remove(cropUI); canvas.remove(cropUI);
canvas.remove(canvas.getItemById('crop-overlay')); canvas.remove(canvas.getItemById('crop-overlay'));
@@ -3626,20 +3684,23 @@ function cropImage(object) {
cropobj = object; cropobj = object;
canvas.uniformScaling = false; canvas.uniformScaling = false;
cropobj.setCoords(); cropobj.setCoords();
var left = // The visible region, in the source bitmap's own pixels, before the image
cropobj.get('left') - // is expanded back to its full size below
(cropobj.get('width') * cropobj.get('scaleX')) / 2;
var top =
cropobj.get('top') -
(cropobj.get('height') * cropobj.get('scaleY')) / 2;
var cropx = cropobj.get('cropX'); var cropx = cropobj.get('cropX');
var cropy = cropobj.get('cropY'); 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(); overlay();
var cropUI = new fabric.Rect({ var cropUI = new fabric.Rect({
left: object.get('left'), left: object.get('left'),
top: object.get('top'), top: object.get('top'),
width: object.get('width') * object.get('scaleX') - 5, width: object.get('width') * object.get('scaleX') - 5,
height: object.get('height') * object.get('scaleY') - 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', originX: 'center',
originY: 'center', originY: 'center',
id: 'crop', id: 'crop',
@@ -3674,15 +3735,19 @@ function cropImage(object) {
}) })
.setCoords(); .setCoords();
canvas.renderAll(); 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({ cropobj.set({
left: left: shownLeft - shift.x,
left + top: shownTop - shift.y,
(cropobj.get('width') * cropobj.get('scaleX')) / 2 -
cropx * cropobj.get('scaleX'),
top:
top +
(cropobj.get('height') * cropobj.get('scaleY')) / 2 -
cropy * cropobj.get('scaleY'),
}); });
cropUI.setControlsVisibility({ cropUI.setControlsVisibility({
mt: false, mt: false,
@@ -4058,6 +4123,7 @@ function newRectangle(color) {
backgroundColor: 'rgba(255,255,255,0)', backgroundColor: 'rgba(255,255,255,0)',
rx: 0, rx: 0,
ry: 0, ry: 0,
cornerRadius: 0,
fill: color, fill: color,
cursorWidth: 1, cursorWidth: 1,
cursorDuration: 1, cursorDuration: 1,
@@ -4315,6 +4381,11 @@ function newAudioLayer(src) {
duration: audio.duration * 1000, duration: audio.duration * 1000,
opacity: 0, opacity: 0,
selectable: false, 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, volume: 0.5,
assetType: 'audio', assetType: 'audio',
shadow: { shadow: {
@@ -4862,9 +4933,17 @@ function bindPointerDrag(e, el, onMove, onEnd) {
window.addEventListener('blur', end); 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 // Dragging a keyframe
function dragKeyframe(e) { function dragKeyframe(e) {
if (e.which == 3) { if (e.which == 3 || modalOpen()) {
return false; return false;
} }
e.stopPropagation(); e.stopPropagation();
@@ -5014,7 +5093,7 @@ function updateTime(drag, check) {
// Dragging the seekbar // Dragging the seekbar
function dragSeekBar(e) { function dragSeekBar(e) {
if (e.which == 3) { if (e.which == 3 || modalOpen()) {
return false; return false;
} }
// Stop the browser from turning the press into a native drag-and-drop. // Stop the browser from turning the press into a native drag-and-drop.
@@ -5085,7 +5164,7 @@ $(document).on(
// Dragging layer horizontally // Dragging layer horizontally
function dragObjectProps(e) { function dragObjectProps(e) {
if (e.which == 3) { if (e.which == 3 || modalOpen()) {
return false; return false;
} }
var drag = $(this).parent(); var drag = $(this).parent();
@@ -5293,13 +5372,17 @@ function resetHeight() {
'calc(100% - ' + (top + 97) + 'px)' 'calc(100% - ' + (top + 97) + 'px)'
); );
$('#properties').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); $('#timeline-handle').css('bottom', top + 95);
resizeCanvas(); resizeCanvas();
} }
// Dragging timeline vertically // Dragging timeline vertically
function dragTimeline(e) { function dragTimeline(e) {
if (e.which == 3) { if (e.which == 3 || modalOpen()) {
return false; return false;
} }
// Suppress text selection for the duration of the drag only - leaving these // Suppress text selection for the duration of the drag only - leaving these
@@ -5325,6 +5408,208 @@ $(document).on('pointerdown', '#timeline-handle', dragTimeline);
oldtimelinepos = $(window).height() - 92 - $('#timearea').height(); 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) // Sync scrolling (vertical)
function syncScroll(el1, el2) { function syncScroll(el1, el2) {
var $el1 = $(el1); var $el1 = $(el1);
+15 -6
View File
@@ -570,7 +570,7 @@ function updatePanelValues() {
o_slider.setValue(object.get('opacity') * 100); o_slider.setValue(object.get('opacity') * 100);
if (object.get('type') == 'rect') { if (object.get('type') == 'rect') {
$('#object-corners input').val( $('#object-corners input').val(
parseFloat(object.get('rx').toFixed(2)) parseFloat(getCornerRadius(object).toFixed(2))
); );
colormode = 'fill'; colormode = 'fill';
o_fill.setColor(object.get('fill')); o_fill.setColor(object.get('fill'));
@@ -1219,12 +1219,13 @@ $(document).on(
// Switch tool // Switch tool
function switchTool(e) { function switchTool(e) {
$('#browser').removeClass('collapsed');
$('#canvas-area').removeClass('canvas-full');
if ($(this).attr('id') == 'more-tool') { if ($(this).attr('id') == 'more-tool') {
showMore(); showMore();
return false; return false;
} }
$('#browser').removeClass('collapsed');
$('#behind-browser').removeClass('collapsed');
applyPanelWidths();
resizeCanvas(); resizeCanvas();
var act = $('.tool-active'); var act = $('.tool-active');
if (act.attr('id') == 'image-tool') { if (act.attr('id') == 'image-tool') {
@@ -1292,6 +1293,10 @@ function dragObject(e) {
if (e.which == 3) { if (e.which == 3) {
return false; return false;
} }
// Measure before the clone leaves the panel - panel items are sized in
// percentages of a resizable panel, so a clone parented to <body> would
// report the body width instead.
var sourcewidth = $(this).width();
var drag = $(this).clone(); var drag = $(this).clone();
drag.css({ drag.css({
background: 'transparent', background: 'transparent',
@@ -1304,7 +1309,7 @@ function dragObject(e) {
zIndex: 9999999, zIndex: 9999999,
left: $(this).offset().left, left: $(this).offset().left,
top: $(this).offset().top, top: $(this).offset().top,
width: canvas.getZoom() * drag.width(), width: canvas.getZoom() * sourcewidth,
pointerEvents: 'none', pointerEvents: 'none',
opacity: 0, opacity: 0,
}); });
@@ -1619,10 +1624,13 @@ $(document).on(
// Collapse library // Collapse library
function collapsePanel() { function collapsePanel() {
var act = $('.tool-active');
if (act.length > 0) {
// Remembered so the handle button can reopen the same tab
lasttool = act.attr('id');
}
$('#browser').addClass('collapsed'); $('#browser').addClass('collapsed');
$('#behind-browser').addClass('collapsed'); $('#behind-browser').addClass('collapsed');
$('#canvas-area').addClass('canvas-full');
var act = $('.tool-active');
if (act.attr('id') == 'image-tool') { if (act.attr('id') == 'image-tool') {
act.find('img').attr('src', 'assets/image.svg'); act.find('img').attr('src', 'assets/image.svg');
} else if (act.attr('id') == 'text-tool') { } else if (act.attr('id') == 'text-tool') {
@@ -1637,6 +1645,7 @@ function collapsePanel() {
act.find('img').attr('src', 'assets/uploads.svg'); act.find('img').attr('src', 'assets/uploads.svg');
} }
$('.tool-active').removeClass('tool-active'); $('.tool-active').removeClass('tool-active');
applyPanelWidths();
resizeCanvas(); resizeCanvas();
} }
$(document).on('click', '#collapse', collapsePanel); $(document).on('click', '#collapse', collapsePanel);
+141 -24
View File
@@ -7,6 +7,14 @@
--input-color: #22233e; --input-color: #22233e;
--accent-color: #166ef1; --accent-color: #166ef1;
--button-hover: #262746; --button-hover: #262746;
/* Panel geometry. Every rule that used to hardcode 76/299/300/375 derives
from these so the resize handles only have to move one number each.
--layers-w tracks --rail-w + --browser-w while the library panel is
visible, but stays put when it is hidden so the layer names keep room. */
--rail-w: 76px;
--browser-w: 299px;
--props-w: 300px;
--layers-w: 375px;
} }
/* /*
:root { :root {
@@ -41,7 +49,7 @@ body {
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
background: var(--panel-back); background: var(--panel-back);
width: 76px; width: var(--rail-w);
border-right: 1px solid var(--panel-stroke); border-right: 1px solid var(--panel-stroke);
box-sizing: border-box; box-sizing: border-box;
} }
@@ -244,7 +252,7 @@ body {
#toolbar { #toolbar {
position: absolute; position: absolute;
height: calc(100% - 340px); height: calc(100% - 340px);
width: 76px; width: var(--rail-w);
background-color: var(--panel-back); background-color: var(--panel-back);
border-right: 1px solid var(--panel-stroke); border-right: 1px solid var(--panel-stroke);
left: 0px; left: 0px;
@@ -308,8 +316,8 @@ body {
#behind-browser { #behind-browser {
position: absolute; position: absolute;
height: 100%; height: 100%;
width: 299px; width: var(--browser-w);
left: 76px; left: var(--rail-w);
background: var(--panel-back); background: var(--panel-back);
border-right: 1px solid var(--panel-stroke); border-right: 1px solid var(--panel-stroke);
z-index: 1; z-index: 1;
@@ -318,10 +326,10 @@ body {
#browser { #browser {
position: absolute; position: absolute;
height: calc(100% - 450px); height: calc(100% - 450px);
width: 299px; width: var(--browser-w);
background-color: var(--panel-back); background-color: var(--panel-back);
border-right: 1px solid var(--panel-stroke); border-right: 1px solid var(--panel-stroke);
left: 76px; left: var(--rail-w);
top: 110px; top: 110px;
box-sizing: border-box; box-sizing: border-box;
z-index: 999999; z-index: 999999;
@@ -333,7 +341,7 @@ body {
display: none !important; display: none !important;
} }
#browser-container { #browser-container {
width: 260px; width: calc(var(--browser-w) - 39px);
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
height: 100%; height: 100%;
@@ -359,7 +367,7 @@ body {
} }
.image-grid-item, .image-grid-item,
.video-grid-item { .video-grid-item {
width: 120px; width: 100%;
margin-bottom: 15px; margin-bottom: 15px;
position: relative; position: relative;
} }
@@ -388,7 +396,7 @@ body {
text-align: right; text-align: right;
text-decoration: none; text-decoration: none;
text-shadow: 0px 1px 5px #000000; text-shadow: 0px 1px 5px #000000;
width: 110px; width: calc(100% - 10px);
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -436,7 +444,9 @@ body {
grid-gap: 15px; grid-gap: 15px;
width: 100%; width: 100%;
grid-auto-columns: auto; grid-auto-columns: auto;
grid-template-columns: 52px 52px 52px 52px; /* Column count follows the panel width so the grid reflows when the library
is resized instead of overflowing a fixed four-column track. */
grid-template-columns: repeat(auto-fill, minmax(52px, 1fr));
overflow: hidden; overflow: hidden;
} }
.scroll-row:before { .scroll-row:before {
@@ -461,7 +471,7 @@ body {
#search-fixed { #search-fixed {
position: fixed; position: fixed;
top: 0px; top: 0px;
width: 279px; width: calc(var(--browser-w) - 20px);
padding-left: 19px; padding-left: 19px;
margin-left: -19px; margin-left: -19px;
z-index: 999999; z-index: 999999;
@@ -776,12 +786,13 @@ body {
position: absolute; position: absolute;
right: 0px; right: 0px;
top: 0px; top: 0px;
width: 300px; width: var(--props-w);
height: calc(100% - 340px); height: calc(100% - 340px);
background-color: var(--panel-back); background-color: var(--panel-back);
border-left: 1px solid var(--panel-stroke); border-left: 1px solid var(--panel-stroke);
z-index: 999; z-index: 999;
overflow-y: overlay; overflow-y: overlay;
overflow-x: hidden;
} }
#properties-overlay { #properties-overlay {
width: 100%; width: 100%;
@@ -830,9 +841,12 @@ hr {
background-color: var(--panel-stroke); background-color: var(--panel-stroke);
margin-bottom: 20px; margin-bottom: 20px;
} }
#properties hr {
width: calc(var(--props-w) - 40px);
}
/* Property sections */ /* Property sections */
.panel-section { .panel-section {
width: 260px; width: calc(var(--props-w) - 40px);
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
} }
@@ -869,8 +883,17 @@ th {
width: 189px; width: 189px;
display: flex; display: flex;
align-items: center; align-items: center;
/* Keeps the inputs against the right edge once the column can be wider
than its contents */
justify-content: flex-end;
margin-left: auto; margin-left: auto;
} }
/* Inside the properties panel the value column follows the panel width, so
dropdowns and sliders grow when the panel is widened. 111px is the label
column plus the panel gutters. The filters popup keeps the fixed width. */
#properties .value-col {
width: calc(var(--props-w) - 111px);
}
/* Dropdows */ /* Dropdows */
.nice-select, .nice-select,
.list, .list,
@@ -1550,16 +1573,12 @@ label span {
#canvas-area { #canvas-area {
position: absolute; position: absolute;
top: 0px; top: 0px;
left: 375px; left: calc(var(--rail-w) + var(--browser-w));
height: calc(100% - 342px); height: calc(100% - 342px);
width: calc(100% - 675px); width: calc(100% - var(--rail-w) - var(--browser-w) - var(--props-w));
box-sizing: border-box; box-sizing: border-box;
overflow: hidden; overflow: hidden;
} }
.canvas-full {
left: 76px !important;
width: calc(100% - 376px) !important;
}
.canvas-container { .canvas-container {
left: 0px; left: 0px;
top: 0px; top: 0px;
@@ -1614,7 +1633,7 @@ canvas {
#layer-list { #layer-list {
position: absolute; position: absolute;
height: 245px; height: 245px;
width: 375px; width: var(--layers-w);
left: 0px; left: 0px;
bottom: 60px; bottom: 60px;
background-color: var(--panel-back); background-color: var(--panel-back);
@@ -1816,7 +1835,7 @@ layer:nth-child(even) .properties {
color: var(--secondary-text-color); color: var(--secondary-text-color);
line-height: 40px; line-height: 40px;
height: 40px; height: 40px;
width: 375px; width: var(--layers-w);
position: fixed; position: fixed;
margin-top: -35px; margin-top: -35px;
background-color: var(--panel-back); background-color: var(--panel-back);
@@ -1840,10 +1859,10 @@ layer:nth-child(even) .properties {
#timearea { #timearea {
position: absolute; position: absolute;
bottom: 60px; bottom: 60px;
left: 375px; left: var(--layers-w);
height: 245px; height: 245px;
background-color: var(--main-back); background-color: var(--main-back);
width: calc(100% - 375px); width: calc(100% - var(--layers-w));
border-top: 1px solid var(--panel-stroke); border-top: 1px solid var(--panel-stroke);
z-index: 999; z-index: 999;
} }
@@ -1858,10 +1877,108 @@ layer:nth-child(even) .properties {
cursor: ns-resize; cursor: ns-resize;
background-color: var(--panel-stroke); background-color: var(--panel-stroke);
} }
/* Side panel resize handles. Each one straddles the border between a side
panel and the canvas, and carries the hide/show button for that panel.
Their height is kept in sync with the panels by resetHeight(). */
.panel-handle {
position: absolute;
top: 0px;
height: calc(100% - 340px);
width: 7px;
z-index: 9999999;
box-sizing: border-box;
touch-action: none;
-webkit-user-drag: none;
-webkit-user-select: none;
user-select: none;
}
#browser-handle {
left: calc(var(--rail-w) + var(--browser-w) - 3px);
}
#properties-handle {
right: calc(var(--props-w) - 3px);
}
#properties-handle.panel-hidden {
right: 0px;
}
.panel-handle:not(.panel-hidden):hover,
.panel-handle.handle-dragging {
cursor: ew-resize;
}
.panel-handle:before {
content: "";
position: absolute;
left: 2px;
top: 0px;
width: 2px;
height: 100%;
background-color: var(--accent-color);
opacity: 0;
}
.panel-handle:not(.panel-hidden):hover:before,
.panel-handle.handle-dragging:before {
opacity: 1;
}
.panel-toggle {
position: absolute;
top: 50%;
margin-top: -18px;
width: 18px;
height: 36px;
background-color: var(--input-color);
border: 1px solid var(--panel-stroke);
box-sizing: border-box;
/* Always on screen, dimmed. Revealing it on hover meant having to find the
3px resize line first, then travel onto the button. */
opacity: 0.4;
}
.panel-toggle:hover {
cursor: pointer;
background-color: var(--button-hover);
}
.panel-toggle:hover,
.panel-handle:hover .panel-toggle,
.panel-handle.handle-dragging .panel-toggle,
.panel-handle.panel-hidden .panel-toggle {
opacity: 1;
}
/* Chevron, pointing at the edge the panel would collapse towards */
.panel-toggle:after {
content: "";
position: absolute;
width: 5px;
height: 5px;
top: 50%;
left: 50%;
border-left: 1.5px solid var(--secondary-text-color);
border-bottom: 1.5px solid var(--secondary-text-color);
}
#browser-handle .panel-toggle {
left: 5px;
border-radius: 0px 4px 4px 0px;
}
#properties-handle .panel-toggle {
right: 5px;
border-radius: 4px 0px 0px 4px;
}
#properties-handle.panel-hidden .panel-toggle {
right: 4px;
}
#browser-handle .panel-toggle:after,
#properties-handle.panel-hidden .panel-toggle:after {
margin: -4px 0px 0px -2px;
transform: rotate(45deg);
}
#properties-handle .panel-toggle:after,
#browser-handle.panel-hidden .panel-toggle:after {
margin: -4px 0px 0px -5px;
transform: rotate(-135deg);
}
#seekarea { #seekarea {
height: 100%; height: 100%;
z-index: 99999; z-index: 99999;
width: calc(100% - 375px); /* Fixed positioning - the percentage is the viewport, not #timearea. */
width: calc(100% - var(--layers-w));
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
position: fixed; position: fixed;