Added initial layer rotation

This commit is contained in:
Igor Zinken
2020-12-23 20:40:41 +01:00
parent b8f25cefa8
commit 31593b9d27
12 changed files with 154 additions and 10 deletions

View File

@@ -55,8 +55,10 @@ npm run lint
# TODO / Roadmap
* Fix bug that occurs when dragging a rotated layer
* Eraser tool
* Fill selection with color (make color not unique to brush, but generic tool prop)
* Animate selection lines between white and black colors, scale lines to always appear at same size, regardless of zoom level
* When resizing document, positioned layers do not scale their position correctly
* Layer bitmaps and masks must not be stored as Vue observables
* Make canvas only as large as its wrapper. Use viewport in zCanvas to render visible content (requires custom scroll handlers for panning)
@@ -66,7 +68,7 @@ npm run lint
* Zoom set original size isn't that accurate (check also on mobile views), needs calculateMaxScaling ?
* Unload Blobs when images are no longer used in document (see sprite-factory disposeSprite, keep instance count of usages)
* Load/save documents directly from/to Dropbox
* Implement layer rotation
* Load/save selections (into document)
* Implement layer scaling
* Implement rectangular selection
* Implement merged layer selection

View File

@@ -0,0 +1,5 @@
{
"en-US": {
"rotation": "Rotation"
}
}

View File

@@ -0,0 +1,80 @@
/**
* The MIT License (MIT)
*
* Igor Zinken 2020 - https://www.igorski.nl
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* 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.
*/
<template>
<div class="tool-option">
<h3 v-t="'rotation'"></h3>
<slider
v-model="rotation"
:min="min"
:max="max"
:tooltip="'none'"
/>
</div>
</template>
<script>
import { mapGetters, mapMutations } from "vuex";
import ToolTypes, { MIN_ZOOM, MAX_ZOOM } from "@/definitions/tool-types";
import Slider from "@/components/ui/slider/slider";
import messages from "./messages.json";
const DEGREES_TO_RADIANS = Math.PI / 180;
export default {
i18n: { messages },
components: {
Slider,
},
data: () => ({
min: 0,
max: 360,
}),
computed: {
...mapGetters([
"activeLayer",
"activeLayerIndex",
]),
// note rotation is stored in radians but represented visually as degrees
rotation: {
get() {
return this.activeLayer.rotation / DEGREES_TO_RADIANS;
},
set( value ) {
this.updateLayer({
index: this.activeLayerIndex,
opts: { rotation: ( value % 360 ) * DEGREES_TO_RADIANS }
});
}
}
},
methods: {
...mapMutations([
"updateLayer",
]),
},
};
</script>
<style lang="scss" scoped>
@import "@/styles/tool-option";
</style>

View File

@@ -60,7 +60,8 @@
import { mapState, mapGetters, mapMutations } from "vuex";
import FileSelector from "./components/file-selector/file-selector";
import Layers from "./components/layers/layers";
import SelectBox from '@/components/ui/select-box/select-box';
import SelectBox from "@/components/ui/select-box/select-box";
import ToolTypes from "@/definitions/tool-types";
import { mapSelectOptions } from "@/utils/search-select-util"
import messages from "./messages.json";
@@ -117,10 +118,12 @@ export default {
switch ( this.activeTool ) {
default:
return null;
case "zoom":
case ToolTypes.ZOOM:
return () => import( "./components/tool-options-zoom/tool-options-zoom" );
case "brush":
case ToolTypes.BRUSH:
return () => import( "./components/tool-options-brush/tool-options-brush" );
case ToolTypes.ROTATE:
return () => import( "./components/tool-options-rotate/tool-options-rotate" );
}
},
},

View File

@@ -4,6 +4,7 @@
"move": "Move",
"polygonalLasso": "Polygonal lasso",
"eyedropper": "Eyedropper",
"rotateLayer": "Rotate layer",
"zoom": "Zoom",
"brush": "Brush",
"color": "Color"

View File

