mirror of
https://github.com/igorski/bitmappery.git
synced 2026-06-17 03:34:56 +02:00
Snap and align now also works on rotated content. Moved math utilities to appropriate namespaces
This commit is contained in:
@@ -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 },
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 );
|
||||
|
||||
@@ -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 ) {
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
13
tests/unit/math/unit-math.spec.js
Normal file
13
tests/unit/math/unit-math.spec.js
Normal 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 );
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user