Files
InvokeAI/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositionGuideModule.ts
Kent Keirsey 14fbee17a3 Rule of 3rds Composition Guide (#8130)
* Add Rule of 4 composition guide to canvas settings and rendering

Co-authored-by: kent <kent@invoke.ai>

* Rename Rule of 4 Guide to Rule of Thirds in canvas composition guide

Co-authored-by: kent <kent@invoke.ai>

* Updates to comp guide and naming

* Fix reference

* Update translation keys and organize settings.

* revert to previous canvas manager for conflict

* Re-add composition guide.

* Fix lint

* prettier

* feat(ui): improve markup in canvas settings popover

* feat(ui): use brand colors for canvas rule of thirds guide

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
2025-06-27 15:05:34 +10:00

151 lines
4.8 KiB
TypeScript

import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { selectRuleOfThirds } from 'features/controlLayers/store/canvasSettingsSlice';
import { selectBbox } from 'features/controlLayers/store/selectors';
import Konva from 'konva';
import type { Logger } from 'roarr';
/**
* Renders the rule of thirds composition guide overlay on the canvas.
* The guide shows a 3x3 grid within the bounding box to help with composition.
*/
export class CanvasCompositionGuideModule extends CanvasModuleBase {
readonly type = 'composition_guide';
readonly id: string;
readonly path: string[];
readonly parent: CanvasManager;
readonly manager: CanvasManager;
readonly log: Logger;
subscriptions: Set<() => void> = new Set();
/**
* The Konva objects that make up the composition guide:
* - A group to hold all the guide lines
* - Individual line objects for the rule of thirds grid
*/
konva: {
group: Konva.Group;
verticalLine1: Konva.Line;
verticalLine2: Konva.Line;
horizontalLine1: Konva.Line;
horizontalLine2: Konva.Line;
};
constructor(manager: CanvasManager) {
super();
this.id = getPrefixedId(this.type);
this.parent = manager;
this.manager = manager;
this.path = this.manager.buildPath(this);
this.log = this.manager.buildLogger(this);
this.log.debug('Creating composition guide module');
this.konva = {
group: new Konva.Group({
name: `${this.type}:group`,
listening: false,
perfectDrawEnabled: false,
}),
verticalLine1: new Konva.Line({
name: `${this.type}:vertical_line_1`,
listening: false,
stroke: 'hsl(220 12% 90% / 0.9)',
strokeWidth: 1,
strokeScaleEnabled: false,
perfectDrawEnabled: false,
dash: [5, 5],
}),
verticalLine2: new Konva.Line({
name: `${this.type}:vertical_line_2`,
listening: false,
stroke: 'hsl(220 12% 90% / 0.9)',
strokeWidth: 1,
strokeScaleEnabled: false,
perfectDrawEnabled: false,
dash: [5, 5],
}),
horizontalLine1: new Konva.Line({
name: `${this.type}:horizontal_line_1`,
listening: false,
stroke: 'hsl(220 12% 90% / 0.9)',
strokeWidth: 1,
strokeScaleEnabled: false,
perfectDrawEnabled: false,
dash: [5, 5],
}),
horizontalLine2: new Konva.Line({
name: `${this.type}:horizontal_line_2`,
listening: false,
stroke: 'hsl(220 12% 90% / 0.9)',
strokeWidth: 1,
strokeScaleEnabled: false,
perfectDrawEnabled: false,
dash: [5, 5],
}),
};
this.konva.group.add(this.konva.verticalLine1);
this.konva.group.add(this.konva.verticalLine2);
this.konva.group.add(this.konva.horizontalLine1);
this.konva.group.add(this.konva.horizontalLine2);
// Listen for changes to the rule of thirds guide setting
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(selectRuleOfThirds, this.render));
// Listen for changes to the bbox to update guide positioning
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(selectBbox, this.render));
}
initialize = () => {
this.log.debug('Initializing composition guide module');
this.render();
};
/**
* Renders the composition guide. The guide is only visible when the setting is enabled.
*/
render = () => {
const ruleOfThirds = this.manager.stateApi.getSettings().ruleOfThirds;
const { x, y, width, height } = this.manager.stateApi.runSelector(selectBbox).rect;
this.konva.group.visible(ruleOfThirds);
if (!ruleOfThirds) {
return;
}
// Calculate the thirds positions of the bounding box
const oneThirdX = x + width / 3;
const twoThirdsX = x + (2 * width) / 3;
const oneThirdY = y + height / 3;
const twoThirdsY = y + (2 * height) / 3;
// Update the vertical lines (divide the bbox into thirds vertically)
this.konva.verticalLine1.points([oneThirdX, y, oneThirdX, y + height]);
this.konva.verticalLine2.points([twoThirdsX, y, twoThirdsX, y + height]);
// Update the horizontal lines (divide the bbox into thirds horizontally)
this.konva.horizontalLine1.points([x, oneThirdY, x + width, oneThirdY]);
this.konva.horizontalLine2.points([x, twoThirdsY, x + width, twoThirdsY]);
};
destroy = () => {
this.log.debug('Destroying composition guide module');
this.subscriptions.forEach((unsubscribe) => unsubscribe());
this.subscriptions.clear();
this.konva.group.destroy();
};
repr = () => {
return {
id: this.id,
type: this.type,
path: this.path,
visible: this.konva.group.visible(),
};
};
}