Snap and align now also works on rotated content. Moved math utilities to appropriate namespaces

This commit is contained in:
Igor Zinken
2021-09-13 21:10:35 +02:00
parent f875a1fc92
commit eb05086116
14 changed files with 145 additions and 128 deletions

View File

@@ -66,7 +66,7 @@ import ToolTypes, { MIN_ZOOM, MAX_ZOOM } from "@/definitions/tool-types";
import Slider from "@/components/ui/slider/slider";
import { enqueueState } from "@/factories/history-state-factory";
import messages from "./messages.json";
import { degreesToRadians, radiansToDegrees } from "@/math/image-math";
import { degreesToRadians, radiansToDegrees } from "@/math/unit-math";
export default {
i18n: { messages },

View File

@@ -20,11 +20,7 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export const degreesToRadians = deg => deg * Math.PI / 180;
export const radiansToDegrees = rad => rad * 180 / Math.PI;
// Safari greatly benefits from round numbers as subpixel content is sometimes ommitted from rendering...
export const fastRound = num => num > 0 ? ( num + .5 ) << 0 : num | 0;
import { fastRound } from "@/math/unit-math";
/**
* Calculates the appropriate dimensions for fitting an image of dimensions
@@ -81,67 +77,3 @@ export const constrain = ( width, height, maxMegaPixel ) => {
export const isPortrait = ( width, height ) => width < height;
export const isLandscape = ( width, height ) => width > height;
export const isSquare = ( width, height ) => width === height;
export const getRotationCenter = ({ left, top, width, height }, rounded = false ) => {
const x = left + width * .5;
const y = top + height * .5;
return {
x: rounded ? fastRound( x ) : x,
y: rounded ? fastRound( y ) : y,
};
};
export const getRotatedSize = ({ width, height }, angleInRadians, rounded = false ) => {
const x1 = -width * .5,
x2 = width * .5,
x3 = width * .5,
x4 = -width * .5,
y1 = height * .5,
y2 = height * .5,
y3 = -height * .5,
y4 = -height * .5;
const cos = Math.cos( angleInRadians );
const sin = Math.sin( angleInRadians );
const x11 = x1 * cos + y1 * sin,
y11 = -x1 * sin + y1 * cos,
x21 = x2 * cos + y2 * sin,
y21 = -x2 * sin + y2 * cos,
x31 = x3 * cos + y3 * sin,
y31 = -x3 * sin + y3 * cos,
x41 = x4 * cos + y4 * sin,
y41 = -x4 * sin + y4 * cos;
const xMin = Math.min( x11, x21, x31, x41 ),
xMax = Math.max( x11, x21, x31, x41 ),
yMin = Math.min( y11, y21, y31, y41 ),
yMax = Math.max( y11, y21, y31, y41 );
const out = {
width : xMax - xMin,
height : yMax - yMin
};
if ( rounded ) {
out.width = fastRound( out.width );
out.height = fastRound( out.height );
}
return out;
};
export const rectangleToCoordinates = ( x, y, width, height ) => [
{ x, y }, { x: x + width, y }, // TL to TR
{ x: x + width, y: y + height }, { x, y: y + height }, // BR to BL
{ x, y }, // back to TL to close selection
];
export const scaleRectangle = ({ left, top, width, height }, scale = 1 ) => {
const scaledWidth = width * scale;
const scaledHeight = height * scale;
return {
left : left - ( scaledWidth * .5 - width * .5 ),
top : top - ( scaledHeight * .5 - height * .5 ),
width : scaledWidth,
height : scaledHeight
};
};

View File

@@ -20,6 +20,83 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { fastRound } from "@/math/unit-math";
const HALF = 0.5;
export const getRotationCenter = ({ left, top, width, height }, rounded = false ) => {
const x = left + width * HALF;
const y = top + height * HALF;
return {
x: rounded ? fastRound( x ) : x,
y: rounded ? fastRound( y ) : y,
};
};
export const rotateRectangle = ( rectangle, angleInRadians = 0, rounded = false ) => {
if ( angleInRadians === 0 ) {
return rectangle;
}
const { x, y, width, height } = rectangle;
const x1 = -width * HALF,
x2 = width * HALF,
x3 = width * HALF,
x4 = -width * HALF,
y1 = height * HALF,
y2 = height * HALF,
y3 = -height * HALF,
y4 = -height * HALF;
const cos = Math.cos( angleInRadians );
const sin = Math.sin( angleInRadians );
const x11 = x1 * cos + y1 * sin,
y11 = -x1 * sin + y1 * cos,
x21 = x2 * cos + y2 * sin,
y21 = -x2 * sin + y2 * cos,
x31 = x3 * cos + y3 * sin,
y31 = -x3 * sin + y3 * cos,
x41 = x4 * cos + y4 * sin,
y41 = -x4 * sin + y4 * cos;
const xMin = Math.min( x11, x21, x31, x41 ),
xMax = Math.max( x11, x21, x31, x41 ),
yMin = Math.min( y11, y21, y31, y41 ),
yMax = Math.max( y11, y21, y31, y41 );
const out = {
width : xMax - xMin,
height : yMax - yMin
};
out.x = x - ( out.width / 2 - width / 2 );
out.y = y - ( out.height / 2 - height / 2 );
if ( rounded ) {
out.x = fastRound( out.x );
out.y = fastRound( out.y );
out.width = fastRound( out.width );
out.height = fastRound( out.height );
}
return out;
};
export const rectangleToCoordinates = ( x, y, width, height ) => [
{ x, y }, { x: x + width, y }, // TL to TR
{ x: x + width, y: y + height }, { x, y: y + height }, // BR to BL
{ x, y }, // back to TL to close selection
];
export const scaleRectangle = ({ left, top, width, height }, scale = 1 ) => {
const scaledWidth = width * scale;
const scaledHeight = height * scale;
return {
left : left - ( scaledWidth * HALF - width * HALF ),
top : top - ( scaledHeight * HALF - height * HALF ),
width : scaledWidth,
height : scaledHeight
};
};
export const areEqual = ( rect1, rect2 ) => {
return (
rect1.left === rect2.left &&

View File

@@ -20,6 +20,12 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export const degreesToRadians = deg => deg * Math.PI / 180;
export const radiansToDegrees = rad => rad * 180 / Math.PI;
// Safari greatly benefits from round numbers as subpixel content is sometimes ommitted from rendering...
export const fastRound = num => num > 0 ? ( num + .5 ) << 0 : num | 0;
const CM_PER_INCH = 2.54;
const MM_PER_INCH = CM_PER_INCH * 10;

View File

@@ -22,7 +22,7 @@
*/
import { sprite } from "zcanvas";
import { SNAP_MARGIN } from "@/definitions/tool-types";
import { fastRound } from "@/math/image-math";
import { fastRound } from "@/math/unit-math";
import { isCoordinateInHorizontalRange, isCoordinateInVerticalRange } from "@/math/point-math";
import { getClosestSnappingPoints } from "@/rendering/snapping";

View File

@@ -26,7 +26,7 @@ import { isInsideTransparentArea } from "@/utils/canvas-util";
import { enqueueState } from "@/factories/history-state-factory";
import { getCanvasInstance, getSpriteForLayer } from "@/factories/sprite-factory";
import { isPointInRange, translatePoints, snapToAngle } from "@/math/point-math";
import { rectangleToCoordinates } from "@/math/image-math";
import { rectangleToCoordinates } from "@/math/rectangle-math";
import { isSelectionClosed, createSelectionForRectangle } from "@/math/selection-math";
import ToolTypes from "@/definitions/tool-types";
import LayerSprite from "@/rendering/canvas-elements/layer-sprite";

View File

@@ -28,7 +28,7 @@ import { blobToResource } from "@/utils/resource-manager";
import { LAYER_GRAPHIC, LAYER_MASK, LAYER_TEXT } from "@/definitions/layer-types";
import { getSizeForBrush } from "@/definitions/brush-types";
import { getRectangleForSelection, isSelectionClosed } from "@/math/selection-math";
import { scaleRectangle } from "@/math/image-math";
import { scaleRectangle } from "@/math/rectangle-math";
import { translatePointerRotation } from "@/math/point-math";
import { renderEffectsForLayer } from "@/services/render-service";
import { clipContextToSelection } from "@/rendering/clipping";

View File

@@ -23,7 +23,7 @@
import Vue from "vue";
import { canvas } from "zcanvas";
import InteractionPane from "@/rendering/canvas-elements/interaction-pane";
import { fastRound } from "@/math/image-math";
import { fastRound } from "@/math/unit-math";
import { renderState } from "@/services/render-service";
class ZoomableCanvas extends canvas {

View File

@@ -20,7 +20,7 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { fastRound } from "@/math/image-math";
import { fastRound } from "@/math/unit-math";
/**
* Renders a Layers text Object as multi line text onto given context.

View File

@@ -28,7 +28,7 @@ import { hasFilters, isEqual as isFiltersEqual } from "@/factories/filters-facto
import { isEqual as isTextEqual } from "@/factories/text-factory";
import { createCanvas, cloneCanvas, matchDimensions } from "@/utils/canvas-util";
import { replaceLayerSource } from "@/utils/layer-util";
import { getRotatedSize, getRotationCenter } from "@/math/image-math";
import { rotateRectangle, getRotationCenter } from "@/math/rectangle-math";
import { hasLayerCache, getLayerCache, setLayerCache } from "@/rendering/cache/bitmap-cache";
import { renderMultiLineText } from "@/rendering/text";
import { loadGoogleFont } from "@/services/font-service";
@@ -63,7 +63,7 @@ export const renderEffectsForLayer = async ( layer, useCaching = true ) => {
++renderState.pending;
// if source is rotated, calculate the width and height for the current rotation
let { width, height } = getRotatedSize( layer, effects.rotation, true );
let { width, height } = rotateRectangle( layer, effects.rotation, true );
const { cvs, ctx } = createCanvas( width, height );
const cached = useCaching ? getLayerCache( layer ) : null;
@@ -89,7 +89,7 @@ export const renderEffectsForLayer = async ( layer, useCaching = true ) => {
hasCachedFilter = false; // new contents need to be refiltered
}
// update dimensions as text shrinks/expands to fit
({ width, height } = getRotatedSize( textData, effects.rotation, true ));
({ width, height } = rotateRectangle( textData, effects.rotation, true ));
matchDimensions({ width, height }, cvs );
// render text onto destination source
ctx.drawImage( textData, 0, 0 );

View File

@@ -26,7 +26,7 @@ import { renderEffectsForLayer } from "@/services/render-service";
import { getSpriteForLayer } from "@/factories/sprite-factory";
import { createCanvas } from "@/utils/canvas-util";
import { createInverseClipping } from "@/rendering/clipping";
import { areEqual } from "@/math/rectangle-math";
import { rotateRectangle, areEqual } from "@/math/rectangle-math";
import { getRectangleForSelection, isSelectionRectangular } from "@/math/selection-math";
/**
@@ -143,7 +143,9 @@ export const deleteSelectionContent = ( activeDocument, activeLayer ) => {
export const getAlignableObjects = ( document, excludeLayer ) => {
// create a rectangle describing the document boundaries
const documentBounds = { x: 0, y: 0, width: document.width, height: document.height, visible: true };
const documentBounds = {
x: 0, y: 0, width: document.width, height: document.height, visible: true
};
// create bounding boxes for all eligible objects
return [ documentBounds, ...document.layers ].reduce(( acc, object ) => {
// ignore this object in case
@@ -155,8 +157,7 @@ export const getAlignableObjects = ( document, excludeLayer ) => {
( excludeLayer && object.id === excludeLayer.id )) {
return acc;
}
// TODO take rotation into account ?
const { x, y, width, height } = object;
const { x, y, width, height } = rotateRectangle( object, object.effects?.rotation );
// 1. vertical top, center and bottom
let guideWidth = document.width, guideHeight = 0;
if ( y > 0 ) {

View File

@@ -1,18 +1,6 @@
import {
fastRound, scaleToRatio, constrain, isPortrait, isLandscape, isSquare, scaleRectangle
} from "@/math/image-math";
import { scaleToRatio, constrain, isPortrait, isLandscape, isSquare } from "@/math/image-math";
describe( "Image math utilities", () => {
describe( "When rounding numbers", () => {
it( "should be able to round positive numbers", () => {
expect( fastRound( 13.67689767 )).toEqual( 14 );
});
it( "should be able to round negative numbers", () => {
expect( fastRound( -12.534523 )).toEqual( -12 );
});
});
describe( "When constrainting an image to the maximum supported size in megapixels", () => {
it ( "should not adjust the dimensions of images below this threshold", () => {
expect( constrain( 7999, 7999, 8000 * 8000 )).toEqual({ width: 7999, height: 7999 });
@@ -58,36 +46,4 @@ describe( "Image math utilities", () => {
expect( isSquare( SQUARE.width, SQUARE.height )).toBe( true );
});
});
describe( "when scaling rectangles", () => {
it( "should be able to scale a rectangle and maintain its relative offset when scaling up", () => {
const rectangle = {
left: 50,
top: 50,
width: 200,
height: 400
};
expect( scaleRectangle( rectangle, 2 )).toEqual({
left: -50,
top: -150,
width: 400,
height: 800
});
});
it( "should be able to scale a rectangle and maintain its relative offset when scaling down", () => {
const rectangle = {
left: -50,
top: -150,
width: 400,
height: 800
};
expect( scaleRectangle( rectangle, 0.5 )).toEqual({
left: 50,
top: 50,
width: 200,
height: 400
});
});
});
});

View File

@@ -1,4 +1,4 @@
import { areEqual } from "@/math/rectangle-math";
import { areEqual, scaleRectangle } from "@/math/rectangle-math";
describe( "Rectangle math utilities", () => {
describe( "When determining the equality of two given rectangles", () => {
@@ -40,4 +40,36 @@ describe( "Rectangle math utilities", () => {
expect( areEqual( rect1, rect2 )).toBe( true );
});
});
describe( "when scaling rectangles", () => {
it( "should be able to scale a rectangle and maintain its relative offset when scaling up", () => {
const rectangle = {
left: 50,
top: 50,
width: 200,
height: 400
};
expect( scaleRectangle( rectangle, 2 )).toEqual({
left: -50,
top: -150,
width: 400,
height: 800
});
});
it( "should be able to scale a rectangle and maintain its relative offset when scaling down", () => {
const rectangle = {
left: -50,
top: -150,
width: 400,
height: 800
};
expect( scaleRectangle( rectangle, 0.5 )).toEqual({
left: 50,
top: 50,
width: 200,
height: 400
});
});
});
});

View File

@@ -0,0 +1,13 @@
import { fastRound } from "@/math/unit-math";
describe( "Unit math utilities", () => {
describe( "When rounding numbers", () => {
it( "should be able to round positive numbers", () => {
expect( fastRound( 13.67689767 )).toEqual( 14 );
});
it( "should be able to round negative numbers", () => {
expect( fastRound( -12.534523 )).toEqual( -12 );
});
});
});