mirror of
https://github.com/igorski/bitmappery.git
synced 2026-06-17 03:34:56 +02:00
Allow drawing operations on scaled layers (#29)
Allow drawing operations on scaled layers
This commit is contained in:
67
README.md
67
README.md
@@ -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:
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 );
|
||||
|
||||
|
||||
@@ -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 ];
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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([
|
||||
|
||||
Reference in New Issue
Block a user