mirror of
https://github.com/igorski/bitmappery.git
synced 2026-06-16 19:25:38 +02:00
Add feather and threshold control to smart fill tool
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
"en-US": {
|
||||
"fill": "Fill",
|
||||
"smartFill": "Use smart fill",
|
||||
"smartFillExpl": "When using smart fill, the area surrounding the fill origin will be filled until a boundary has been recognized."
|
||||
"smartFillExpl": "When using smart fill, the area surrounding the fill origin will be filled until a boundary has been recognized.",
|
||||
"feather": "Feather",
|
||||
"threshold": "Threshold"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Igor Zinken 2022 - https://www.igorski.nl
|
||||
* Igor Zinken 2022-2026 - 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
|
||||
@@ -32,18 +32,41 @@
|
||||
/>
|
||||
</div>
|
||||
<p v-t="'smartFillExpl'" class="expl"></p>
|
||||
<div class="wrapper wrapper--slider">
|
||||
<label v-t="'feather'"></label>
|
||||
<slider
|
||||
v-model="feather"
|
||||
:min="0"
|
||||
:max="50"
|
||||
:disabled="disabled || !smartFill"
|
||||
:tooltip="'none'"
|
||||
/>
|
||||
</div>
|
||||
<div class="wrapper wrapper--slider">
|
||||
<label v-t="'threshold'"></label>
|
||||
<slider
|
||||
v-model="threshold"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:disabled="disabled || !smartFill"
|
||||
:tooltip="'none'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { mapGetters, mapMutations } from "vuex";
|
||||
import ToggleButton from "@/components/third-party/vue-js-toggle-button/ToggleButton.vue";
|
||||
import Slider from "@/components/ui/slider/slider.vue";
|
||||
import { type FillToolOptions } from "@/options/editor";
|
||||
import ToolTypes, { canDraw } from "@/definitions/tool-types";
|
||||
import messages from "./messages.json";
|
||||
|
||||
export default {
|
||||
i18n: { messages },
|
||||
components: {
|
||||
Slider,
|
||||
ToggleButton,
|
||||
},
|
||||
computed: {
|
||||
@@ -56,23 +79,42 @@ export default {
|
||||
disabled(): boolean {
|
||||
return !canDraw( this.activeDocument, this.activeLayer, this.activeLayerMask );
|
||||
},
|
||||
feather: {
|
||||
get(): number {
|
||||
return this.fillOptions.feather;
|
||||
},
|
||||
set( value: number ): void {
|
||||
this.update( "feather", value );
|
||||
},
|
||||
},
|
||||
smartFill: {
|
||||
get(): boolean {
|
||||
return this.fillOptions.smartFill;
|
||||
},
|
||||
set( value: boolean ): void {
|
||||
this.setToolOptionValue({
|
||||
tool: ToolTypes.FILL,
|
||||
option: "smartFill",
|
||||
value
|
||||
});
|
||||
}
|
||||
}
|
||||
this.update( "smartFill", value );
|
||||
},
|
||||
},
|
||||
threshold: {
|
||||
get(): number {
|
||||
return this.fillOptions.threshold;
|
||||
},
|
||||
set( value: number ): void {
|
||||
this.update( "threshold", value );
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([
|
||||
"setToolOptionValue",
|
||||
]),
|
||||
update( option: Partial<FillToolOptions>, value: boolean | number ): void {
|
||||
this.setToolOptionValue({
|
||||
tool: ToolTypes.FILL,
|
||||
option,
|
||||
value
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -136,6 +136,8 @@ export type SelectionToolOptions = {
|
||||
|
||||
export type FillToolOptions = {
|
||||
smartFill: boolean;
|
||||
feather: number;
|
||||
threshold: number;
|
||||
};
|
||||
|
||||
export type WandToolOptions = {
|
||||
|
||||
@@ -355,9 +355,10 @@ export default class LayerRenderer extends ZoomableSprite {
|
||||
if ( this.toolOptions.smartFill ) {
|
||||
// we need to translate pointer offset to match the relative, untransformed source layer content
|
||||
const point = rotatePointer( this._pointer, this.layer, width, height );
|
||||
floodFill( ctx, point.x, point.y, color );
|
||||
const { fillOptions } = this.getStore().getters;
|
||||
floodFill( ctx, point.x, point.y, color, fillOptions.feather, fillOptions.threshold );
|
||||
} else {
|
||||
ctx.fillStyle = this.getStore().getters.activeColor;
|
||||
ctx.fillStyle = color;
|
||||
if ( selection ) {
|
||||
ctx.fill();
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Igor Zinken 2022-2023 - https://www.igorski.nl
|
||||
* Igor Zinken 2022-2026 - 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
|
||||
@@ -35,10 +35,12 @@ const TWO_PI = Math.PI * 2;
|
||||
* @param {Number} sourceY y-coordinate of the fill origin
|
||||
* @param {String} fillColor RGBA String value for the fill color
|
||||
* @param {Number=} feather optional amount of pixels at edges to fill (less aliased result)
|
||||
* @param {Number=} threshold optional threshold to apply to the color selection
|
||||
*/
|
||||
export const floodFill = ( ctx: CanvasRenderingContext2D, sourceX: number, sourceY: number,
|
||||
fillColor: string, feather = 5 ): void => {
|
||||
const path = selectByColor( ctx.canvas, sourceX, sourceY );
|
||||
export const floodFill = (
|
||||
ctx: CanvasRenderingContext2D, sourceX: number, sourceY: number, fillColor: string, feather = 5, threshold = 0,
|
||||
): void => {
|
||||
const path = selectByColor( ctx.canvas, sourceX, sourceY, threshold );
|
||||
|
||||
ctx.strokeStyle = fillColor;
|
||||
ctx.fillStyle = fillColor;
|
||||
|
||||
@@ -63,7 +63,7 @@ export const createEditorState = ( props?: Partial<EditorState> ): EditorState =
|
||||
[ ToolTypes.ERASER ]: { size: 10, type: BrushTypes.PAINT_BRUSH, opacity: 1, thickness: .5 },
|
||||
[ ToolTypes.CLONE ] : { size: 10, type: BrushTypes.PAINT_BRUSH, opacity: .75, thickness: .5, sourceLayerId: TOOL_SRC_MERGED, coords: null },
|
||||
[ ToolTypes.SELECTION ] : { lockRatio: false, xRatio: 1, yRatio: 1 },
|
||||
[ ToolTypes.FILL ] : { smartFill: true },
|
||||
[ ToolTypes.FILL ] : { smartFill: true, feather: 5, threshold: 0 },
|
||||
[ ToolTypes.WAND ] : { threshold: 50, sampleMerged: true },
|
||||
},
|
||||
snapAlign : true,
|
||||
|
||||
@@ -17,7 +17,7 @@ describe( "Vuex editor module", () => {
|
||||
[ ToolTypes.ERASER ]: { size: 10, type: BrushTypes.PAINT_BRUSH, opacity: 1, thickness: .5 },
|
||||
[ ToolTypes.CLONE ] : { size: 10, type: BrushTypes.PAINT_BRUSH, opacity: 1, thickness: .5, sourceLayerId: TOOL_SRC_MERGED, coords: { x: 10, y: 15 } },
|
||||
[ ToolTypes.SELECTION ] : { lockRatio: false, xRatio: 1, yRatio: 1 },
|
||||
[ ToolTypes.FILL ] : { smartFill: true },
|
||||
[ ToolTypes.FILL ] : { smartFill: true, feather: 5, threshold: 0 },
|
||||
[ ToolTypes.WAND ] : { threshold: 15, sampleMerged: true },
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user