From 859371ea017d35542d7dc94fec7cc4dc40e77ca3 Mon Sep 17 00:00:00 2001 From: funny bot Date: Thu, 23 Jul 2026 19:26:19 -0700 Subject: [PATCH 1/8] Clean up selection widget code --- .../view/paper-ext/Paper.SelectionWidget.js | 162 ++++++++---------- 1 file changed, 68 insertions(+), 94 deletions(-) diff --git a/engine/src/view/paper-ext/Paper.SelectionWidget.js b/engine/src/view/paper-ext/Paper.SelectionWidget.js index 7fa4866db..9e5c053ac 100644 --- a/engine/src/view/paper-ext/Paper.SelectionWidget.js +++ b/engine/src/view/paper-ext/Paper.SelectionWidget.js @@ -205,7 +205,7 @@ class SelectionWidget { } set currentTransformation(currentTransformation) { - if(['translate', 'scale', 'rotate', 'gradient-stop', 'gradient-point', 'gradient-none'].indexOf(currentTransformation) === -1) { + if(['translate', 'scale-edge', 'scale-corner', 'rotate', 'gradient-stop', 'gradient-point', 'gradient-none'].indexOf(currentTransformation) === -1) { console.error('Paper.SelectionWidget: Invalid transformation type: ' + currentTransformation); currentTransformation = null; } else { @@ -268,167 +268,141 @@ class SelectionWidget { if (item.data.handleType === 'rotation') { this.currentTransformation = 'rotate'; } else if (item.data.handleType === 'scale') { - this.currentTransformation = 'scale'; + if (item.data.handleEdge.includes('Center')) { + this.currentTransformation = 'scale-edge'; + } + else { + this.currentTransformation = 'scale-corner'; + } } else { this.currentTransformation = 'translate'; } - this._ghost.data.initialPosition = this._ghost.position; + this._initialPoint = e.point; + this._truePivot = this.pivot; + this._ghost.data.offset = new paper.Point(0, 0); this._ghost.data.scale = new paper.Point(1, 1); + this._ghost.data.transform = new paper.Matrix(); } /** * */ updateTransformation (item, e) { - if(this.currentTransformation.substring(0,8) === 'gradient') { + if(this.currentTransformation && this.currentTransformation.substring(0,8) === 'gradient') { return this.updateGradientTransformation(item, e); } - if (!this.mod || !this.mod.initiated) { - this.mod = { - initiated: true - } - - this.mod.onePoint = new paper.Point(1, 1); - this.mod.initialPoint = e.point; - - this.mod.truePivot = this.pivot; - - if (this.currentTransformation === 'translate') { - this.mod.action = 'translate'; - this.mod.initialPosition = this._ghost.position; - } - else if (this.currentTransformation === 'rotate') { - this.mod.action = 'rotate'; - this.mod.rotateDelta = 0; - this.mod.initialAngle = this.mod.initialPoint.subtract(this.pivot).angle; - this.mod.initialBoxRotation = this.boxRotation || 0; - } else if (item.data.handleEdge.includes('Center')) { - this.mod.action = 'move-edge'; - this.mod.topLeft = item.data.handleEdge === 'topCenter' || item.data.handleEdge === 'leftCenter'; - this.mod.vertical = item.data.handleEdge === 'topCenter' || item.data.handleEdge === 'bottomCenter'; - - this.mod.transformMatrix = new paper.Matrix(); - } else { - this.mod.action = 'move-corner'; - this.mod.scaleFactor = this.mod.onePoint; - } - } - - this.mod.modifiers = { - skew: e.modifiers.command, // Skew when Ctrl/Cmd pressed - center: !e.modifiers.alt, // Always scale from center unless Alt pressed + var modifiers = { + skew: e.modifiers.command, // Skew when Ctrl/Cmd pressed + center: !e.modifiers.alt, // Always scale from center unless Alt pressed freescale: !e.modifiers.shift // Never retain proportions unless Shift pressed - } + }; + var topLeft = item.data.handleEdge === 'topCenter' || item.data.handleEdge === 'leftCenter'; + var vertical = item.data.handleEdge === 'topCenter' || item.data.handleEdge === 'bottomCenter'; - if (this.mod.action === 'translate') { - var initialDelta = e.point.subtract(this.mod.initialPoint); - if (!this.mod.modifiers.freescale) { + this._ghost.matrix.reset(); + + if (this.currentTransformation === 'translate') { + var initialDelta = e.point.subtract(this._initialPoint); + if (!modifiers.freescale) { var angle = initialDelta.angle; - angle = Math.round(Math.round(angle / 45) * 45) * Math.PI / 180; + angle = Math.round(angle / 45) * 45 * Math.PI / 180; var angleVector = new paper.Point(Math.cos(angle), Math.sin(angle)); initialDelta = initialDelta.project(angleVector); } - this.mod.offset = initialDelta; - this._ghost.position = this.mod.initialPosition.add(initialDelta); + this._ghost.data.offset = initialDelta; + this._ghost.translate(initialDelta); } - else if (this.mod.action === 'rotate') { - this._ghost.rotate(-this.mod.rotateDelta, this.pivot); - - var rotateDelta = e.point.subtract(this.pivot).angle - this.mod.initialAngle; - if (!this.mod.modifiers.freescale) { - rotateDelta = Math.round(Math.round(rotateDelta / 45) * 45); + else if (this.currentTransformation === 'rotate') { + var rotateDelta = e.point.subtract(this.pivot).angle - this._initialPoint.subtract(this.pivot).angle; + if (!modifiers.freescale) { + rotateDelta = Math.round(rotateDelta / 45) * 45; } - this.mod.rotateDelta = rotateDelta; - this.boxRotation = this.mod.initialBoxRotation + rotateDelta; - - this._ghost.rotate(this.mod.rotateDelta, this.pivot); - } else if (this.mod.action === 'move-corner') { + this._ghost.rotate(rotateDelta, this.pivot); + } else if (this.currentTransformation === 'scale-corner') { this._ghost.rotate(-this.boxRotation, this.pivot); - this._ghost.scale(this.mod.onePoint.divide(this.mod.scaleFactor), this.mod.truePivot); - if (this.mod.modifiers.center) { - this.mod.truePivot = this.pivot; + if (modifiers.center) { + this._truePivot = this.pivot; } else { let bounds = this._ghost.bounds; switch (item.data.handleEdge) { case 'topRight': - this.mod.truePivot = bounds.bottomLeft; + this._truePivot = bounds.bottomLeft; break; case 'topLeft': - this.mod.truePivot = bounds.bottomRight; + this._truePivot = bounds.bottomRight; break; case 'bottomRight': - this.mod.truePivot = bounds.topLeft; + this._truePivot = bounds.topLeft; break; case 'bottomLeft': - this.mod.truePivot = bounds.topRight; + this._truePivot = bounds.topRight; break; } } - var currentPointRelative = e.point.rotate(-this.boxRotation, this.pivot).subtract(this.mod.truePivot); - var initialPointRelative = this.mod.initialPoint.rotate(-this.boxRotation, this.pivot).subtract(this.mod.truePivot); + var currentPointRelative = e.point.rotate(-this.boxRotation, this.pivot).subtract(this._truePivot); + var initialPointRelative = this._initialPoint.rotate(-this.boxRotation, this.pivot).subtract(this._truePivot); var scaleFactor = currentPointRelative.divide(initialPointRelative); - if (!this.mod.modifiers.freescale) { + if (!modifiers.freescale) { if (Math.abs(scaleFactor.x) < Math.abs(scaleFactor.y)) { scaleFactor.x = Math.sign(scaleFactor.x) * Math.abs(scaleFactor.y); } else { scaleFactor.y = Math.sign(scaleFactor.y) * Math.abs(scaleFactor.x); } } - this.mod.scaleFactor = scaleFactor; + this._ghost.data.scale = scaleFactor; - this._ghost.scale(this.mod.scaleFactor, this.mod.truePivot); + this._ghost.scale(scaleFactor, this._truePivot); this._ghost.rotate(this.boxRotation, this.pivot); } else { this._ghost.rotate(-this.boxRotation, this.pivot); - this._ghost.translate(this.mod.truePivot.multiply(-1)).transform(this.mod.transformMatrix.inverted()).translate(this.mod.truePivot); - if (this.mod.modifiers.center) { - this.mod.truePivot = this.pivot; + if (modifiers.center) { + this._truePivot = this.pivot; } else { - if (this.mod.topLeft) { - this.mod.truePivot = this._ghost.bounds.bottomRight; + if (topLeft) { + this._truePivot = this._ghost.bounds.bottomRight; } else { - this.mod.truePivot = this._ghost.bounds.topLeft; + this._truePivot = this._ghost.bounds.topLeft; } } - this.mod.transformMatrix.reset(); + this._ghost.data.transform.reset(); var currentPointRelative = e.point.rotate(-this.boxRotation, this.pivot); - var initialPointRelative = this.mod.initialPoint.rotate(-this.boxRotation, this.pivot); + var initialPointRelative = this._initialPoint.rotate(-this.boxRotation, this.pivot); - if (!this.mod.modifiers.skew || (this.mod.modifiers.skew && e.modifiers.shift)) { - var scaleFactor = currentPointRelative.subtract(this.mod.truePivot).divide(initialPointRelative.subtract(this.mod.truePivot)); - if (this.mod.vertical) { + if (!modifiers.skew || (modifiers.skew && e.modifiers.shift)) { + var scaleFactor = currentPointRelative.subtract(this._truePivot).divide(initialPointRelative.subtract(this._truePivot)); + if (vertical) { scaleFactor.x = 1; } else { scaleFactor.y = 1; } - this.mod.transformMatrix.scale(scaleFactor) + this._ghost.data.transform.scale(scaleFactor) } - if (this.mod.modifiers.skew) { + if (modifiers.skew) { var shearFactor = currentPointRelative.subtract(initialPointRelative).divide(this._ghost.bounds.height, this._ghost.bounds.width); - if (this.mod.vertical) { + if (vertical) { shearFactor.y = 0; } else { shearFactor.x = 0; } - if (this.mod.modifiers.center) { + if (modifiers.center) { shearFactor = shearFactor.multiply(2); } - if (this.mod.topLeft) { + if (topLeft) { shearFactor = shearFactor.multiply(-1); - }; + } - this.mod.transformMatrix.shear(shearFactor.transform(this.mod.transformMatrix.inverted())); + this._ghost.data.transform.shear(shearFactor); } - this._ghost.translate(this.mod.truePivot.multiply(-1)).transform(this.mod.transformMatrix).translate(this.mod.truePivot); + this._ghost.translate(this._truePivot.multiply(-1)).transform(this._ghost.data.transform).translate(this._truePivot); this._ghost.rotate(this.boxRotation, this.pivot); } } @@ -444,18 +418,18 @@ class SelectionWidget { this._ghost.remove(); - if (this.mod.action === 'translate') { - this.translateSelection(this.mod.offset); - } else if (this.mod.action === 'rotate') { + if (this.currentTransformation === 'translate') { + this.translateSelection(this._ghost.data.offset); + } else if (this.currentTransformation === 'rotate') { + this.boxRotation += this._ghost.rotation; this.rotateSelection(this._ghost.rotation); - } else if (this.mod.action === 'move-corner') { - this.scaleSelection(this.mod.scaleFactor, this.mod.truePivot); + } else if (this.currentTransformation === 'scale-corner') { + this.scaleSelection(this._ghost.data.scale, this._truePivot); } else { - this.transformSelection(this.mod.transformMatrix, this.mod.truePivot); + this.transformSelection(this._ghost.data.transform, this._truePivot); } this._currentTransformation = null; - this.mod.initiated = false; } /** From bc752eed4e7d7ce5a0029128e07057f9d0ee57d3 Mon Sep 17 00:00:00 2001 From: funny bot Date: Thu, 23 Jul 2026 19:29:56 -0700 Subject: [PATCH 2/8] Add Alt to rotate from corner --- .../view/paper-ext/Paper.SelectionWidget.js | 116 ++++++++---------- 1 file changed, 54 insertions(+), 62 deletions(-) diff --git a/engine/src/view/paper-ext/Paper.SelectionWidget.js b/engine/src/view/paper-ext/Paper.SelectionWidget.js index 9e5c053ac..a95d56546 100644 --- a/engine/src/view/paper-ext/Paper.SelectionWidget.js +++ b/engine/src/view/paper-ext/Paper.SelectionWidget.js @@ -302,8 +302,34 @@ class SelectionWidget { var vertical = item.data.handleEdge === 'topCenter' || item.data.handleEdge === 'bottomCenter'; this._ghost.matrix.reset(); + this._ghost.rotate(-this.boxRotation, this.pivot); + if (modifiers.center) { + this._truePivot = this.pivot; + } else if (this.currentTransformation === 'scale-edge') { + this._truePivot = topLeft ? this._ghost.bounds.bottomRight : this._ghost.bounds.topLeft; + } else { + let bounds = this._ghost.bounds; + switch (item.data.handleEdge) { + case 'topRight': + this._truePivot = bounds.bottomLeft; + break; + case 'topLeft': + this._truePivot = bounds.bottomRight; + break; + case 'bottomRight': + this._truePivot = bounds.topLeft; + break; + case 'bottomLeft': + this._truePivot = bounds.topRight; + break; + } + } + let unrotatedPivot = this._truePivot; + this._truePivot = this._truePivot.rotate(this.boxRotation, this.pivot); + if (this.currentTransformation === 'translate') { + this._ghost.matrix.reset(); var initialDelta = e.point.subtract(this._initialPoint); if (!modifiers.freescale) { var angle = initialDelta.angle; @@ -313,38 +339,16 @@ class SelectionWidget { } this._ghost.data.offset = initialDelta; this._ghost.translate(initialDelta); - } - else if (this.currentTransformation === 'rotate') { - var rotateDelta = e.point.subtract(this.pivot).angle - this._initialPoint.subtract(this.pivot).angle; + } else if (this.currentTransformation === 'rotate') { + this._ghost.matrix.reset(); + var rotateDelta = e.point.subtract(this._truePivot).angle - this._initialPoint.subtract(this._truePivot).angle; if (!modifiers.freescale) { rotateDelta = Math.round(rotateDelta / 45) * 45; } - this._ghost.rotate(rotateDelta, this.pivot); + this._ghost.rotate(rotateDelta, this._truePivot); } else if (this.currentTransformation === 'scale-corner') { - this._ghost.rotate(-this.boxRotation, this.pivot); - - if (modifiers.center) { - this._truePivot = this.pivot; - } else { - let bounds = this._ghost.bounds; - switch (item.data.handleEdge) { - case 'topRight': - this._truePivot = bounds.bottomLeft; - break; - case 'topLeft': - this._truePivot = bounds.bottomRight; - break; - case 'bottomRight': - this._truePivot = bounds.topLeft; - break; - case 'bottomLeft': - this._truePivot = bounds.topRight; - break; - } - } - - var currentPointRelative = e.point.rotate(-this.boxRotation, this.pivot).subtract(this._truePivot); - var initialPointRelative = this._initialPoint.rotate(-this.boxRotation, this.pivot).subtract(this._truePivot); + var currentPointRelative = e.point.rotate(-this.boxRotation, this.pivot).subtract(unrotatedPivot); + var initialPointRelative = this._initialPoint.rotate(-this.boxRotation, this.pivot).subtract(unrotatedPivot); var scaleFactor = currentPointRelative.divide(initialPointRelative); if (!modifiers.freescale) { if (Math.abs(scaleFactor.x) < Math.abs(scaleFactor.y)) { @@ -355,54 +359,38 @@ class SelectionWidget { } this._ghost.data.scale = scaleFactor; - this._ghost.scale(scaleFactor, this._truePivot); + this._ghost.scale(scaleFactor, unrotatedPivot); this._ghost.rotate(this.boxRotation, this.pivot); } else { - this._ghost.rotate(-this.boxRotation, this.pivot); - - if (modifiers.center) { - this._truePivot = this.pivot; - } else { - if (topLeft) { - this._truePivot = this._ghost.bounds.bottomRight; - } else { - this._truePivot = this._ghost.bounds.topLeft; - } - } - - this._ghost.data.transform.reset(); - var currentPointRelative = e.point.rotate(-this.boxRotation, this.pivot); var initialPointRelative = this._initialPoint.rotate(-this.boxRotation, this.pivot); + this._ghost.data.transform.reset(); if (!modifiers.skew || (modifiers.skew && e.modifiers.shift)) { - var scaleFactor = currentPointRelative.subtract(this._truePivot).divide(initialPointRelative.subtract(this._truePivot)); - if (vertical) { + var scaleFactor = currentPointRelative.subtract(unrotatedPivot).divide(initialPointRelative.subtract(unrotatedPivot)); + if (vertical) scaleFactor.x = 1; - } else { + else scaleFactor.y = 1; - } this._ghost.data.transform.scale(scaleFactor) } if (modifiers.skew) { var shearFactor = currentPointRelative.subtract(initialPointRelative).divide(this._ghost.bounds.height, this._ghost.bounds.width); - if (vertical) { + if (vertical) shearFactor.y = 0; - } else { + else shearFactor.x = 0; - } - if (modifiers.center) { + + if (modifiers.center) shearFactor = shearFactor.multiply(2); - } - if (topLeft) { + if (topLeft) shearFactor = shearFactor.multiply(-1); - } this._ghost.data.transform.shear(shearFactor); } - this._ghost.translate(this._truePivot.multiply(-1)).transform(this._ghost.data.transform).translate(this._truePivot); + this._ghost.translate(unrotatedPivot.multiply(-1)).transform(this._ghost.data.transform).translate(unrotatedPivot); this._ghost.rotate(this.boxRotation, this.pivot); } } @@ -422,7 +410,7 @@ class SelectionWidget { this.translateSelection(this._ghost.data.offset); } else if (this.currentTransformation === 'rotate') { this.boxRotation += this._ghost.rotation; - this.rotateSelection(this._ghost.rotation); + this.rotateSelection(this._ghost.rotation, this._truePivot); } else if (this.currentTransformation === 'scale-corner') { this.scaleSelection(this._ghost.data.scale, this._truePivot); } else { @@ -445,23 +433,26 @@ class SelectionWidget { /** * */ - rotateSelection(angle) { + rotateSelection(angle, pivot = this.pivot) { this._itemsInSelection.forEach(item => { - item.rotate(angle, this.pivot); + item.rotate(angle, pivot); }); + + this.pivot = this.pivot.rotate(angle, pivot); } /** * */ scaleSelection(scale, pivot = this.pivot) { + let unrotatedPivot = pivot.rotate(-this.boxRotation, this.pivot); this._itemsInSelection.forEach(item => { item.rotate(-this.boxRotation, this.pivot); - item.scale(scale, pivot); + item.scale(scale, unrotatedPivot); item.rotate(this.boxRotation, this.pivot); }); - var newPivot = pivot.add(this.pivot.subtract(pivot).multiply(scale)); + var newPivot = unrotatedPivot.add(this.pivot.subtract(unrotatedPivot).multiply(scale)); this.pivot = newPivot.rotate(this.boxRotation, this.pivot); } @@ -469,15 +460,16 @@ class SelectionWidget { * */ transformSelection(matrix, pivot = this.pivot) { + let unrotatedPivot = pivot.rotate(-this.boxRotation, this.pivot); this._itemsInSelection.forEach(item => { item.rotate(-this.boxRotation, this.pivot); - item.translate(pivot.multiply(-1)).transform(matrix).translate(pivot); + item.translate(unrotatedPivot.multiply(-1)).transform(matrix).translate(unrotatedPivot); item.rotate(this.boxRotation, this.pivot); }); // Note that the GUI won't show this pivot as the center because it doesn't account for skew. // The pivot point after the skew will look a bit off. - var newPivot = pivot.add(this.pivot.subtract(pivot).transform(matrix)); + var newPivot = unrotatedPivot.add(this.pivot.subtract(unrotatedPivot).transform(matrix)); this.pivot = newPivot.rotate(this.boxRotation, this.pivot); } From 6ce01f44b18bb8c5d1ecbc72a23b6c78863d76a1 Mon Sep 17 00:00:00 2001 From: funny bot Date: Thu, 23 Jul 2026 20:22:02 -0700 Subject: [PATCH 3/8] Add moveable selection pivot point --- .../view/paper-ext/Paper.SelectionWidget.js | 44 ++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/engine/src/view/paper-ext/Paper.SelectionWidget.js b/engine/src/view/paper-ext/Paper.SelectionWidget.js index a95d56546..63bf6aa50 100644 --- a/engine/src/view/paper-ext/Paper.SelectionWidget.js +++ b/engine/src/view/paper-ext/Paper.SelectionWidget.js @@ -205,7 +205,8 @@ class SelectionWidget { } set currentTransformation(currentTransformation) { - if(['translate', 'scale-edge', 'scale-corner', 'rotate', 'gradient-stop', 'gradient-point', 'gradient-none'].indexOf(currentTransformation) === -1) { + if(['translate', 'scale-edge', 'scale-corner', 'rotate', 'move-pivot', + 'gradient-stop', 'gradient-point', 'gradient-none'].indexOf(currentTransformation) === -1) { console.error('Paper.SelectionWidget: Invalid transformation type: ' + currentTransformation); currentTransformation = null; } else { @@ -265,7 +266,11 @@ class SelectionWidget { this._ghost = this._buildGhost(); this._layer.addChild(this._ghost); - if (item.data.handleType === 'rotation') { + if (item.data.handleType === 'pivot') { + this.currentTransformation = 'move-pivot'; + this._newPivot = this.pivot; + this._ghost.remove(); + } else if (item.data.handleType === 'rotation') { this.currentTransformation = 'rotate'; } else if (item.data.handleType === 'scale') { if (item.data.handleEdge.includes('Center')) { @@ -294,9 +299,9 @@ class SelectionWidget { } var modifiers = { - skew: e.modifiers.command, // Skew when Ctrl/Cmd pressed - center: !e.modifiers.alt, // Always scale from center unless Alt pressed - freescale: !e.modifiers.shift // Never retain proportions unless Shift pressed + skew: e.modifiers.command, // Skew when Ctrl/Cmd pressed + center: !e.modifiers.alt, // Always scale from center unless Alt pressed + constrain: e.modifiers.shift // Never retain proportions unless Shift pressed }; var topLeft = item.data.handleEdge === 'topCenter' || item.data.handleEdge === 'leftCenter'; var vertical = item.data.handleEdge === 'topCenter' || item.data.handleEdge === 'bottomCenter'; @@ -327,22 +332,28 @@ class SelectionWidget { let unrotatedPivot = this._truePivot; this._truePivot = this._truePivot.rotate(this.boxRotation, this.pivot); - - if (this.currentTransformation === 'translate') { + if (this.currentTransformation === 'move-pivot') { + item.matrix.reset(); + var initialDelta = e.point.subtract(this._initialPoint); + if (modifiers.constrain) { + var direction = new paper.Point({ length: 1, angle: Math.round(initialDelta.angle / 45) * 45 }); + initialDelta = initialDelta.project(direction); + } + item.translate(initialDelta); + this._newPivot = item.position; + } else if (this.currentTransformation === 'translate') { this._ghost.matrix.reset(); var initialDelta = e.point.subtract(this._initialPoint); - if (!modifiers.freescale) { - var angle = initialDelta.angle; - angle = Math.round(angle / 45) * 45 * Math.PI / 180; - var angleVector = new paper.Point(Math.cos(angle), Math.sin(angle)); - initialDelta = initialDelta.project(angleVector); + if (modifiers.constrain) { + var direction = new paper.Point({ length: 1, angle: Math.round(initialDelta.angle / 45) * 45 }); + initialDelta = initialDelta.project(direction); } this._ghost.data.offset = initialDelta; this._ghost.translate(initialDelta); } else if (this.currentTransformation === 'rotate') { this._ghost.matrix.reset(); var rotateDelta = e.point.subtract(this._truePivot).angle - this._initialPoint.subtract(this._truePivot).angle; - if (!modifiers.freescale) { + if (modifiers.constrain) { rotateDelta = Math.round(rotateDelta / 45) * 45; } this._ghost.rotate(rotateDelta, this._truePivot); @@ -350,7 +361,7 @@ class SelectionWidget { var currentPointRelative = e.point.rotate(-this.boxRotation, this.pivot).subtract(unrotatedPivot); var initialPointRelative = this._initialPoint.rotate(-this.boxRotation, this.pivot).subtract(unrotatedPivot); var scaleFactor = currentPointRelative.divide(initialPointRelative); - if (!modifiers.freescale) { + if (modifiers.constrain) { if (Math.abs(scaleFactor.x) < Math.abs(scaleFactor.y)) { scaleFactor.x = Math.sign(scaleFactor.x) * Math.abs(scaleFactor.y); } else { @@ -406,7 +417,9 @@ class SelectionWidget { this._ghost.remove(); - if (this.currentTransformation === 'translate') { + if (this.currentTransformation === 'move-pivot') { + if (this._newPivot) this.pivot = this._newPivot; + } else if (this.currentTransformation === 'translate') { this.translateSelection(this._ghost.data.offset); } else if (this.currentTransformation === 'rotate') { this.boxRotation += this._ghost.rotation; @@ -557,7 +570,6 @@ class SelectionWidget { fillColor: SelectionWidget.PIVOT_FILL_COLOR, strokeColor: SelectionWidget.PIVOT_STROKE_COLOR, }); - handle.locked = true; return handle; } From 260c05525532bddcd833f08b2c11ee037064a5cf Mon Sep 17 00:00:00 2001 From: funny bot Date: Thu, 23 Jul 2026 20:39:11 -0700 Subject: [PATCH 4/8] Add pivot property to clips --- engine/src/base/Clip.js | 15 +++++++++++++++ engine/src/view/View.Clip.js | 4 ++-- engine/src/view/View.Frame.js | 1 + .../src/view/paper-ext/Paper.SelectionWidget.js | 2 ++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/engine/src/base/Clip.js b/engine/src/base/Clip.js index 0df88a5cf..036c674bf 100644 --- a/engine/src/base/Clip.js +++ b/engine/src/base/Clip.js @@ -53,6 +53,7 @@ Wick.Clip = class extends Wick.Tickable { this._isSynced = false; this._transformation = args.transformation || new Wick.Transformation(); + this._pivot = args.pivot || [0,0]; this.cursor = 'default'; @@ -74,6 +75,7 @@ Wick.Clip = class extends Wick.Tickable { var data = super._serialize(args); data.transformation = this.transformation.values; + data.pivot = this._pivot; data.timeline = this._timeline; data.animationType = this._animationType; data.singleFrameNumber = this._singleFrameNumber; @@ -87,6 +89,7 @@ Wick.Clip = class extends Wick.Tickable { super._deserialize(data); this.transformation = new Wick.Transformation(data.transformation); + this.pivot = data.pivot; this._timeline = data.timeline; this._animationType = data.animationType || 'loop'; this._singleFrameNumber = data.singleFrameNumber || 1; @@ -549,6 +552,18 @@ Wick.Clip = class extends Wick.Tickable { } } + /** + * The pivot point of the clip. + * @type {Array} + */ + get pivot() { + return this._pivot; + } + + set pivot(pivot) { + this._pivot = pivot; + } + /** * Perform circular hit test with other clip. * @param {Wick.Clip} other - the clip to hit test with diff --git a/engine/src/view/View.Clip.js b/engine/src/view/View.Clip.js index d423ab48c..fac6a2238 100644 --- a/engine/src/view/View.Clip.js +++ b/engine/src/view/View.Clip.js @@ -172,7 +172,7 @@ Wick.View.Clip = class extends Wick.View { //this._radius = null; - this.group.pivot = new this.paper.Point(0,0); + this.group.pivot = this.model.pivot || new this.paper.Point(0,0); this.group.position.x = this.model.transformation.x; this.group.position.y = this.model.transformation.y; this.group.scaling.x = this.model.transformation.scaleX; @@ -206,7 +206,7 @@ Wick.View.Clip = class extends Wick.View { }); group.addChild(border); - group.pivot = new this.paper.Point(0,0); + group.pivot = this.model.pivot || new this.paper.Point(0,0); group.position.x = this.model.transformation.x; group.position.y = this.model.transformation.y; group.scaling.x = this.model.transformation.scaleX; diff --git a/engine/src/view/View.Frame.js b/engine/src/view/View.Frame.js index bb714415a..82efc882c 100644 --- a/engine/src/view/View.Frame.js +++ b/engine/src/view/View.Frame.js @@ -138,6 +138,7 @@ Wick.View.Frame = class extends Wick.View { opacity: child.opacity }); + wickClip.pivot = [child.pivot.x, child.pivot.y]; } }); diff --git a/engine/src/view/paper-ext/Paper.SelectionWidget.js b/engine/src/view/paper-ext/Paper.SelectionWidget.js index 63bf6aa50..2c49d1702 100644 --- a/engine/src/view/paper-ext/Paper.SelectionWidget.js +++ b/engine/src/view/paper-ext/Paper.SelectionWidget.js @@ -419,6 +419,8 @@ class SelectionWidget { if (this.currentTransformation === 'move-pivot') { if (this._newPivot) this.pivot = this._newPivot; + if (this._itemsInSelection.length === 1 && this._itemsInSelection[0] instanceof paper.Group) + this._itemsInSelection[0].pivot = this._itemsInSelection[0].globalToLocal(this._newPivot); } else if (this.currentTransformation === 'translate') { this.translateSelection(this._ghost.data.offset); } else if (this.currentTransformation === 'rotate') { From 52c0f28b7b78f1225c120d778fd9d2cd53519b4f Mon Sep 17 00:00:00 2001 From: funny bot Date: Sun, 26 Jul 2026 18:21:27 -0700 Subject: [PATCH 5/8] Selection box skew, clip skewing and tweens, inspector skew --- engine/src/Transformation.js | 26 +++ engine/src/base/Clip.js | 13 ++ engine/src/base/Selection.js | 31 ++++ engine/src/base/Tween.js | 2 +- engine/src/view/View.Clip.js | 12 +- engine/src/view/View.Frame.js | 13 +- engine/src/view/View.Selection.js | 15 ++ .../view/paper-ext/Paper.SelectionWidget.js | 156 +++++++++++------- src/Editor/Panels/Inspector/Inspector.jsx | 14 ++ 9 files changed, 209 insertions(+), 73 deletions(-) diff --git a/engine/src/Transformation.js b/engine/src/Transformation.js index 21e97e9db..3e1c99dce 100644 --- a/engine/src/Transformation.js +++ b/engine/src/Transformation.js @@ -36,9 +36,14 @@ Wick.Transformation = class { this.scaleX = args.scaleX === undefined ? 1 : args.scaleX; this.scaleY = args.scaleY === undefined ? 1 : args.scaleY; this.rotation = args.rotation === undefined ? 0 : args.rotation; + this.skew = args.skew === undefined ? 0 : args.skew; this.opacity = args.opacity === undefined ? 1 : args.opacity; } + get scaledSkew () { + return Math.atan(Math.tan(this.skew * Math.PI/180) * this.scaleY / this.scaleX) * 180/Math.PI; + } + /** * An object containing the values of this transformation. */ @@ -49,10 +54,31 @@ Wick.Transformation = class { scaleX: this.scaleX, scaleY: this.scaleY, rotation: this.rotation, + skew: this.skew, opacity: this.opacity, } } + /** + * An array containing the matrix values of this transformation. + */ + get matrix () { + // https://github.com/paperjs/paper.js/blob/92775f5279c05fb7f0a743e9e7fa02cd40ec1e70/src/basic/Matrix.js#L687 + const { x, y, scaleX, scaleY, rotation } = this; + const degrees = 180 / Math.PI, + rotateRad = rotation / degrees, + skewRad = this.scaledSkew / degrees; + let a, b, c, d; + let r = scaleX, + det = scaleY * r, + at = Math.tan(skewRad) * r * r; + a = Math.cos(rotateRad) * r; + b = Math.sqrt(r * r - a * a) * (rotateRad > 0 ? 1 : -1); + d = (b * at + a * det) / (a * a + b * b); + c = (at - b * d) / a; + return [a, b, c, d, x, y]; + } + /** * Creates a copy of this transformation. * @returns {Wick.Transformation} the copied transformation. diff --git a/engine/src/base/Clip.js b/engine/src/base/Clip.js index 036c674bf..f895fbce4 100644 --- a/engine/src/base/Clip.js +++ b/engine/src/base/Clip.js @@ -1334,6 +1334,19 @@ let avgIntersection = { this._onDirtyTransform(); } + /** + * The skew of the clip. + * @type {number} + */ + get skew() { + return this.transformation.skew; + } + + set skew(skew) { + this.transformation.skew = skew; + + this._onDirtyTransform(); + } /** * The opacity of the clip. diff --git a/engine/src/base/Selection.js b/engine/src/base/Selection.js index 782f3a953..84956fdf2 100644 --- a/engine/src/base/Selection.js +++ b/engine/src/base/Selection.js @@ -34,6 +34,7 @@ Wick.Selection = class extends Wick.Base { this._selectedObjectsUUIDs = args.selectedObjects || []; this._widgetRotation = args.widgetRotation || 0; + this._widgetSkew = args.widgetSkew || 0; this._pivotPoint = { x: 0, y: 0 }; this._originalWidth = 0; this._originalHeight = 0; @@ -47,6 +48,7 @@ Wick.Selection = class extends Wick.Base { var data = super._serialize(args); data.selectedObjects = Array.from(this._selectedObjectsUUIDs); data.widgetRotation = this._widgetRotation; + data.widgetSkew = this._widgetSkew; data.pivotPoint = { x: this._pivotPoint.x, y: this._pivotPoint.y, @@ -60,6 +62,7 @@ Wick.Selection = class extends Wick.Base { super._deserialize(data); this._selectedObjectsUUIDs = data.selectedObjects || []; this._widgetRotation = data.widgetRotation; + this._widgetSkew = data.widgetSkew; this._pivotPoint = { x: data.pivotPoint.x, y: data.pivotPoint.y @@ -96,6 +99,7 @@ Wick.Selection = class extends Wick.Base { "width", "height", "rotation", + "skew", "opacity", "sound", "soundVolume", @@ -399,6 +403,18 @@ Wick.Selection = class extends Wick.Base { this._widgetRotation = widgetRotation; } + /** + * The skew of the selection (used for canvas selections) + * @type {number} + */ + get widgetSkew() { + return this._widgetSkew; + } + + set widgetSkew(widgetSkew) { + this._widgetSkew = widgetSkew; + } + /** * The point that transformations to the selection will be based around. * @type {object} @@ -561,6 +577,19 @@ Wick.Selection = class extends Wick.Base { this.view.rotation = rotation; } + /** + * The horizontal skew of the selection. + * @type {number} + */ + get skew() { + return this.view.skew; + } + + set skew(skew) { + this.project.tryToAutoCreateTween(); + this.view.skew = skew; + } + /** * It is the original width of the selection at creation. * @type {number} @@ -980,6 +1009,7 @@ Wick.Selection = class extends Wick.Base { if (selectedObject instanceof Wick.Clip) { // Single clip selected: Use that Clip's transformation for the pivot point and rotation this._widgetRotation = selectedObject.transformation.rotation; + this._widgetSkew = selectedObject.transformation.skew; this._pivotPoint = { x: selectedObject.transformation.x, y: selectedObject.transformation.y, @@ -987,6 +1017,7 @@ Wick.Selection = class extends Wick.Base { } else { // Path selected or multiple objects selected: Reset rotation and use center for pivot point this._widgetRotation = 0; + this._widgetSkew = 0; var boundsCenter = this.view._getSelectedObjectsBounds().center; this._pivotPoint = { diff --git a/engine/src/base/Tween.js b/engine/src/base/Tween.js index 9c0fa994d..48a620cc0 100644 --- a/engine/src/base/Tween.js +++ b/engine/src/base/Tween.js @@ -72,7 +72,7 @@ Wick.Tween = class extends Wick.Base { var t = Wick.Tween._calculateTimeValue(tweenA, tweenB, playheadPosition); // Interpolate every transformation attribute using the t value - ["x", "y", "scaleX", "scaleY", "rotation", "opacity"].forEach(propName => { + ["x", "y", "scaleX", "scaleY", "rotation", "skew", "opacity"].forEach(propName => { var tweenFn = tweenA._getTweenFunction(); var tt = tweenFn(t); var valA = tweenA.transformation[propName]; diff --git a/engine/src/view/View.Clip.js b/engine/src/view/View.Clip.js index fac6a2238..e8cd29cf7 100644 --- a/engine/src/view/View.Clip.js +++ b/engine/src/view/View.Clip.js @@ -173,11 +173,7 @@ Wick.View.Clip = class extends Wick.View { //this._radius = null; this.group.pivot = this.model.pivot || new this.paper.Point(0,0); - this.group.position.x = this.model.transformation.x; - this.group.position.y = this.model.transformation.y; - this.group.scaling.x = this.model.transformation.scaleX; - this.group.scaling.y = this.model.transformation.scaleY; - this.group.rotation = this.model.transformation.rotation; + this.group.matrix.set(this.model.transformation.matrix); this.group.opacity = this.model.transformation.opacity; } @@ -207,11 +203,7 @@ Wick.View.Clip = class extends Wick.View { group.addChild(border); group.pivot = this.model.pivot || new this.paper.Point(0,0); - group.position.x = this.model.transformation.x; - group.position.y = this.model.transformation.y; - group.scaling.x = this.model.transformation.scaleX; - group.scaling.y = this.model.transformation.scaleY; - group.rotation = this.model.transformation.rotation; + group.matrix.set(this.model.transformation.matrix); return group; } diff --git a/engine/src/view/View.Frame.js b/engine/src/view/View.Frame.js index 82efc882c..7344208b6 100644 --- a/engine/src/view/View.Frame.js +++ b/engine/src/view/View.Frame.js @@ -129,14 +129,15 @@ Wick.View.Frame = class extends Wick.View { }).forEach(child => { if (child instanceof paper.Group || child instanceof Wick.Clip) { var wickClip = Wick.ObjectCache.getObjectByUUID(child.data.wickUUID); + var values = child.matrix.decompose(); wickClip.transformation = new Wick.Transformation({ - x: child.position.x, - y: child.position.y, - scaleX: child.scaling.x, - scaleY: child.scaling.y, - rotation: child.rotation, + x: values.translation.x, + y: values.translation.y, + scaleX: values.scaling.x, + scaleY: values.scaling.y, + rotation: values.rotation, + skew: Math.atan(Math.tan(values.skewing.x*Math.PI/180) * values.scaling.x/values.scaling.y) * 180/Math.PI, opacity: child.opacity - }); wickClip.pivot = [child.pivot.x, child.pivot.y]; } diff --git a/engine/src/view/View.Selection.js b/engine/src/view/View.Selection.js index 9c9f71e3b..042af8d93 100644 --- a/engine/src/view/View.Selection.js +++ b/engine/src/view/View.Selection.js @@ -48,6 +48,7 @@ Wick.View.Selection = class extends Wick.View { */ applyChanges () { this.model.widgetRotation = this.widget.rotation; + this.model.widgetSkew = this.widget.skew; this.model.pivotPoint = { x: this.widget.pivot.x, y: this.widget.pivot.y, @@ -115,6 +116,19 @@ Wick.View.Selection = class extends Wick.View { this.model.widgetRotation = rotation; } + /** + * + */ + get skew () { + return this.widget.skew; + } + + set skew (skew) { + this.widget.skew = skew; + this.model.project.view.applyChanges(); + this.model.widgetSkew = skew; + } + /** * */ @@ -166,6 +180,7 @@ Wick.View.Selection = class extends Wick.View { render () { this._widget.build({ boxRotation: this.model.widgetRotation, + boxSkew: this.model.widgetSkew, items: this._getSelectedObjectViews(), pivot: new paper.Point(this.model.pivotPoint.x, this.model.pivotPoint.y), useGradientGUI: this.model.useGradientGUI, diff --git a/engine/src/view/paper-ext/Paper.SelectionWidget.js b/engine/src/view/paper-ext/Paper.SelectionWidget.js index 2c49d1702..cbc77b331 100644 --- a/engine/src/view/paper-ext/Paper.SelectionWidget.js +++ b/engine/src/view/paper-ext/Paper.SelectionWidget.js @@ -79,6 +79,12 @@ class SelectionWidget { }; } + _skewPoint = (point, skewX, skewY, pivot) => { + if (!pivot) pivot = new paper.Point(0,0); + let d = point.subtract(pivot); + return pivot.add(d.add(d.y * Math.tan(skewX * Math.PI/180), d.x * Math.tan(skewY * Math.PI/180))); + } + /** * The item containing the widget GUI */ @@ -108,6 +114,17 @@ class SelectionWidget { this._boxRotation = boxRotation; } + /** + * The horizontal skew of the selection box GUI. + */ + get boxSkew() { + return this._boxSkew; + } + + set boxSkew(boxSkew) { + this._boxSkew = boxSkew; + } + /** * The items currently inside the selection widget */ @@ -130,7 +147,7 @@ class SelectionWidget { * The position of the top left corner of the selection box. */ get position() { - return this._boundingBox.topLeft.rotate(this.rotation, this.pivot); + return this._skewPoint(this._boundingBox.topLeft, this.skew, 0, this.pivot).rotate(this.rotation, this.pivot); } set position(position) { @@ -175,6 +192,19 @@ class SelectionWidget { this.rotateSelection(d); } + /** + * The horizontal skew of the selection. + */ + get skew() { + return this._boxSkew; + } + + set skew(skew) { + if (skew === 90) skew = 89.999; + var d = Math.tan(skew * Math.PI/180) - Math.tan(this.skew * Math.PI/180); + this.transformSelection((new paper.Matrix()).shear(d, 0)); + } + /** * Flip the selected items horizontally. */ @@ -224,11 +254,13 @@ class SelectionWidget { build(args) { if (!args) args = {}; if (!args.boxRotation) args.boxRotation = 0; + if (!args.boxSkew) args.boxSkew = 0; if (!args.items) args.items = []; if (!args.pivot) args.pivot = new paper.Point(); this._itemsInSelection = args.items; this._boxRotation = args.boxRotation; + this._boxSkew = args.boxSkew; this._pivot = args.pivot; this._useGradientGUI = args.useGradientGUI; @@ -307,7 +339,7 @@ class SelectionWidget { var vertical = item.data.handleEdge === 'topCenter' || item.data.handleEdge === 'bottomCenter'; this._ghost.matrix.reset(); - this._ghost.rotate(-this.boxRotation, this.pivot); + this._ghost.rotate(-this.boxRotation, this.pivot).skew(-this.boxSkew, 0, this.pivot); if (modifiers.center) { this._truePivot = this.pivot; } else if (this.currentTransformation === 'scale-edge') { @@ -330,7 +362,7 @@ class SelectionWidget { } } let unrotatedPivot = this._truePivot; - this._truePivot = this._truePivot.rotate(this.boxRotation, this.pivot); + this._truePivot = this._skewPoint(this._truePivot, this.boxSkew, 0, this.pivot).rotate(this.boxRotation, this.pivot); if (this.currentTransformation === 'move-pivot') { item.matrix.reset(); @@ -358,9 +390,11 @@ class SelectionWidget { } this._ghost.rotate(rotateDelta, this._truePivot); } else if (this.currentTransformation === 'scale-corner') { - var currentPointRelative = e.point.rotate(-this.boxRotation, this.pivot).subtract(unrotatedPivot); - var initialPointRelative = this._initialPoint.rotate(-this.boxRotation, this.pivot).subtract(unrotatedPivot); - var scaleFactor = currentPointRelative.divide(initialPointRelative); + var deltaLocal = this._skewPoint(e.point.subtract(this._initialPoint).rotate(-this.boxRotation), -this.boxSkew, 0), + cornerLocal = this._skewPoint(item.position.rotate(-this.boxRotation, this.pivot), -this.boxSkew, 0, this.pivot), + distCorner = cornerLocal.subtract(unrotatedPivot), + distMovedCorner = distCorner.add(deltaLocal); + var scaleFactor = distMovedCorner.divide(distCorner); if (modifiers.constrain) { if (Math.abs(scaleFactor.x) < Math.abs(scaleFactor.y)) { scaleFactor.x = Math.sign(scaleFactor.x) * Math.abs(scaleFactor.y); @@ -371,28 +405,24 @@ class SelectionWidget { this._ghost.data.scale = scaleFactor; this._ghost.scale(scaleFactor, unrotatedPivot); - this._ghost.rotate(this.boxRotation, this.pivot); + this._ghost.skew(this.boxSkew, 0, this.pivot).rotate(this.boxRotation, this.pivot); } else { - var currentPointRelative = e.point.rotate(-this.boxRotation, this.pivot); - var initialPointRelative = this._initialPoint.rotate(-this.boxRotation, this.pivot); - + var deltaLocal = this._skewPoint(e.point.subtract(this._initialPoint).rotate(-this.boxRotation), -this.boxSkew, 0), + edgeLocal = this._skewPoint(item.position.rotate(-this.boxRotation, this.pivot), -this.boxSkew, 0, this.pivot), + distEdge = edgeLocal.subtract(unrotatedPivot), + distMovedEdge = distEdge.add(deltaLocal); this._ghost.data.transform.reset(); if (!modifiers.skew || (modifiers.skew && e.modifiers.shift)) { - var scaleFactor = currentPointRelative.subtract(unrotatedPivot).divide(initialPointRelative.subtract(unrotatedPivot)); - if (vertical) - scaleFactor.x = 1; - else - scaleFactor.y = 1; - - this._ghost.data.transform.scale(scaleFactor) + var scaleFactor = distMovedEdge.divide(distEdge); + if (vertical) scaleFactor.x = 1; + else scaleFactor.y = 1; + + this._ghost.data.transform.scale(scaleFactor); } if (modifiers.skew) { - var shearFactor = currentPointRelative.subtract(initialPointRelative).divide(this._ghost.bounds.height, this._ghost.bounds.width); - if (vertical) - shearFactor.y = 0; - else - shearFactor.x = 0; - + var shearFactor = deltaLocal.divide(this._ghost.bounds.height, this._ghost.bounds.width); + if (vertical) shearFactor.y = 0; + else shearFactor.x = 0; if (modifiers.center) shearFactor = shearFactor.multiply(2); if (topLeft) @@ -400,9 +430,8 @@ class SelectionWidget { this._ghost.data.transform.shear(shearFactor); } - this._ghost.translate(unrotatedPivot.multiply(-1)).transform(this._ghost.data.transform).translate(unrotatedPivot); - this._ghost.rotate(this.boxRotation, this.pivot); + this._ghost.skew(this.boxSkew, 0, this.pivot).rotate(this.boxRotation, this.pivot); } } @@ -430,6 +459,12 @@ class SelectionWidget { this.scaleSelection(this._ghost.data.scale, this._truePivot); } else { this.transformSelection(this._ghost.data.transform, this._truePivot); + + // Paper.js matrix transforms are reversed. + var mat = (new paper.Matrix()).rotate(this.boxRotation).skew(this.boxSkew, 0).append(this._ghost.data.transform); + var adj = mat.decompose(); + this.boxRotation = adj.rotation; + this.boxSkew = Math.atan(Math.tan(adj.skewing.x * Math.PI/180) * adj.scaling.x/adj.scaling.y) * 180/Math.PI; } this._currentTransformation = null; @@ -460,32 +495,30 @@ class SelectionWidget { * */ scaleSelection(scale, pivot = this.pivot) { - let unrotatedPivot = pivot.rotate(-this.boxRotation, this.pivot); + let unrotatedPivot = this._skewPoint(pivot.rotate(-this.boxRotation, this.pivot), -this.boxSkew, 0, this.pivot); this._itemsInSelection.forEach(item => { - item.rotate(-this.boxRotation, this.pivot); + item.rotate(-this.boxRotation, this.pivot).skew(-this.boxSkew, 0, this.pivot); item.scale(scale, unrotatedPivot); - item.rotate(this.boxRotation, this.pivot); + item.skew(this.boxSkew, 0, this.pivot).rotate(this.boxRotation, this.pivot); }); var newPivot = unrotatedPivot.add(this.pivot.subtract(unrotatedPivot).multiply(scale)); - this.pivot = newPivot.rotate(this.boxRotation, this.pivot); + this.pivot = this._skewPoint(newPivot, this.boxSkew, 0, this.pivot).rotate(this.boxRotation, this.pivot); } /** * */ transformSelection(matrix, pivot = this.pivot) { - let unrotatedPivot = pivot.rotate(-this.boxRotation, this.pivot); + let unrotatedPivot = this._skewPoint(pivot.rotate(-this.boxRotation, this.pivot), -this.boxSkew, 0, this.pivot); this._itemsInSelection.forEach(item => { - item.rotate(-this.boxRotation, this.pivot); + item.rotate(-this.boxRotation, this.pivot).skew(-this.boxSkew, 0, this.pivot); item.translate(unrotatedPivot.multiply(-1)).transform(matrix).translate(unrotatedPivot); - item.rotate(this.boxRotation, this.pivot); + item.skew(this.boxSkew, 0, this.pivot).rotate(this.boxRotation, this.pivot); }); - // Note that the GUI won't show this pivot as the center because it doesn't account for skew. - // The pivot point after the skew will look a bit off. var newPivot = unrotatedPivot.add(this.pivot.subtract(unrotatedPivot).transform(matrix)); - this.pivot = newPivot.rotate(this.boxRotation, this.pivot); + this.pivot = this._skewPoint(newPivot, this.boxSkew, 0, this.pivot).rotate(this.boxRotation, this.pivot); } _buildGUI() { @@ -516,7 +549,7 @@ class SelectionWidget { this._pivotPointHandle = this._buildPivotPointHandle(); this.layer.addChild(this._pivotPointHandle); - this.item.rotate(this.boxRotation, this._center); + this.item.skew(this.boxSkew, 0, this._center).rotate(this.boxRotation, this._center); this.item.children.forEach(child => { child.data.isSelectionBoxGUI = true; @@ -539,7 +572,7 @@ class SelectionWidget { _buildItemOutlines() { return this._itemsInSelection.map(item => { var clone = item.clone({ insert: false }); - clone.rotate(-this.boxRotation, this._center); + clone.rotate(-this.boxRotation, this._center).skew(-this.boxSkew, 0, this._center); var bounds = clone.bounds; var border = new paper.Path.Rectangle({ from: bounds.topLeft, @@ -561,6 +594,7 @@ class SelectionWidget { fillColor: SelectionWidget.HANDLE_FILL_COLOR, strokeColor: SelectionWidget.HANDLE_STROKE_COLOR, }); + handle.skew(-this.boxSkew, 0); return handle; } @@ -609,26 +643,36 @@ class SelectionWidget { // +------+ // | + // Compensate for the selection's horizontal skew var r = SelectionWidget.ROTATION_HOTSPOT_RADIUS / paper.view.zoom; - var hotspot = new paper.Path([ - new paper.Point(0, 0), - new paper.Point(0, r), - new paper.Point(r, r), - new paper.Point(r, -r), - new paper.Point(-r, -r), - new paper.Point(-r, 0), - ]); - hotspot.fillColor = SelectionWidget.ROTATION_HOTSPOT_FILLCOLOR; - hotspot.position.x = this.boundingBox[cornerName].x; - hotspot.position.y = this.boundingBox[cornerName].y; + var theta = this.boxSkew * Math.PI/180; + if (cornerName === 'topLeft' || cornerName === 'bottomRight') theta *= -1; + + var cost = Math.cos(theta), + sint = Math.sin(theta); + var corner = (theta >= 0) + ? [new paper.Point(r/cost, -r)] + : [new paper.Point(r/cost, -r*(cost+sint)), + new paper.Point(r*(sint/cost+1), -r)] + var hotspot = new paper.Path({ + segments: [ + new paper.Point(0, 0), + new paper.Point(0, r*cost), + new paper.Point(r/cost, r*(cost-sint)), + ...corner, + new paper.Point(r*(sint/cost-1), -r), + new paper.Point(-r, 0), + ], + pivot: [0, 0], + fillColor: SelectionWidget.ROTATION_HOTSPOT_FILLCOLOR, + position: this.boundingBox[cornerName] + }); // Orient the rotation handles in the correct direction, even if the selection is flipped - hotspot.rotate({ - 'topRight': 0, - 'bottomRight': 90, - 'bottomLeft': 180, - 'topLeft': 270, - }[cornerName]); + hotspot.scale( + (cornerName === 'topLeft' || cornerName === 'bottomLeft') ? -1 : 1, + (cornerName === 'bottomRight' || cornerName === 'bottomLeft') ? -1 : 1 + ); // Some metadata. hotspot.data.handleType = 'rotation'; @@ -667,7 +711,7 @@ class SelectionWidget { strokeWidth: SelectionWidget.GHOST_STROKE_WIDTH / paper.view.zoom, applyMatrix: false, }); - boundsOutline.rotate(this.boxRotation, this._center); + boundsOutline.skew(this.boxSkew, 0, this._center).rotate(this.boxRotation, this._center); ghost.addChild(boundsOutline); ghost.opacity = 0.5; @@ -684,7 +728,7 @@ class SelectionWidget { var itemsForBoundsCalc = this._itemsInSelection.map(item => { var clone = item.clone(); - clone.rotate(-this.boxRotation, center); + clone.rotate(-this.boxRotation, center).skew(-this.boxSkew, 0, center); clone.remove(); return clone; }); diff --git a/src/Editor/Panels/Inspector/Inspector.jsx b/src/Editor/Panels/Inspector/Inspector.jsx index 22ecc5436..09acfde38 100644 --- a/src/Editor/Panels/Inspector/Inspector.jsx +++ b/src/Editor/Panels/Inspector/Inspector.jsx @@ -559,6 +559,19 @@ class Inspector extends Component { ) } + /** + * Renders an inspector row allowing viewing and editing of the selection's skew. + */ + renderSkew = () => { + return ( + this.setSelectionAttribute('skew', val)} + id="inspector-skew" /> + ) + } + /** * Renders an inspector row allowing viewing and editing of the selection's opacity. */ @@ -586,6 +599,7 @@ class Inspector extends Component { {this.renderSize()} {this.renderScale()} {this.renderRotation()} + {this.renderSkew()} {this.renderOpacity()} ) From 14ac68eb95d9388cece251ae388b721b428fcb31 Mon Sep 17 00:00:00 2001 From: funny bot Date: Mon, 27 Jul 2026 19:35:29 -0700 Subject: [PATCH 6/8] Change selection rotate handle UI, fix transform bug with pivot point --- engine/src/view/View.Clip.js | 4 +- engine/src/view/View.Frame.js | 4 +- .../view/paper-ext/Paper.SelectionWidget.js | 46 ++++++++----------- 3 files changed, 22 insertions(+), 32 deletions(-) diff --git a/engine/src/view/View.Clip.js b/engine/src/view/View.Clip.js index e8cd29cf7..ec84d1df9 100644 --- a/engine/src/view/View.Clip.js +++ b/engine/src/view/View.Clip.js @@ -173,7 +173,7 @@ Wick.View.Clip = class extends Wick.View { //this._radius = null; this.group.pivot = this.model.pivot || new this.paper.Point(0,0); - this.group.matrix.set(this.model.transformation.matrix); + this.group.matrix.translate(this.group.pivot.multiply(-1)).prepend(new paper.Matrix(this.model.transformation.matrix)); this.group.opacity = this.model.transformation.opacity; } @@ -203,7 +203,7 @@ Wick.View.Clip = class extends Wick.View { group.addChild(border); group.pivot = this.model.pivot || new this.paper.Point(0,0); - group.matrix.set(this.model.transformation.matrix); + group.matrix.translate(this.group.pivot.multiply(-1)).prepend(new paper.Matrix(this.model.transformation.matrix)); return group; } diff --git a/engine/src/view/View.Frame.js b/engine/src/view/View.Frame.js index 7344208b6..c4badd55e 100644 --- a/engine/src/view/View.Frame.js +++ b/engine/src/view/View.Frame.js @@ -131,8 +131,8 @@ Wick.View.Frame = class extends Wick.View { var wickClip = Wick.ObjectCache.getObjectByUUID(child.data.wickUUID); var values = child.matrix.decompose(); wickClip.transformation = new Wick.Transformation({ - x: values.translation.x, - y: values.translation.y, + x: child.position.x, + y: child.position.y, scaleX: values.scaling.x, scaleY: values.scaling.y, rotation: values.rotation, diff --git a/engine/src/view/paper-ext/Paper.SelectionWidget.js b/engine/src/view/paper-ext/Paper.SelectionWidget.js index cbc77b331..d8eceadb3 100644 --- a/engine/src/view/paper-ext/Paper.SelectionWidget.js +++ b/engine/src/view/paper-ext/Paper.SelectionWidget.js @@ -421,12 +421,13 @@ class SelectionWidget { } if (modifiers.skew) { var shearFactor = deltaLocal.divide(this._ghost.bounds.height, this._ghost.bounds.width); - if (vertical) shearFactor.y = 0; - else shearFactor.x = 0; - if (modifiers.center) - shearFactor = shearFactor.multiply(2); - if (topLeft) - shearFactor = shearFactor.multiply(-1); + if (vertical) { + shearFactor.y = 0; + shearFactor.x *= this._ghost.bounds.height / distEdge.y; + } else { + shearFactor.x = 0; + shearFactor.y *= this._ghost.bounds.width / distEdge.x; + } this._ghost.data.transform.shear(shearFactor); } @@ -643,36 +644,25 @@ class SelectionWidget { // +------+ // | - // Compensate for the selection's horizontal skew var r = SelectionWidget.ROTATION_HOTSPOT_RADIUS / paper.view.zoom; - var theta = this.boxSkew * Math.PI/180; - if (cornerName === 'topLeft' || cornerName === 'bottomRight') theta *= -1; - - var cost = Math.cos(theta), - sint = Math.sin(theta); - var corner = (theta >= 0) - ? [new paper.Point(r/cost, -r)] - : [new paper.Point(r/cost, -r*(cost+sint)), - new paper.Point(r*(sint/cost+1), -r)] - var hotspot = new paper.Path({ - segments: [ - new paper.Point(0, 0), - new paper.Point(0, r*cost), - new paper.Point(r/cost, r*(cost-sint)), - ...corner, - new paper.Point(r*(sint/cost-1), -r), - new paper.Point(-r, 0), - ], - pivot: [0, 0], - fillColor: SelectionWidget.ROTATION_HOTSPOT_FILLCOLOR, - position: this.boundingBox[cornerName] + var angle = this.boxSkew * Math.PI/180; + if (cornerName === 'topLeft' || cornerName === 'bottomRight') angle *= -1; + var hotspot = new paper.Path.Arc({ + from: [r * Math.sin(angle), r * Math.cos(angle)], through: [0,-r], to: [-r,0], + pivot: [0, 0] }); + hotspot.firstSegment.handleIn = [0,0]; + hotspot.lastSegment.handleOut = [0,0]; + hotspot.add([0,0]); + hotspot.fillColor = SelectionWidget.ROTATION_HOTSPOT_FILLCOLOR; + hotspot.position = this.boundingBox[cornerName]; // Orient the rotation handles in the correct direction, even if the selection is flipped hotspot.scale( (cornerName === 'topLeft' || cornerName === 'bottomLeft') ? -1 : 1, (cornerName === 'bottomRight' || cornerName === 'bottomLeft') ? -1 : 1 ); + hotspot.skew(-this.boxSkew, 0); // Some metadata. hotspot.data.handleType = 'rotation'; From 7428ac2a2217195741c619281e3ec3f50930b262 Mon Sep 17 00:00:00 2001 From: funny bot Date: Tue, 28 Jul 2026 13:55:19 -0700 Subject: [PATCH 7/8] Change skews to shears and add safety checks to rotation handles --- engine/src/Transformation.js | 6 +- engine/src/base/Clip.js | 10 +- engine/src/base/Selection.js | 32 +++---- engine/src/base/Tween.js | 2 +- engine/src/view/View.Frame.js | 2 +- engine/src/view/View.Selection.js | 14 +-- .../view/paper-ext/Paper.SelectionWidget.js | 93 +++++++++---------- src/Editor/Panels/Inspector/Inspector.jsx | 14 +-- 8 files changed, 82 insertions(+), 91 deletions(-) diff --git a/engine/src/Transformation.js b/engine/src/Transformation.js index 3e1c99dce..cdafd4425 100644 --- a/engine/src/Transformation.js +++ b/engine/src/Transformation.js @@ -36,12 +36,12 @@ Wick.Transformation = class { this.scaleX = args.scaleX === undefined ? 1 : args.scaleX; this.scaleY = args.scaleY === undefined ? 1 : args.scaleY; this.rotation = args.rotation === undefined ? 0 : args.rotation; - this.skew = args.skew === undefined ? 0 : args.skew; + this.shear = args.shear === undefined ? 0 : args.shear; this.opacity = args.opacity === undefined ? 1 : args.opacity; } get scaledSkew () { - return Math.atan(Math.tan(this.skew * Math.PI/180) * this.scaleY / this.scaleX) * 180/Math.PI; + return Math.atan(this.shear * this.scaleY / this.scaleX) * 180/Math.PI; } /** @@ -54,7 +54,7 @@ Wick.Transformation = class { scaleX: this.scaleX, scaleY: this.scaleY, rotation: this.rotation, - skew: this.skew, + shear: this.shear, opacity: this.opacity, } } diff --git a/engine/src/base/Clip.js b/engine/src/base/Clip.js index f895fbce4..d357c6f09 100644 --- a/engine/src/base/Clip.js +++ b/engine/src/base/Clip.js @@ -1335,15 +1335,15 @@ let avgIntersection = { this._onDirtyTransform(); } /** - * The skew of the clip. + * The shear of the clip. * @type {number} */ - get skew() { - return this.transformation.skew; + get shear() { + return this.transformation.shear; } - set skew(skew) { - this.transformation.skew = skew; + set shear(shear) { + this.transformation.shear = shear; this._onDirtyTransform(); } diff --git a/engine/src/base/Selection.js b/engine/src/base/Selection.js index 84956fdf2..87a66330f 100644 --- a/engine/src/base/Selection.js +++ b/engine/src/base/Selection.js @@ -34,7 +34,7 @@ Wick.Selection = class extends Wick.Base { this._selectedObjectsUUIDs = args.selectedObjects || []; this._widgetRotation = args.widgetRotation || 0; - this._widgetSkew = args.widgetSkew || 0; + this._widgetShear = args.widgetShear || 0; this._pivotPoint = { x: 0, y: 0 }; this._originalWidth = 0; this._originalHeight = 0; @@ -48,7 +48,7 @@ Wick.Selection = class extends Wick.Base { var data = super._serialize(args); data.selectedObjects = Array.from(this._selectedObjectsUUIDs); data.widgetRotation = this._widgetRotation; - data.widgetSkew = this._widgetSkew; + data.widgetShear = this._widgetShear; data.pivotPoint = { x: this._pivotPoint.x, y: this._pivotPoint.y, @@ -62,7 +62,7 @@ Wick.Selection = class extends Wick.Base { super._deserialize(data); this._selectedObjectsUUIDs = data.selectedObjects || []; this._widgetRotation = data.widgetRotation; - this._widgetSkew = data.widgetSkew; + this._widgetShear = data.widgetShear; this._pivotPoint = { x: data.pivotPoint.x, y: data.pivotPoint.y @@ -99,7 +99,7 @@ Wick.Selection = class extends Wick.Base { "width", "height", "rotation", - "skew", + "shear", "opacity", "sound", "soundVolume", @@ -404,15 +404,15 @@ Wick.Selection = class extends Wick.Base { } /** - * The skew of the selection (used for canvas selections) + * The shear of the selection (used for canvas selections) * @type {number} */ - get widgetSkew() { - return this._widgetSkew; + get widgetShear() { + return this._widgetShear; } - set widgetSkew(widgetSkew) { - this._widgetSkew = widgetSkew; + set widgetShear(widgetShear) { + this._widgetShear = widgetShear; } /** @@ -578,16 +578,16 @@ Wick.Selection = class extends Wick.Base { } /** - * The horizontal skew of the selection. + * The horizontal shear of the selection. * @type {number} */ - get skew() { - return this.view.skew; + get shear() { + return this.view.shear; } - set skew(skew) { + set shear(shear) { this.project.tryToAutoCreateTween(); - this.view.skew = skew; + this.view.shear = shear; } /** @@ -1009,7 +1009,7 @@ Wick.Selection = class extends Wick.Base { if (selectedObject instanceof Wick.Clip) { // Single clip selected: Use that Clip's transformation for the pivot point and rotation this._widgetRotation = selectedObject.transformation.rotation; - this._widgetSkew = selectedObject.transformation.skew; + this._widgetShear = selectedObject.transformation.shear; this._pivotPoint = { x: selectedObject.transformation.x, y: selectedObject.transformation.y, @@ -1017,7 +1017,7 @@ Wick.Selection = class extends Wick.Base { } else { // Path selected or multiple objects selected: Reset rotation and use center for pivot point this._widgetRotation = 0; - this._widgetSkew = 0; + this._widgetShear = 0; var boundsCenter = this.view._getSelectedObjectsBounds().center; this._pivotPoint = { diff --git a/engine/src/base/Tween.js b/engine/src/base/Tween.js index 48a620cc0..70edca57d 100644 --- a/engine/src/base/Tween.js +++ b/engine/src/base/Tween.js @@ -72,7 +72,7 @@ Wick.Tween = class extends Wick.Base { var t = Wick.Tween._calculateTimeValue(tweenA, tweenB, playheadPosition); // Interpolate every transformation attribute using the t value - ["x", "y", "scaleX", "scaleY", "rotation", "skew", "opacity"].forEach(propName => { + ["x", "y", "scaleX", "scaleY", "rotation", "shear", "opacity"].forEach(propName => { var tweenFn = tweenA._getTweenFunction(); var tt = tweenFn(t); var valA = tweenA.transformation[propName]; diff --git a/engine/src/view/View.Frame.js b/engine/src/view/View.Frame.js index c4badd55e..89907570c 100644 --- a/engine/src/view/View.Frame.js +++ b/engine/src/view/View.Frame.js @@ -136,7 +136,7 @@ Wick.View.Frame = class extends Wick.View { scaleX: values.scaling.x, scaleY: values.scaling.y, rotation: values.rotation, - skew: Math.atan(Math.tan(values.skewing.x*Math.PI/180) * values.scaling.x/values.scaling.y) * 180/Math.PI, + shear: Math.tan(values.skewing.x * Math.PI/180) * values.scaling.x/values.scaling.y, opacity: child.opacity }); wickClip.pivot = [child.pivot.x, child.pivot.y]; diff --git a/engine/src/view/View.Selection.js b/engine/src/view/View.Selection.js index 042af8d93..528e44bb5 100644 --- a/engine/src/view/View.Selection.js +++ b/engine/src/view/View.Selection.js @@ -48,7 +48,7 @@ Wick.View.Selection = class extends Wick.View { */ applyChanges () { this.model.widgetRotation = this.widget.rotation; - this.model.widgetSkew = this.widget.skew; + this.model.widgetShear = this.widget.shear; this.model.pivotPoint = { x: this.widget.pivot.x, y: this.widget.pivot.y, @@ -119,14 +119,14 @@ Wick.View.Selection = class extends Wick.View { /** * */ - get skew () { - return this.widget.skew; + get shear () { + return this.widget.shear; } - set skew (skew) { - this.widget.skew = skew; + set shear (shear) { + this.widget.shear = shear; this.model.project.view.applyChanges(); - this.model.widgetSkew = skew; + this.model.widgetShear = shear; } /** @@ -180,7 +180,7 @@ Wick.View.Selection = class extends Wick.View { render () { this._widget.build({ boxRotation: this.model.widgetRotation, - boxSkew: this.model.widgetSkew, + boxShear: this.model.widgetShear, items: this._getSelectedObjectViews(), pivot: new paper.Point(this.model.pivotPoint.x, this.model.pivotPoint.y), useGradientGUI: this.model.useGradientGUI, diff --git a/engine/src/view/paper-ext/Paper.SelectionWidget.js b/engine/src/view/paper-ext/Paper.SelectionWidget.js index d8eceadb3..de878e4e8 100644 --- a/engine/src/view/paper-ext/Paper.SelectionWidget.js +++ b/engine/src/view/paper-ext/Paper.SelectionWidget.js @@ -79,10 +79,10 @@ class SelectionWidget { }; } - _skewPoint = (point, skewX, skewY, pivot) => { + _shearPoint = (point, shearX, shearY, pivot) => { if (!pivot) pivot = new paper.Point(0,0); let d = point.subtract(pivot); - return pivot.add(d.add(d.y * Math.tan(skewX * Math.PI/180), d.x * Math.tan(skewY * Math.PI/180))); + return pivot.add(d.add(d.y * shearX, d.x * shearY)); } /** @@ -115,14 +115,14 @@ class SelectionWidget { } /** - * The horizontal skew of the selection box GUI. + * The horizontal shear of the selection box GUI. */ - get boxSkew() { - return this._boxSkew; + get boxShear() { + return this._boxShear; } - set boxSkew(boxSkew) { - this._boxSkew = boxSkew; + set boxShear(boxShear) { + this._boxShear = boxShear; } /** @@ -147,7 +147,7 @@ class SelectionWidget { * The position of the top left corner of the selection box. */ get position() { - return this._skewPoint(this._boundingBox.topLeft, this.skew, 0, this.pivot).rotate(this.rotation, this.pivot); + return this._shearPoint(this._boundingBox.topLeft, this.shear, 0, this.pivot).rotate(this.rotation, this.pivot); } set position(position) { @@ -193,15 +193,14 @@ class SelectionWidget { } /** - * The horizontal skew of the selection. + * The horizontal shear of the selection. */ - get skew() { - return this._boxSkew; + get shear() { + return this._boxShear; } - set skew(skew) { - if (skew === 90) skew = 89.999; - var d = Math.tan(skew * Math.PI/180) - Math.tan(this.skew * Math.PI/180); + set shear(shear) { + var d = shear - this.shear; this.transformSelection((new paper.Matrix()).shear(d, 0)); } @@ -254,13 +253,13 @@ class SelectionWidget { build(args) { if (!args) args = {}; if (!args.boxRotation) args.boxRotation = 0; - if (!args.boxSkew) args.boxSkew = 0; + if (!args.boxShear) args.boxShear = 0; if (!args.items) args.items = []; if (!args.pivot) args.pivot = new paper.Point(); this._itemsInSelection = args.items; this._boxRotation = args.boxRotation; - this._boxSkew = args.boxSkew; + this._boxShear = args.boxShear; this._pivot = args.pivot; this._useGradientGUI = args.useGradientGUI; @@ -339,7 +338,7 @@ class SelectionWidget { var vertical = item.data.handleEdge === 'topCenter' || item.data.handleEdge === 'bottomCenter'; this._ghost.matrix.reset(); - this._ghost.rotate(-this.boxRotation, this.pivot).skew(-this.boxSkew, 0, this.pivot); + this._ghost.rotate(-this.boxRotation, this.pivot).shear(-this.boxShear, 0, this.pivot); if (modifiers.center) { this._truePivot = this.pivot; } else if (this.currentTransformation === 'scale-edge') { @@ -362,7 +361,7 @@ class SelectionWidget { } } let unrotatedPivot = this._truePivot; - this._truePivot = this._skewPoint(this._truePivot, this.boxSkew, 0, this.pivot).rotate(this.boxRotation, this.pivot); + this._truePivot = this._shearPoint(this._truePivot, this.boxShear, 0, this.pivot).rotate(this.boxRotation, this.pivot); if (this.currentTransformation === 'move-pivot') { item.matrix.reset(); @@ -390,8 +389,8 @@ class SelectionWidget { } this._ghost.rotate(rotateDelta, this._truePivot); } else if (this.currentTransformation === 'scale-corner') { - var deltaLocal = this._skewPoint(e.point.subtract(this._initialPoint).rotate(-this.boxRotation), -this.boxSkew, 0), - cornerLocal = this._skewPoint(item.position.rotate(-this.boxRotation, this.pivot), -this.boxSkew, 0, this.pivot), + var deltaLocal = this._shearPoint(e.point.subtract(this._initialPoint).rotate(-this.boxRotation), -this.boxShear, 0), + cornerLocal = this._shearPoint(item.position.rotate(-this.boxRotation, this.pivot), -this.boxShear, 0, this.pivot), distCorner = cornerLocal.subtract(unrotatedPivot), distMovedCorner = distCorner.add(deltaLocal); var scaleFactor = distMovedCorner.divide(distCorner); @@ -405,10 +404,10 @@ class SelectionWidget { this._ghost.data.scale = scaleFactor; this._ghost.scale(scaleFactor, unrotatedPivot); - this._ghost.skew(this.boxSkew, 0, this.pivot).rotate(this.boxRotation, this.pivot); + this._ghost.shear(this.boxShear, 0, this.pivot).rotate(this.boxRotation, this.pivot); } else { - var deltaLocal = this._skewPoint(e.point.subtract(this._initialPoint).rotate(-this.boxRotation), -this.boxSkew, 0), - edgeLocal = this._skewPoint(item.position.rotate(-this.boxRotation, this.pivot), -this.boxSkew, 0, this.pivot), + var deltaLocal = this._shearPoint(e.point.subtract(this._initialPoint).rotate(-this.boxRotation), -this.boxShear, 0), + edgeLocal = this._shearPoint(item.position.rotate(-this.boxRotation, this.pivot), -this.boxShear, 0, this.pivot), distEdge = edgeLocal.subtract(unrotatedPivot), distMovedEdge = distEdge.add(deltaLocal); this._ghost.data.transform.reset(); @@ -432,7 +431,7 @@ class SelectionWidget { this._ghost.data.transform.shear(shearFactor); } this._ghost.translate(unrotatedPivot.multiply(-1)).transform(this._ghost.data.transform).translate(unrotatedPivot); - this._ghost.skew(this.boxSkew, 0, this.pivot).rotate(this.boxRotation, this.pivot); + this._ghost.shear(this.boxShear, 0, this.pivot).rotate(this.boxRotation, this.pivot); } } @@ -462,10 +461,10 @@ class SelectionWidget { this.transformSelection(this._ghost.data.transform, this._truePivot); // Paper.js matrix transforms are reversed. - var mat = (new paper.Matrix()).rotate(this.boxRotation).skew(this.boxSkew, 0).append(this._ghost.data.transform); + var mat = (new paper.Matrix()).rotate(this.boxRotation).shear(this.boxShear, 0).append(this._ghost.data.transform); var adj = mat.decompose(); this.boxRotation = adj.rotation; - this.boxSkew = Math.atan(Math.tan(adj.skewing.x * Math.PI/180) * adj.scaling.x/adj.scaling.y) * 180/Math.PI; + this.boxShear = Math.tan(adj.skewing.x * Math.PI/180) * adj.scaling.x/adj.scaling.y; } this._currentTransformation = null; @@ -496,30 +495,30 @@ class SelectionWidget { * */ scaleSelection(scale, pivot = this.pivot) { - let unrotatedPivot = this._skewPoint(pivot.rotate(-this.boxRotation, this.pivot), -this.boxSkew, 0, this.pivot); + let unrotatedPivot = this._shearPoint(pivot.rotate(-this.boxRotation, this.pivot), -this.boxShear, 0, this.pivot); this._itemsInSelection.forEach(item => { - item.rotate(-this.boxRotation, this.pivot).skew(-this.boxSkew, 0, this.pivot); + item.rotate(-this.boxRotation, this.pivot).shear(-this.boxShear, 0, this.pivot); item.scale(scale, unrotatedPivot); - item.skew(this.boxSkew, 0, this.pivot).rotate(this.boxRotation, this.pivot); + item.shear(this.boxShear, 0, this.pivot).rotate(this.boxRotation, this.pivot); }); var newPivot = unrotatedPivot.add(this.pivot.subtract(unrotatedPivot).multiply(scale)); - this.pivot = this._skewPoint(newPivot, this.boxSkew, 0, this.pivot).rotate(this.boxRotation, this.pivot); + this.pivot = this._shearPoint(newPivot, this.boxShear, 0, this.pivot).rotate(this.boxRotation, this.pivot); } /** * */ transformSelection(matrix, pivot = this.pivot) { - let unrotatedPivot = this._skewPoint(pivot.rotate(-this.boxRotation, this.pivot), -this.boxSkew, 0, this.pivot); + let unrotatedPivot = this._shearPoint(pivot.rotate(-this.boxRotation, this.pivot), -this.boxShear, 0, this.pivot); this._itemsInSelection.forEach(item => { - item.rotate(-this.boxRotation, this.pivot).skew(-this.boxSkew, 0, this.pivot); + item.rotate(-this.boxRotation, this.pivot).shear(-this.boxShear, 0, this.pivot); item.translate(unrotatedPivot.multiply(-1)).transform(matrix).translate(unrotatedPivot); - item.skew(this.boxSkew, 0, this.pivot).rotate(this.boxRotation, this.pivot); + item.shear(this.boxShear, 0, this.pivot).rotate(this.boxRotation, this.pivot); }); var newPivot = unrotatedPivot.add(this.pivot.subtract(unrotatedPivot).transform(matrix)); - this.pivot = this._skewPoint(newPivot, this.boxSkew, 0, this.pivot).rotate(this.boxRotation, this.pivot); + this.pivot = this._shearPoint(newPivot, this.boxShear, 0, this.pivot).rotate(this.boxRotation, this.pivot); } _buildGUI() { @@ -550,7 +549,7 @@ class SelectionWidget { this._pivotPointHandle = this._buildPivotPointHandle(); this.layer.addChild(this._pivotPointHandle); - this.item.skew(this.boxSkew, 0, this._center).rotate(this.boxRotation, this._center); + this.item.shear(this.boxShear, 0, this._center).rotate(this.boxRotation, this._center); this.item.children.forEach(child => { child.data.isSelectionBoxGUI = true; @@ -573,7 +572,7 @@ class SelectionWidget { _buildItemOutlines() { return this._itemsInSelection.map(item => { var clone = item.clone({ insert: false }); - clone.rotate(-this.boxRotation, this._center).skew(-this.boxSkew, 0, this._center); + clone.rotate(-this.boxRotation, this._center).shear(-this.boxShear, 0, this._center); var bounds = clone.bounds; var border = new paper.Path.Rectangle({ from: bounds.topLeft, @@ -595,7 +594,7 @@ class SelectionWidget { fillColor: SelectionWidget.HANDLE_FILL_COLOR, strokeColor: SelectionWidget.HANDLE_STROKE_COLOR, }); - handle.skew(-this.boxSkew, 0); + handle.shear(-this.boxShear, 0); return handle; } @@ -634,18 +633,10 @@ class SelectionWidget { } _buildRotationHotspot(cornerName) { - // Build the not-yet-rotated hotspot, which starts out like this: - - // | - // +---+ - // | | - // ---+--+ |--- - // | | - // +------+ - // | - + // Build the not-yet-rotated hotspot var r = SelectionWidget.ROTATION_HOTSPOT_RADIUS / paper.view.zoom; - var angle = this.boxSkew * Math.PI/180; + var angle = (Math.atan(this.boxShear) + Math.PI/2) % Math.PI - Math.PI/2; + if (angle === -Math.PI/2) angle = Math.sign(this.boxShear) * 1.57; // Just under pi/2 if (cornerName === 'topLeft' || cornerName === 'bottomRight') angle *= -1; var hotspot = new paper.Path.Arc({ from: [r * Math.sin(angle), r * Math.cos(angle)], through: [0,-r], to: [-r,0], @@ -662,7 +653,7 @@ class SelectionWidget { (cornerName === 'topLeft' || cornerName === 'bottomLeft') ? -1 : 1, (cornerName === 'bottomRight' || cornerName === 'bottomLeft') ? -1 : 1 ); - hotspot.skew(-this.boxSkew, 0); + hotspot.shear(-this.boxShear, 0); // Some metadata. hotspot.data.handleType = 'rotation'; @@ -701,7 +692,7 @@ class SelectionWidget { strokeWidth: SelectionWidget.GHOST_STROKE_WIDTH / paper.view.zoom, applyMatrix: false, }); - boundsOutline.skew(this.boxSkew, 0, this._center).rotate(this.boxRotation, this._center); + boundsOutline.shear(this.boxShear, 0, this._center).rotate(this.boxRotation, this._center); ghost.addChild(boundsOutline); ghost.opacity = 0.5; @@ -718,7 +709,7 @@ class SelectionWidget { var itemsForBoundsCalc = this._itemsInSelection.map(item => { var clone = item.clone(); - clone.rotate(-this.boxRotation, center).skew(-this.boxSkew, 0, center); + clone.rotate(-this.boxRotation, center).shear(-this.boxShear, 0, center); clone.remove(); return clone; }); diff --git a/src/Editor/Panels/Inspector/Inspector.jsx b/src/Editor/Panels/Inspector/Inspector.jsx index 09acfde38..42e5ce414 100644 --- a/src/Editor/Panels/Inspector/Inspector.jsx +++ b/src/Editor/Panels/Inspector/Inspector.jsx @@ -560,15 +560,15 @@ class Inspector extends Component { } /** - * Renders an inspector row allowing viewing and editing of the selection's skew. + * Renders an inspector row allowing viewing and editing of the selection's shear. */ - renderSkew = () => { + renderShear = () => { return ( this.setSelectionAttribute('skew', val)} - id="inspector-skew" /> + tooltip="Shear" + val={this.getSelectionAttribute('shear')} + onChange={(val) => this.setSelectionAttribute('shear', val)} + id="inspector-shear" /> ) } @@ -599,7 +599,7 @@ class Inspector extends Component { {this.renderSize()} {this.renderScale()} {this.renderRotation()} - {this.renderSkew()} + {this.renderShear()} {this.renderOpacity()} ) From 836f786625f98aeb46113978b2f62e61b5545272 Mon Sep 17 00:00:00 2001 From: funny bot Date: Wed, 29 Jul 2026 15:34:39 -0700 Subject: [PATCH 8/8] Change a calculation in transform matrix values --- engine/src/Transformation.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/engine/src/Transformation.js b/engine/src/Transformation.js index cdafd4425..8783e141a 100644 --- a/engine/src/Transformation.js +++ b/engine/src/Transformation.js @@ -69,13 +69,13 @@ Wick.Transformation = class { rotateRad = rotation / degrees, skewRad = this.scaledSkew / degrees; let a, b, c, d; - let r = scaleX, + let r = scaleX, r2 = r * r, det = scaleY * r, - at = Math.tan(skewRad) * r * r; + at = Math.tan(skewRad) * r2; a = Math.cos(rotateRad) * r; - b = Math.sqrt(r * r - a * a) * (rotateRad > 0 ? 1 : -1); - d = (b * at + a * det) / (a * a + b * b); - c = (at - b * d) / a; + b = Math.sqrt(r2 - a * a) * (rotateRad > 0 ? 1 : -1); + d = (b * at + a * det) / r2; + c = (a * at - b * det) / r2; return [a, b, c, d, x, y]; }