-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Feat(UI): Add linear and radial gradient tools to canvas #8774
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
Merged
lstein
merged 19 commits into
invoke-ai:main
from
DustyShoe:Feat(UI)/Canvas/Add_gradient_tool
Feb 3, 2026
Merged
Changes from 7 commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
878cb33
Adding gradient tool to canvas. Lineara and radial.
DustyShoe 40a2cc8
Formatting again...
DustyShoe b66cf61
Formatting again 2...
DustyShoe 61f0381
Minor bug fix
DustyShoe 11caa0d
Some button design tweaking
DustyShoe 6c3251b
Merge branch 'main' into Feat(UI)/Canvas/Add_gradient_tool
DustyShoe 2bf0fe9
Fixed icorrect wording where Circular was used instead of Radial.
DustyShoe 236cd12
Update invokeai/frontend/web/src/features/controlLayers/konva/CanvasO…
DustyShoe ebba9b6
Update invokeai/frontend/web/src/features/controlLayers/components/To…
DustyShoe 95def55
Update invokeai/frontend/web/src/features/controlLayers/components/To…
DustyShoe 63f3218
Update invokeai/frontend/web/src/features/controlLayers/components/To…
DustyShoe 6fcbd25
Autocommit fix on mouse leaving canvas area
DustyShoe e035155
Merge branch 'main' into Feat(UI)/Canvas/Add_gradient_tool
DustyShoe 7cdde45
Merge branch 'main' into Feat(UI)/Canvas/Add_gradient_tool
DustyShoe 2d6dc80
Merge branch 'main' into Feat(UI)/Canvas/Add_gradient_tool
dunkeroni 829c294
feature(canvas): move gradient mode controls to top toolbar; remove p…
dunkeroni 4df2f92
(chore) prettier
dunkeroni 46481f1
remove fixed icon size
dunkeroni fc8a3ef
Merge branch 'main' into Feat(UI)/Canvas/Add_gradient_tool
lstein File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
191 changes: 191 additions & 0 deletions
191
invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolGradientButton.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,191 @@ | ||
| import { | ||
| Box, | ||
| ButtonGroup, | ||
| IconButton, | ||
| Popover, | ||
| PopoverArrow, | ||
| PopoverBody, | ||
| PopoverContent, | ||
| PopoverTrigger, | ||
| Portal, | ||
| Tooltip, | ||
| useDisclosure, | ||
| } from '@invoke-ai/ui-library'; | ||
| import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; | ||
| import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks'; | ||
| import { selectGradientType, settingsGradientTypeChanged } from 'features/controlLayers/store/canvasSettingsSlice'; | ||
| import { memo, useCallback, useEffect, useId } from 'react'; | ||
| import { useTranslation } from 'react-i18next'; | ||
|
|
||
| const GradientToolIcon = memo(() => { | ||
| const id = useId(); | ||
| const gradientId = `${id}-gradient-tool-horizontal`; | ||
| return ( | ||
| <Box as="svg" viewBox="0 0 24 24" boxSize={6} aria-hidden focusable={false} display="block"> | ||
| <defs> | ||
| <linearGradient id={gradientId} x1="0" y1="0.5" x2="1" y2="0.5"> | ||
| <stop offset="0%" stopColor="currentColor" stopOpacity="0.25" /> | ||
| <stop offset="100%" stopColor="currentColor" stopOpacity="0.85" /> | ||
| </linearGradient> | ||
| </defs> | ||
| <rect | ||
| x="3" | ||
| y="6" | ||
| width="18" | ||
| height="12" | ||
| rx="2" | ||
| fill={`url(#${gradientId})`} | ||
| stroke="currentColor" | ||
| strokeOpacity="0.9" | ||
| strokeWidth="1" | ||
| /> | ||
| </Box> | ||
| ); | ||
| }); | ||
| GradientToolIcon.displayName = 'GradientToolIcon'; | ||
|
|
||
| const GradientLinearIcon = memo(() => { | ||
| const id = useId(); | ||
| const gradientId = `${id}-gradient-linear-diagonal`; | ||
| return ( | ||
| <Box as="svg" viewBox="0 0 24 24" boxSize="22px" aria-hidden focusable={false} display="block"> | ||
| <defs> | ||
| <linearGradient id={gradientId} x1="0" y1="1" x2="1" y2="0"> | ||
| <stop offset="0%" stopColor="currentColor" stopOpacity="0.25" /> | ||
|
DustyShoe marked this conversation as resolved.
Outdated
|
||
| <stop offset="100%" stopColor="currentColor" stopOpacity="0.85" /> | ||
| </linearGradient> | ||
| </defs> | ||
| <rect | ||
| x="4" | ||
| y="4" | ||
| width="16" | ||
| height="16" | ||
| rx="2" | ||
| fill={`url(#${gradientId})`} | ||
| stroke="currentColor" | ||
| strokeOpacity="0.9" | ||
| strokeWidth="1" | ||
| /> | ||
| </Box> | ||
| ); | ||
| }); | ||
| GradientLinearIcon.displayName = 'GradientLinearIcon'; | ||
|
|
||
| const GradientRadialIcon = memo(() => { | ||
| const id = useId(); | ||
| const gradientId = `${id}-gradient-radial`; | ||
| return ( | ||
| <Box as="svg" viewBox="0 0 24 24" boxSize="22px" aria-hidden focusable={false} display="block"> | ||
| <defs> | ||
| <radialGradient id={gradientId} cx="0.5" cy="0.5" r="0.5"> | ||
| <stop offset="0%" stopColor="currentColor" stopOpacity="0.25" /> | ||
|
DustyShoe marked this conversation as resolved.
Outdated
|
||
| <stop offset="100%" stopColor="currentColor" stopOpacity="0.85" /> | ||
| </radialGradient> | ||
| </defs> | ||
| <circle | ||
| cx="12" | ||
| cy="12" | ||
| r="8" | ||
| fill={`url(#${gradientId})`} | ||
| stroke="currentColor" | ||
| strokeOpacity="0.9" | ||
| strokeWidth="1" | ||
| /> | ||
| </Box> | ||
| ); | ||
| }); | ||
| GradientRadialIcon.displayName = 'GradientRadialIcon'; | ||
|
|
||
| export const ToolGradientButton = memo(() => { | ||
| const { t } = useTranslation(); | ||
| const isSelected = useToolIsSelected('gradient'); | ||
| const selectGradient = useSelectTool('gradient'); | ||
| const gradientType = useAppSelector(selectGradientType); | ||
| const dispatch = useAppDispatch(); | ||
| const disclosure = useDisclosure(); | ||
|
|
||
| useEffect(() => { | ||
| if (!isSelected) { | ||
| disclosure.onClose(); | ||
| } | ||
| }, [disclosure, isSelected]); | ||
|
|
||
| const handleClick = useCallback(() => { | ||
| selectGradient(); | ||
| if (disclosure.isOpen) { | ||
| disclosure.onClose(); | ||
| } else { | ||
| disclosure.onOpen(); | ||
| } | ||
| }, [disclosure, selectGradient]); | ||
|
|
||
| const setLinear = useCallback(() => { | ||
| dispatch(settingsGradientTypeChanged('linear')); | ||
| }, [dispatch]); | ||
|
|
||
| const setRadial = useCallback(() => { | ||
| dispatch(settingsGradientTypeChanged('radial')); | ||
| }, [dispatch]); | ||
|
|
||
| const gradientLabel = t('controlLayers.tool.gradient', { defaultValue: 'Gradient' }); | ||
| const linearLabel = t('controlLayers.gradient.linear', { defaultValue: t('common.linear', 'Linear') }); | ||
| const radialLabel = t('controlLayers.gradient.radial', { defaultValue: 'Radial' }); | ||
|
|
||
| return ( | ||
| <Popover | ||
| isLazy | ||
| isOpen={disclosure.isOpen} | ||
| onClose={disclosure.onClose} | ||
| closeOnBlur={true} | ||
| closeOnEsc={true} | ||
| placement="right-start" | ||
| gutter={0} | ||
| > | ||
| <PopoverTrigger> | ||
| <Tooltip label={gradientLabel} placement="end"> | ||
| <IconButton | ||
| aria-label={gradientLabel} | ||
| icon={<GradientToolIcon />} | ||
| isActive={isSelected} | ||
| colorScheme={isSelected ? 'invokeBlue' : 'base'} | ||
| variant="solid" | ||
| onClick={handleClick} | ||
| /> | ||
| </Tooltip> | ||
| </PopoverTrigger> | ||
| <Portal> | ||
| <PopoverContent width="auto" minW={0} ms={-20} mt={-7} bg="transparent" border="none" boxShadow="none"> | ||
| <PopoverArrow display="none" /> | ||
| <PopoverBody p={1}> | ||
| <ButtonGroup isAttached size="sm" orientation="vertical"> | ||
| <Tooltip label={linearLabel}> | ||
| <IconButton | ||
| aria-label={linearLabel} | ||
| icon={<GradientLinearIcon />} | ||
| colorScheme={gradientType === 'linear' ? 'invokeBlue' : 'base'} | ||
| variant="solid" | ||
| w="30px" | ||
| h="30px" | ||
| onClick={setLinear} | ||
| /> | ||
| </Tooltip> | ||
| <Tooltip label={radialLabel}> | ||
| <IconButton | ||
| aria-label={radialLabel} | ||
| icon={<GradientRadialIcon />} | ||
| colorScheme={gradientType === 'radial' ? 'invokeBlue' : 'base'} | ||
| variant="solid" | ||
| w="30px" | ||
| h="30px" | ||
| onClick={setRadial} | ||
| /> | ||
| </Tooltip> | ||
| </ButtonGroup> | ||
| </PopoverBody> | ||
| </PopoverContent> | ||
| </Portal> | ||
| </Popover> | ||
| ); | ||
| }); | ||
|
|
||
| ToolGradientButton.displayName = 'ToolGradientButton'; | ||
36 changes: 36 additions & 0 deletions
36
invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolGradientClipToggle.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import { IconButton, Tooltip } from '@invoke-ai/ui-library'; | ||
| import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; | ||
| import { | ||
| selectGradientClipEnabled, | ||
| settingsGradientClipToggled, | ||
| } from 'features/controlLayers/store/canvasSettingsSlice'; | ||
| import { memo, useCallback } from 'react'; | ||
| import { useTranslation } from 'react-i18next'; | ||
| import { PiCropBold } from 'react-icons/pi'; | ||
|
|
||
| export const ToolGradientClipToggle = memo(() => { | ||
| const { t } = useTranslation(); | ||
| const isEnabled = useAppSelector(selectGradientClipEnabled); | ||
| const dispatch = useAppDispatch(); | ||
|
|
||
| const onClick = useCallback(() => { | ||
| dispatch(settingsGradientClipToggled()); | ||
| }, [dispatch]); | ||
|
|
||
| const label = t('controlLayers.gradient.clip', { defaultValue: 'Clip Gradient' }); | ||
|
|
||
| return ( | ||
| <Tooltip label={label}> | ||
| <IconButton | ||
| aria-label={label} | ||
| icon={<PiCropBold size={16} />} | ||
| size="sm" | ||
| variant="solid" | ||
| colorScheme={isEnabled ? 'invokeBlue' : 'base'} | ||
| onClick={onClick} | ||
| /> | ||
| </Tooltip> | ||
| ); | ||
| }); | ||
|
|
||
| ToolGradientClipToggle.displayName = 'ToolGradientClipToggle'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.