Add feather and threshold control to smart fill tool

This commit is contained in:
Igor Zinken
2026-04-26 11:46:29 +02:00
parent fc0e708738
commit 6584c86633
7 changed files with 66 additions and 17 deletions

View File

@@ -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"
}
}

View File

@@ -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>

View File

@@ -136,6 +136,8 @@ export type SelectionToolOptions = {
export type FillToolOptions = {
smartFill: boolean;
feather: number;
threshold: number;
};
export type WandToolOptions = {

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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,

View File

@@ -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 },
};