Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion invokeai/frontend/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -3169,7 +3169,8 @@
"isolatedLayerPreview": "Isolated Layer Preview",
"isolatedLayerPreviewDesc": "Whether to show only this layer when performing operations like filtering or transforming.",
"invertBrushSizeScrollDirection": "Invert Scroll for Brush Size",
"pressureSensitivity": "Pressure Sensitivity"
"pressureAffectsWidth": "Pressure Affects Width",
"pressureAffectsBrushOpacity": "Pressure Affects Opacity"
},
"HUD": {
"bbox": "Bbox",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { CanvasSettingsIsolatedStagingPreviewSwitch } from 'features/controlLaye
import { CanvasSettingsLogDebugInfoButton } from 'features/controlLayers/components/Settings/CanvasSettingsLogDebugInfo';
import { CanvasSettingsOutputOnlyMaskedRegionsCheckbox } from 'features/controlLayers/components/Settings/CanvasSettingsOutputOnlyMaskedRegionsCheckbox';
import { CanvasSettingsPreserveMaskCheckbox } from 'features/controlLayers/components/Settings/CanvasSettingsPreserveMaskCheckbox';
import { CanvasSettingsPressureSensitivityCheckbox } from 'features/controlLayers/components/Settings/CanvasSettingsPressureSensitivity';
import { CanvasSettingsPressureOptions } from 'features/controlLayers/components/Settings/CanvasSettingsPressureSensitivity';
import { CanvasSettingsRecalculateRectsButton } from 'features/controlLayers/components/Settings/CanvasSettingsRecalculateRectsButton';
import { CanvasSettingsRuleOfThirdsSwitch } from 'features/controlLayers/components/Settings/CanvasSettingsRuleOfThirdsGuideSwitch';
import { CanvasSettingsSaveAllImagesToGalleryCheckbox } from 'features/controlLayers/components/Settings/CanvasSettingsSaveAllImagesToGalleryCheckbox';
Expand Down Expand Up @@ -60,7 +60,7 @@ export const CanvasSettingsPopover = memo(() => {
</Text>
</Flex>
<CanvasSettingsInvertScrollCheckbox />
<CanvasSettingsPressureSensitivityCheckbox />
<CanvasSettingsPressureOptions />
<CanvasSettingsPreserveMaskCheckbox />
<CanvasSettingsClipToBboxCheckbox />
<CanvasSettingsOutputOnlyMaskedRegionsCheckbox />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,39 @@
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import {
selectPressureSensitivity,
settingsPressureSensitivityToggled,
selectPressureAffectsOpacity,
selectPressureAffectsWidth,
settingsPressureAffectsOpacityToggled,
settingsPressureAffectsWidthToggled,
} from 'features/controlLayers/store/canvasSettingsSlice';
import type { ChangeEventHandler } from 'react';
import { memo, useCallback } from 'react';
import { Fragment, memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';

export const CanvasSettingsPressureSensitivityCheckbox = memo(() => {
export const CanvasSettingsPressureOptions = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const pressureSensitivity = useAppSelector(selectPressureSensitivity);
const onChange = useCallback<ChangeEventHandler<HTMLInputElement>>(() => {
dispatch(settingsPressureSensitivityToggled());
const pressureAffectsWidth = useAppSelector(selectPressureAffectsWidth);
const pressureAffectsOpacity = useAppSelector(selectPressureAffectsOpacity);
const onWidthChange = useCallback<ChangeEventHandler<HTMLInputElement>>(() => {
dispatch(settingsPressureAffectsWidthToggled());
}, [dispatch]);
const onOpacityChange = useCallback<ChangeEventHandler<HTMLInputElement>>(() => {
dispatch(settingsPressureAffectsOpacityToggled());
}, [dispatch]);

return (
<FormControl w="full">
<FormLabel flexGrow={1}>{t('controlLayers.settings.pressureSensitivity')}</FormLabel>
<Checkbox isChecked={pressureSensitivity} onChange={onChange} />
</FormControl>
<Fragment>
<FormControl w="full">
<FormLabel flexGrow={1}>{t('controlLayers.settings.pressureAffectsWidth')}</FormLabel>
<Checkbox isChecked={pressureAffectsWidth} onChange={onWidthChange} />
</FormControl>
<FormControl w="full">
<FormLabel flexGrow={1}>{t('controlLayers.settings.pressureAffectsBrushOpacity')}</FormLabel>
<Checkbox isChecked={pressureAffectsOpacity} onChange={onOpacityChange} />
</FormControl>
</Fragment>
);
});

CanvasSettingsPressureSensitivityCheckbox.displayName = 'CanvasSettingsPressureSensitivityCheckbox';
CanvasSettingsPressureOptions.displayName = 'CanvasSettingsPressureOptions';
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,9 @@ export class CanvasEntityBufferObjectRenderer extends CanvasModuleBase {
}

// Move the buffer to the persistent objects group/renderers
if (this.renderer instanceof CanvasObjectBrushLineWithPressure) {
this.renderer.finalizePressureImage();
}
this.parent.renderer.adoptObjectRenderer(this.renderer);

if (pushToState) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ import type { CanvasEntityBufferObjectRenderer } from 'features/controlLayers/ko
import type { CanvasEntityObjectRenderer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityObjectRenderer';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
import {
appendPressureStrokeRenderOpsToCanvas,
getPressureStrokeRenderOps,
getPressureStrokeRenderOpsFromPointIndex,
type PressureStrokeCanvasTarget,
renderPressureStrokeToCanvas,
} from 'features/controlLayers/konva/pressure';
import { getSVGPathDataFromPoints } from 'features/controlLayers/konva/util';
import type { CanvasBrushLineWithPressureState } from 'features/controlLayers/store/types';
import Konva from 'konva';
Expand All @@ -21,9 +28,15 @@ export class CanvasObjectBrushLineWithPressure extends CanvasModuleBase {
readonly log: Logger;

state: CanvasBrushLineWithPressureState;
pressurePreview: {
target: PressureStrokeCanvasTarget | null;
renderedPointCount: number;
previewKey: string | null;
};
konva: {
group: Konva.Group;
line: Konva.Path;
pressureImage: Konva.Image;
};

constructor(
Expand Down Expand Up @@ -53,26 +66,202 @@ export class CanvasObjectBrushLineWithPressure extends CanvasModuleBase {
globalCompositeOperation: (state.globalCompositeOperation ?? 'source-over') as GlobalCompositeOperation,
perfectDrawEnabled: false,
}),
pressureImage: new Konva.Image({
name: `${this.type}:pressure_image`,
image: document.createElement('canvas'),
listening: false,
visible: false,
globalCompositeOperation: (state.globalCompositeOperation ?? 'source-over') as GlobalCompositeOperation,
perfectDrawEnabled: false,
}),
};
this.konva.group.add(this.konva.line);
this.konva.group.add(this.konva.line, this.konva.pressureImage);
this.state = state;
this.pressurePreview = {
target: null,
renderedPointCount: 0,
previewKey: null,
};
}

getPressurePreviewKey = (state: CanvasBrushLineWithPressureState): string => {
const { id, strokeWidth, color, pressureAffectsWidth, pressureAffectsOpacity, globalCompositeOperation } = state;

return [
id,
strokeWidth,
color.r,
color.g,
color.b,
color.a,
pressureAffectsWidth,
pressureAffectsOpacity,
globalCompositeOperation ?? 'source-over',
].join(':');
};

resetPressurePreview = () => {
this.pressurePreview.target = null;
this.pressurePreview.renderedPointCount = 0;
this.pressurePreview.previewKey = null;
};

syncPressureImage = (arg: { canvas: HTMLCanvasElement; x: number; y: number; globalCompositeOperation?: string }) => {
const { canvas, x, y, globalCompositeOperation } = arg;
this.konva.pressureImage.setAttrs({
image: canvas,
x,
y,
width: canvas.width,
height: canvas.height,
visible: true,
globalCompositeOperation: (globalCompositeOperation ?? 'source-over') as GlobalCompositeOperation,
});
this.konva.pressureImage.getLayer()?.batchDraw();
};

updatePressureImage = () => {
const { points, color, strokeWidth, globalCompositeOperation, pressureAffectsWidth, pressureAffectsOpacity } =
this.state;
const renderOps = getPressureStrokeRenderOps({
points,
strokeWidth,
color,
pressureAffectsWidth,
pressureAffectsOpacity,
});

const rasterizedStroke = renderPressureStrokeToCanvas(renderOps);

if (!rasterizedStroke) {
this.resetPressurePreview();
this.konva.pressureImage.setAttrs({
visible: false,
width: 0,
height: 0,
});
return;
}

this.syncPressureImage({
canvas: rasterizedStroke.canvas,
x: rasterizedStroke.x,
y: rasterizedStroke.y,
globalCompositeOperation,
});
};

updatePressureImagePreview = () => {
const { points, color, strokeWidth, globalCompositeOperation, pressureAffectsWidth, pressureAffectsOpacity } =
this.state;
const pointCount = Math.floor(points.length / 3);
const previewKey = this.getPressurePreviewKey(this.state);
const shouldResetPreview =
this.pressurePreview.target === null ||
this.pressurePreview.previewKey !== previewKey ||
pointCount <= this.pressurePreview.renderedPointCount;

if (shouldResetPreview) {
this.resetPressurePreview();
const renderOps = getPressureStrokeRenderOps({
points,
strokeWidth,
color,
pressureAffectsWidth,
pressureAffectsOpacity,
});
const target = appendPressureStrokeRenderOpsToCanvas(null, renderOps);

if (!target) {
this.konva.pressureImage.setAttrs({
visible: false,
width: 0,
height: 0,
});
return;
}

this.pressurePreview.target = target;
this.pressurePreview.renderedPointCount = pointCount;
this.pressurePreview.previewKey = previewKey;
this.syncPressureImage({
canvas: target.canvas,
x: target.x,
y: target.y,
globalCompositeOperation,
});
return;
}

const incrementalRenderOps = getPressureStrokeRenderOpsFromPointIndex({
points,
strokeWidth,
color,
pressureAffectsWidth,
pressureAffectsOpacity,
startPointIndex: Math.max(0, this.pressurePreview.renderedPointCount - 1),
});
const target = appendPressureStrokeRenderOpsToCanvas(this.pressurePreview.target, incrementalRenderOps);

if (!target) {
this.updatePressureImage();
return;
}

this.pressurePreview.target = target;
this.pressurePreview.renderedPointCount = pointCount;
this.pressurePreview.previewKey = previewKey;
this.syncPressureImage({
canvas: target.canvas,
x: target.x,
y: target.y,
globalCompositeOperation,
});
};

finalizePressureImage = () => {
if (!this.state.pressureAffectsOpacity) {
return;
}

this.resetPressurePreview();
this.updatePressureImage();
};

shouldUseNativePressurePreview = () => this.parent.type === 'buffer_renderer';

update(state: CanvasBrushLineWithPressureState, force = false): boolean {
if (force || this.state !== state) {
this.log.trace({ state }, 'Updating brush line with pressure');
const { points, color, strokeWidth, globalCompositeOperation } = state;
const { points, color, strokeWidth, globalCompositeOperation, pressureAffectsWidth, pressureAffectsOpacity } =
state;
this.konva.line.visible(!pressureAffectsOpacity);
this.konva.pressureImage.visible(pressureAffectsOpacity);
this.konva.line.setAttrs({
globalCompositeOperation: (globalCompositeOperation ?? 'source-over') as GlobalCompositeOperation,
data: getSVGPathDataFromPoints(points, {
size: strokeWidth / 2,
simulatePressure: false,
last: true,
thinning: 1,
thinning: pressureAffectsWidth ? 1 : 0,
}),
fill: rgbaColorToString(color),
});
this.state = state;
if (pressureAffectsOpacity) {
if (this.shouldUseNativePressurePreview()) {
this.updatePressureImagePreview();
} else {
this.updatePressureImage();
}
} else {
this.resetPressurePreview();
this.konva.pressureImage.setAttrs({
visible: false,
width: 0,
height: 0,
});
}
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,13 @@ export class CanvasObjectEraserLineWithPressure extends CanvasModuleBase {
update(state: CanvasEraserLineWithPressureState, force = false): boolean {
if (force || this.state !== state) {
this.log.trace({ state }, 'Updating eraser line with pressure');
const { points, strokeWidth } = state;
const { points, strokeWidth, pressureAffectsWidth } = state;
this.konva.line.setAttrs({
data: getSVGPathDataFromPoints(points, {
size: strokeWidth / 2,
simulatePressure: false,
last: true,
thinning: 1,
thinning: pressureAffectsWidth ? 1 : 0,
}),
});
this.state = state;
Expand Down
Loading
Loading