mirror of
https://github.com/igorski/bitmappery.git
synced 2026-06-17 03:34:56 +02:00
Added initial layer rotation
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"en-US": {
|
||||
"rotation": "Rotation"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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" );
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"move": "Move",
|
||||
"polygonalLasso": "Polygonal lasso",
|
||||
"eyedropper": "Eyedropper",
|
||||
"rotateLayer": "Rotate layer",
|
||||
"zoom": "Zoom",
|
||||
"brush": "Brush",
|
||||
"color": "Color"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 ) => {
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -229,6 +229,10 @@ function handleKeyDown( event ) {
|
||||
}
|
||||
break;
|
||||
|
||||
case 82: // R
|
||||
setActiveTool( ToolTypes.ROTATE );
|
||||
break;
|
||||
|
||||
case 83: // S
|
||||
if ( hasOption ) {
|
||||
// ...
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user