Allow drawing operations on scaled layers (#29)

Allow drawing operations on scaled layers
This commit is contained in:
Igor Zinken
2023-04-16 10:58:09 +02:00
committed by GitHub
parent 5d841623d5
commit b47ba5bcd0
9 changed files with 102 additions and 60 deletions

View File

@@ -6,18 +6,18 @@ No, I'm building a tool that does the bare minimum what I require and what I don
find in other open source tools. That doesn't mean of course that contributions
related to Photoshop-esque features aren't welcomed.
## The [Issue Tracker](https://github.com/igorski/bitmappery/issues) is your point of contact
Bug reports, feature requests, questions and discussions are welcome on the GitHub Issue Tracker, please do not send e-mails through the development website. However, please search before posting to avoid duplicates, and limit to one issue per post.
Please vote on feature requests by using the Thumbs Up/Down reaction on the first post.
### All hand-written ?
Yep, though it helps having worked in the photo software industry for five years, having
tackled the problems before. Also, BitMappery is reusing [zCanvas](https://github.com/igorski/zCanvas)
under the hood for rendering and bitmap blitting. BitMappery is written on top of [Vue](https://github.com/vuejs/vue) using [Vuex](https://github.com/vuejs/vuex) and [VueI18n](https://github.com/kazupon/vue-i18n).
## The [Issue Tracker](https://github.com/igorski/bitmappery/issues) is your point of contact
Bug reports, feature requests, questions and discussions are welcome on the GitHub Issue Tracker, please do not send e-mails through the development website. However, please search before posting to avoid duplicates, and limit to one issue per post.
Please vote on feature requests by using the Thumbs Up/Down reaction on the first post.
## Model
BitMappery works with entities known as _Documents_. A Document contains several _Layers_, each of
@@ -113,9 +113,10 @@ Whenever an action (that requires an undo state) can be triggered in multiple lo
inside a component and as a keyboard shortcut in `@/src/services/keyboard-service`), you can
create a custom handler inside `@/src/factories/action-factory` to avoid code duplication.
## Dropbox integration
## Third party storage integration
Requires you to [register a client id or access token](https://www.dropbox.com/developers/apps).
Requires you to register a client id or access token in the developer portal of the third party
storage provider. Currently, there is support for [Dropbox](https://www.dropbox.com/developers/apps) and [Google Drive](https://console.cloud.google.com/).
## Project setup
@@ -134,7 +135,29 @@ after which you can run:
The above will suffice when working solely on the JavaScript side of things.
### WebAssembly
## Docker based self hosted version
#### Step 1 : Clone the BitMappery project into a local folder :
```bash
git clone https://github.com/igorski/bitmappery.git
```
#### Step 2 : Build the image using the provided Dockerfile :
```bash
docker build -t bitmappery .
```
#### Step 3 : Once the image is built, run the container and bind the ports :
```bash
docker run -d -p 5173:5173 --name bitmappery-container bitmappery
```
Once the container is started, you can access BitMappery at `http://localhost:5173`
## WebAssembly
BitMappery can also use WebAssembly to increase performance of image manipulation. The source
code is C based and compiled to WASM using [Emscripten](https://github.com/emscripten-core/emscripten). Because this setup is a little more cumbersome, the repository contains precompiled binaries
@@ -158,31 +181,7 @@ now you can compile all source files to WASM using:
npm run wasm
```
## Docker
### Step 1 : Go into a folder on your local machine and git clone the BitMappery project :
```bash
git clone https://github.com/igorski/bitmappery.git
```
### Step 2 : Build the image using the dockerfile provided :
```bash
docker build -t bitmappery .
```
### Step 3 : Once the image is built, run the container and bind the ports :
```bash
docker run -d -p 5173:5173 --name bitmappery-container bitmappery
```
### Step 4 : Once the container is started, you should be able to reach `http://localhost:5173`
## Benchmarks
### Benchmarks
On a particular (low powered) configuration, running all filters on a particular source takes:

View File

@@ -2,7 +2,6 @@
"en-US": {
"scale": "Scale",
"reset": "Reset",
"save": "Save",
"drawingDisabledUntilSaved": "Scaled layers are not drawable until the scale is made permanent."
"save": "Save"
}
}

View File

@@ -1,7 +1,7 @@
/**
* The MIT License (MIT)
*
* Igor Zinken 2021-2022 - https://www.igorski.nl
* Igor Zinken 2021-2023 - 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
@@ -23,7 +23,6 @@
<template>
<div class="tool-option">
<h3 v-t="'scale'"></h3>
<p v-t="'drawingDisabledUntilSaved'"></p>
<div class="wrapper full slider">
<slider
v-model="scale"

View File

@@ -53,9 +53,7 @@ export const SELECTION_TOOLS = [ ToolTypes.SELECTION, ToolTypes.LASSO, ToolTypes
export const canDraw = ( activeDocument: Document, activeLayer: Layer ): boolean => {
return activeDocument &&
( activeLayer?.mask || activeLayer?.type === LayerTypes.LAYER_GRAPHIC ) &&
// scaled layers should commit their scale before allowing draw operations
( activeLayer.effects.scale === 1 );
( activeLayer?.mask !== null || activeLayer?.type === LayerTypes.LAYER_GRAPHIC );
};
// we cannot draw in selection if a layer is mirrored (see https://github.com/igorski/bitmappery/issues/5)

View File

@@ -374,9 +374,15 @@ class LayerSprite extends ZoomableSprite {
} else {
// render full brush stroke path directly onto the Layer source
ctx = createCanvas( orgContext.canvas.width, orgContext.canvas.height ).ctx;
// take optional layer scaling into account
const scale = 1 / this.layer.effects.scale;
ctx.translate(
( this.layer.width / 2 ) - ( this.layer.width * scale ) / 2,
( this.layer.height / 2 ) - ( this.layer.height * scale ) / 2
);
// transform destination context in case the current layer is rotated or mirrored
ctx.scale( mirrorX ? -1 : 1, mirrorY ? -1 : 1 );
this._brush.pointers = rotatePointers( this._brush.pointers, this.layer, width, height );
this._brush.pointers = rotatePointers( this._brush.pointers, this.layer, width, height ).map(({ x, y }) => ({ x: x * scale, y: y * scale }));
}
renderBrushStroke( ctx, this._brush, overrides );

View File

@@ -20,9 +20,9 @@
* 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 type { Rectangle } from "zcanvas";
import type { Point, Rectangle } from "zcanvas";
import type { Shape, Selection } from "@/definitions/document";
import { shapeToRectangle } from "@/utils/shape-util";
import { shapeToRectangle, scaleShape } from "@/utils/shape-util";
export const selectionToRectangle = ( selection: Selection ): Rectangle => {
if ( selection.length === 1 ) {
@@ -46,6 +46,10 @@ export const selectionToRectangle = ( selection: Selection ): Rectangle => {
};
};
export const scaleSelection = ( selection: Selection, scale: number ): Selection => {
return selection.map(( shape: Shape ) => scaleShape( shape, scale ));
};
export const getLastShape = ( selection: Selection ): Shape => {
return selection[ selection.length - 1 ];
};

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 type { Rectangle } from "zcanvas";
import type { Point, Rectangle } from "zcanvas";
import type { Shape } from "@/definitions/document";
export const shapeToRectangle = ( shape: Shape ): Rectangle => {
@@ -51,6 +51,10 @@ export const rectangleToShape = ( width: number, height: number, x = 0, y = 0 ):
{ x, y }
];
export const scaleShape = ( shape: Shape, scale: number ): Shape => {
return shape.map(( point: Point ) => ({ x: point.x * scale, y: point.y * scale }));
};
export const isShapeRectangular = ( shape: Shape ): boolean => {
if ( shape.length !== 5 ) {
return false;

View File

@@ -1,5 +1,5 @@
import { it, describe, expect } from "vitest";
import { selectionToRectangle, getLastShape } from "@/utils/selection-util";
import { selectionToRectangle, scaleSelection, getLastShape } from "@/utils/selection-util";
describe( "Selection utilities", () => {
const selection = [
@@ -16,22 +16,40 @@ describe( "Selection utilities", () => {
]
];
it( "should be able to calculate the bounding box of a selection with a single Shape", () => {
expect( selectionToRectangle( [ selection[ 1 ]] )).toEqual({
left: 25,
top: 25,
width: 50,
height: 50
describe( "when calculating the bounding box rectangle for a selection", () => {
it( "should be able to calculate the bounding box of a selection with a single Shape", () => {
expect( selectionToRectangle( [ selection[ 1 ]] )).toEqual({
left: 25,
top: 25,
width: 50,
height: 50
});
});
it( "should be able to calculate the bounding box of a selection with multiple Shapes", () => {
expect( selectionToRectangle( selection )).toEqual({
left: 10,
top: 5,
width: 65,
height: 70
});
});
});
it( "should be able to calculate the bounding box of a selection with multiple Shapes", () => {
expect( selectionToRectangle( selection )).toEqual({
left: 10,
top: 5,
width: 65,
height: 70
});
it( "should be able to scale the Shapes inside a selection", () => {
expect( scaleSelection( selection, 1.5 )).toEqual([
[
{ x: 15, y: 7.5 },
{ x: 75, y: 7.5 },
{ x: 75, y: 82.5 },
{ x: 15, y: 82.5 }
], [
{ x: 37.5, y: 37.5 },
{ x: 112.5, y: 37.5 },
{ x: 112.5, y: 112.5 },
{ x: 37.5, y: 112.5 }
]
]);
});
it( "should be able to retrieve the last shape in the selection", () => {

View File

@@ -1,6 +1,6 @@
import { it, describe, expect } from "vitest";
import {
shapeToRectangle, rectangleToShape,
shapeToRectangle, rectangleToShape, scaleShape,
isShapeRectangular, isShapeClosed
} from "@/utils/shape-util";
@@ -30,6 +30,21 @@ describe( "Shape utilities", () => {
]);
});
it( "should be able to scale a Shape by given factor", () => {
const shape = [
{ x: 100, y: 150 },
{ x: 50, y: 899 },
{ x: 50, y: 100 },
{ x: 101, y: 100 }
];
expect( scaleShape( shape, 2 )).toEqual([
{ x: 200, y: 300 },
{ x: 100, y: 1798 },
{ x: 100, y: 200 },
{ x: 202, y: 200 }
]);
});
describe(" when determining whether a shape is rectangular", () => {
it( "should recognize unclosed shapes", () => {
expect( isShapeRectangular([