@@ -105,6 +105,11 @@ export default {
i18n: "eyedropper", icon: "eyedropper",
disabled: !this.activeLayer
},
{
type: ToolTypes.ROTATE,
i18n: "rotateLayer", icon: "rotate",
disabled: !this.activeLayer
},
{
type: ToolTypes.BRUSH,
i18n: "brush", icon: "paintbrush",

View File

@@ -24,7 +24,7 @@ import Vue from "vue";
import { sprite } from "zcanvas";
import { createCanvas, resizeImage, globalToLocal } from "@/utils/canvas-util";
import { LAYER_GRAPHIC, LAYER_MASK } from "@/definitions/layer-types";
import { isPointInRange } from "@/utils/image-math";
import { isPointInRange, translatePointerRotation, getRotationCenter } from "@/utils/image-math";
import ToolTypes from "@/definitions/tool-types";
/**
@@ -197,6 +197,13 @@ class LayerSprite extends sprite {
this._pointerX = x;
this._pointerY = y;
// for rotated content we must translate the pointer coordinates to the unrotated position
if ( this.layer.rotation ) {
// TODO: this is going all sorts of wrong.
const { tX, tY } = getRotationCenter( this._bounds );
({ x, y } = translatePointerRotation( x, y, tX, tY, this.layer.rotation ));
}
if ( !this._isBrushMode ) {
// not drawable, perform default behaviour (drag)
if ( this.actionTarget === "mask" ) {
@@ -253,6 +260,13 @@ class LayerSprite extends sprite {
}
draw( documentContext ) {
if ( this.layer.rotation ) {
documentContext.save();
const { tX, tY } = getRotationCenter( this._bounds );
documentContext.translate( tX, tY );
documentContext.rotate( this.layer.rotation );
documentContext.translate( -tX, -tY );
}
if ( !this.isMaskable() ) {
// use base draw() logic when no mask is set
super.draw( documentContext );
@@ -277,6 +291,9 @@ class LayerSprite extends sprite {
( .5 + height ) << 0
);
}
if ( this.layer.rotation ) {
documentContext.restore();
}
// render brush outline at pointer position
if ( this._isBrushMode ) {
documentContext.save();

View File

@@ -25,10 +25,12 @@ import { MAX_IMAGE_SIZE, MAX_MEGAPIXEL } from "@/definitions/image-types";
import { LAYER_GRAPHIC } from "@/definitions/layer-types";
export default {
MOVE : "move",
ZOOM : "zoom",
BRUSH : "brush",
LASSO : "lasso"
MOVE : "move",
LASSO : "lasso",
EYEDROPPER : "eyedropper",
ROTATE : "rotate",
BRUSH : "brush",
ZOOM : "zoom",
};
export const canUseBrush = ( activeDocument, activeLayer ) => {

View File

@@ -31,7 +31,7 @@ const LayerFactory = {
create({
name = "New Layer",
type = LAYER_GRAPHIC, bitmap = null, mask = null,
x = 0, y = 0, maskX = 0, maskY = 0, width = 1, height = 1, visible = true
x = 0, y = 0, maskX = 0, maskY = 0, width = 1, height = 1, rotation = 0, visible = true
} = {}) {
return {
id: `layer_${( ++UID_COUNTER )}`,
@@ -45,6 +45,7 @@ const LayerFactory = {
maskY,
width,
height,
rotation,
visible,
selection: null, // only used at runtime, will not be serialized
}
@@ -66,6 +67,7 @@ const LayerFactory = {
y2: layer.maskY,
w: layer.width,
h: layer.height,
r: layer.rotation,
v: layer.visible,
};
},
@@ -88,6 +90,7 @@ const LayerFactory = {
maskY: layer.y2,
width: layer.w,
height: layer.h,
rotation: layer.r,
visible: layer.v
});
}

View File

@@ -229,6 +229,10 @@ function handleKeyDown( event ) {
}
break;
case 82: // R
setActiveTool( ToolTypes.ROTATE );
break;
case 83: // S
if ( hasOption ) {
// ...

View File

@@ -105,3 +105,21 @@ export const getRectangleForSelection = ( selection ) => {
height : maxY - minY
};
};
export const translatePointerRotation = ( x, y, rotationCenterX, rotationCenterY, angleInRadians ) => {
const x2 = x - rotationCenterX;
const y2 = y - rotationCenterY;
const cos = Math.cos( -angleInRadians );
const sin = Math.sin( -angleInRadians );
return {
x : x2 * cos - y2 * sin + rotationCenterX,
y : x2 * sin + y2 * cos + rotationCenterY
};
};
export const getRotationCenter = ({ left, top, width, height }) => ({
tX : left + width * .5,
tY : top + height * .5
});

View File

@@ -23,6 +23,7 @@ describe( "Layer factory", () => {
maskY: 0,
width: 1,
height: 1,
rotation: 0,
visible: true,
selection: null,
});
@@ -40,6 +41,7 @@ describe( "Layer factory", () => {
maskY: 25,
width: 16,
height: 9,
rotation: 270,
visible: false,
});
expect( layer ).toEqual({
@@ -54,6 +56,7 @@ describe( "Layer factory", () => {
maskY: 25,
width: 16,
height: 9,
rotation: 270,
visible: false,
selection: null,
})
@@ -70,6 +73,7 @@ describe( "Layer factory", () => {
y: 50,
width: 16,
height: 9,
rotation: -90,
visible: false,
});