-
Notifications
You must be signed in to change notification settings - Fork 2.6k
feat(ui): Raster Layer Color Adjusters #8420
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
82f3992
8259268
47ccd33
eb5648a
0ec08f0
8a5906b
1df81e9
dba956b
ea2f953
613c915
f39fbde
23e5dca
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,230 @@ | ||||||
import { | ||||||
Button, | ||||||
ButtonGroup, | ||||||
CompositeNumberInput, | ||||||
CompositeSlider, | ||||||
Flex, | ||||||
FormControl, | ||||||
FormLabel, | ||||||
IconButton, | ||||||
Switch, | ||||||
Text, | ||||||
} from '@invoke-ai/ui-library'; | ||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; | ||||||
import { RasterLayerCurvesEditor } from 'features/controlLayers/components/RasterLayer/RasterLayerCurvesEditor'; | ||||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; | ||||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; | ||||||
import { | ||||||
rasterLayerAdjustmentsCurvesUpdated, | ||||||
rasterLayerAdjustmentsSet, | ||||||
rasterLayerAdjustmentsSimpleUpdated, | ||||||
} from 'features/controlLayers/store/canvasSlice'; | ||||||
import { selectEntity } from 'features/controlLayers/store/selectors'; | ||||||
import React, { memo, useCallback, useMemo } from 'react'; | ||||||
import { useTranslation } from 'react-i18next'; | ||||||
import { PiCaretDownBold } from 'react-icons/pi'; | ||||||
|
||||||
export const RasterLayerAdjustmentsPanel = memo(() => { | ||||||
const dispatch = useAppDispatch(); | ||||||
const entityIdentifier = useEntityIdentifierContext<'raster_layer'>(); | ||||||
const canvasManager = useCanvasManager(); | ||||||
const layer = useAppSelector((s) => selectEntity(s.canvas.present, entityIdentifier)); | ||||||
const { t } = useTranslation(); | ||||||
|
||||||
const hasAdjustments = Boolean(layer?.adjustments); | ||||||
const enabled = Boolean(layer?.adjustments?.enabled); | ||||||
const collapsed = Boolean(layer?.adjustments?.collapsed); | ||||||
const mode = layer?.adjustments?.mode ?? 'simple'; | ||||||
const simple = layer?.adjustments?.simple ?? { | ||||||
brightness: 0, | ||||||
contrast: 0, | ||||||
saturation: 0, | ||||||
temperature: 0, | ||||||
tint: 0, | ||||||
sharpness: 0, | ||||||
}; | ||||||
|
||||||
const onToggleEnabled = useCallback( | ||||||
(v: boolean) => { | ||||||
// Only toggle the enabled state; preserve current mode/collapsed so users can A/B compare | ||||||
dispatch(rasterLayerAdjustmentsSet({ entityIdentifier, adjustments: { enabled: v } })); | ||||||
}, | ||||||
[dispatch, entityIdentifier] | ||||||
); | ||||||
Comment on lines
+47
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this logic and be merged in to |
||||||
|
||||||
const onReset = useCallback(() => { | ||||||
// Reset values to defaults but keep adjustments present; preserve enabled/collapsed/mode | ||||||
dispatch( | ||||||
rasterLayerAdjustmentsSimpleUpdated({ | ||||||
entityIdentifier, | ||||||
simple: { | ||||||
brightness: 0, | ||||||
contrast: 0, | ||||||
saturation: 0, | ||||||
temperature: 0, | ||||||
tint: 0, | ||||||
sharpness: 0, | ||||||
}, | ||||||
}) | ||||||
); | ||||||
const defaultPoints: Array<[number, number]> = [ | ||||||
[0, 0], | ||||||
[255, 255], | ||||||
]; | ||||||
dispatch(rasterLayerAdjustmentsCurvesUpdated({ entityIdentifier, channel: 'master', points: defaultPoints })); | ||||||
dispatch(rasterLayerAdjustmentsCurvesUpdated({ entityIdentifier, channel: 'r', points: defaultPoints })); | ||||||
dispatch(rasterLayerAdjustmentsCurvesUpdated({ entityIdentifier, channel: 'g', points: defaultPoints })); | ||||||
dispatch(rasterLayerAdjustmentsCurvesUpdated({ entityIdentifier, channel: 'b', points: defaultPoints })); | ||||||
}, [dispatch, entityIdentifier]); | ||||||
|
||||||
const onToggleCollapsed = useCallback(() => { | ||||||
dispatch( | ||||||
rasterLayerAdjustmentsSet({ | ||||||
entityIdentifier, | ||||||
adjustments: { collapsed: !collapsed }, | ||||||
}) | ||||||
); | ||||||
}, [dispatch, entityIdentifier, collapsed]); | ||||||
|
||||||
const onSetMode = useCallback( | ||||||
(nextMode: 'simple' | 'curves') => { | ||||||
if (!layer?.adjustments) { | ||||||
return; | ||||||
} | ||||||
if (nextMode === mode) { | ||||||
return; | ||||||
} | ||||||
dispatch( | ||||||
rasterLayerAdjustmentsSet({ | ||||||
entityIdentifier, | ||||||
adjustments: { mode: nextMode }, | ||||||
}) | ||||||
); | ||||||
}, | ||||||
[dispatch, entityIdentifier, layer?.adjustments, mode] | ||||||
); | ||||||
|
||||||
// Memoized click handlers to avoid inline arrow functions in JSX | ||||||
const onClickModeSimple = useCallback(() => onSetMode('simple'), [onSetMode]); | ||||||
const onClickModeCurves = useCallback(() => onSetMode('curves'), [onSetMode]); | ||||||
|
||||||
const slider = useMemo( | ||||||
() => | ||||||
({ | ||||||
row: (label: string, value: number, onChange: (v: number) => void, min = -1, max = 1, step = 0.01) => ( | ||||||
<FormControl pr={2}> | ||||||
<Flex alignItems="center" gap={3} mb={1}> | ||||||
<FormLabel m={0} flexShrink={0} minW="90px"> | ||||||
{label} | ||||||
</FormLabel> | ||||||
<CompositeNumberInput value={value} onChange={onChange} min={min} max={max} step={step} flex="0 0 96px" /> | ||||||
</Flex> | ||||||
<CompositeSlider value={value} onChange={onChange} min={min} max={max} step={step} marks /> | ||||||
Comment on lines
+120
to
+122
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shoudl add |
||||||
</FormControl> | ||||||
), | ||||||
}) as const, | ||||||
[] | ||||||
); | ||||||
Comment on lines
+111
to
+127
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should just be a separate component not a memoized object |
||||||
|
||||||
const onBrightness = useCallback( | ||||||
(v: number) => dispatch(rasterLayerAdjustmentsSimpleUpdated({ entityIdentifier, simple: { brightness: v } })), | ||||||
[dispatch, entityIdentifier] | ||||||
); | ||||||
const onContrast = useCallback( | ||||||
(v: number) => dispatch(rasterLayerAdjustmentsSimpleUpdated({ entityIdentifier, simple: { contrast: v } })), | ||||||
[dispatch, entityIdentifier] | ||||||
); | ||||||
const onSaturation = useCallback( | ||||||
(v: number) => dispatch(rasterLayerAdjustmentsSimpleUpdated({ entityIdentifier, simple: { saturation: v } })), | ||||||
[dispatch, entityIdentifier] | ||||||
); | ||||||
const onTemperature = useCallback( | ||||||
(v: number) => dispatch(rasterLayerAdjustmentsSimpleUpdated({ entityIdentifier, simple: { temperature: v } })), | ||||||
[dispatch, entityIdentifier] | ||||||
); | ||||||
const onTint = useCallback( | ||||||
(v: number) => dispatch(rasterLayerAdjustmentsSimpleUpdated({ entityIdentifier, simple: { tint: v } })), | ||||||
[dispatch, entityIdentifier] | ||||||
); | ||||||
const onSharpness = useCallback( | ||||||
(v: number) => dispatch(rasterLayerAdjustmentsSimpleUpdated({ entityIdentifier, simple: { sharpness: v } })), | ||||||
[dispatch, entityIdentifier] | ||||||
); | ||||||
|
||||||
const handleToggleEnabled = useCallback( | ||||||
(e: React.ChangeEvent<HTMLInputElement>) => onToggleEnabled(e.target.checked), | ||||||
[onToggleEnabled] | ||||||
); | ||||||
|
||||||
const onFinish = useCallback(async () => { | ||||||
// Bake current visual into layer pixels, then clear adjustments | ||||||
const adapter = canvasManager.getAdapter(entityIdentifier); | ||||||
if (!adapter || adapter.type !== 'raster_layer_adapter') { | ||||||
return; | ||||||
} | ||||||
const rect = adapter.transformer.getRelativeRect(); | ||||||
try { | ||||||
await adapter.renderer.rasterize({ rect, replaceObjects: true }); | ||||||
// Clear adjustments after baking | ||||||
dispatch(rasterLayerAdjustmentsSet({ entityIdentifier, adjustments: null })); | ||||||
} catch { | ||||||
// no-op; leave state unchanged on failure | ||||||
} | ||||||
}, [canvasManager, entityIdentifier, dispatch]); | ||||||
|
||||||
// Hide the panel entirely until adjustments are added via context menu | ||||||
if (!hasAdjustments) { | ||||||
return null; | ||||||
} | ||||||
|
||||||
return ( | ||||||
<> | ||||||
<Flex alignItems="center" gap={3} mt={2} mb={2}> | ||||||
<IconButton | ||||||
aria-label={collapsed ? t('controlLayers.adjustments.expand') : t('controlLayers.adjustments.collapse')} | ||||||
size="sm" | ||||||
variant="ghost" | ||||||
onClick={onToggleCollapsed} | ||||||
icon={ | ||||||
<PiCaretDownBold | ||||||
style={{ transform: collapsed ? 'rotate(-90deg)' : 'rotate(0deg)', transition: 'transform 0.2s' }} | ||||||
/> | ||||||
} | ||||||
/> | ||||||
<Text fontWeight={600} flex={1}> | ||||||
Adjustments | ||||||
</Text> | ||||||
<ButtonGroup size="sm" isAttached variant="outline"> | ||||||
<Button onClick={onClickModeSimple} isActive={mode === 'simple'}> | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
{t('controlLayers.adjustments.simple')} | ||||||
</Button> | ||||||
<Button onClick={onClickModeCurves} isActive={mode === 'curves'}> | ||||||
{t('controlLayers.adjustments.curves')} | ||||||
</Button> | ||||||
</ButtonGroup> | ||||||
<Switch isChecked={enabled} onChange={handleToggleEnabled} /> | ||||||
<Button size="sm" onClick={onReset} isDisabled={!layer?.adjustments} colorScheme="red"> | ||||||
{t('controlLayers.adjustments.reset')} | ||||||
</Button> | ||||||
<Button size="sm" onClick={onFinish} isDisabled={!layer?.adjustments} colorScheme="green"> | ||||||
{t('controlLayers.adjustments.finish')} | ||||||
</Button> | ||||||
</Flex> | ||||||
|
||||||
{!collapsed && mode === 'simple' && ( | ||||||
<> | ||||||
{slider.row(t('controlLayers.adjustments.brightness'), simple.brightness, onBrightness)} | ||||||
{slider.row(t('controlLayers.adjustments.contrast'), simple.contrast, onContrast)} | ||||||
{slider.row(t('controlLayers.adjustments.saturation'), simple.saturation, onSaturation)} | ||||||
{slider.row(t('controlLayers.adjustments.temperature'), simple.temperature, onTemperature)} | ||||||
{slider.row(t('controlLayers.adjustments.tint'), simple.tint, onTint)} | ||||||
{slider.row(t('controlLayers.adjustments.sharpness'), simple.sharpness, onSharpness, 0, 1, 0.01)} | ||||||
</> | ||||||
)} | ||||||
|
||||||
{!collapsed && mode === 'curves' && <RasterLayerCurvesEditor />} | ||||||
</> | ||||||
); | ||||||
}); | ||||||
|
||||||
RasterLayerAdjustmentsPanel.displayName = 'RasterLayerAdjustmentsPanel'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggest splitting this component into 3 components: