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
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:
@@ -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.
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
+359
-74
@@ -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,
|
||||||
.setCoords();
|
top: centerY + center.y,
|
||||||
canvas.renderAll();
|
})
|
||||||
obj.set({
|
.setCoords();
|
||||||
left: cleft + (obj.get('width') * obj.get('scaleX')) / 2,
|
canvas.renderAll();
|
||||||
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
@@ -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
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user