mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Merges development
This commit is contained in:
parent
fe7ab6e480
commit
e5dcae5fff
@ -5,6 +5,8 @@ import shutil
|
|||||||
import mimetypes
|
import mimetypes
|
||||||
import traceback
|
import traceback
|
||||||
import math
|
import math
|
||||||
|
import io
|
||||||
|
import base64
|
||||||
|
|
||||||
from flask import Flask, redirect, send_from_directory
|
from flask import Flask, redirect, send_from_directory
|
||||||
from flask_socketio import SocketIO
|
from flask_socketio import SocketIO
|
||||||
@ -64,10 +66,7 @@ class InvokeAIWebServer:
|
|||||||
__name__, static_url_path='', static_folder='../frontend/dist/'
|
__name__, static_url_path='', static_folder='../frontend/dist/'
|
||||||
)
|
)
|
||||||
|
|
||||||
self.socketio = SocketIO(
|
self.socketio = SocketIO(self.app, **socketio_args)
|
||||||
self.app,
|
|
||||||
**socketio_args
|
|
||||||
)
|
|
||||||
|
|
||||||
# Keep Server Alive Route
|
# Keep Server Alive Route
|
||||||
@self.app.route('/flaskwebgui-keep-server-alive')
|
@self.app.route('/flaskwebgui-keep-server-alive')
|
||||||
@ -102,6 +101,7 @@ class InvokeAIWebServer:
|
|||||||
close_server_on_exit = False
|
close_server_on_exit = False
|
||||||
try:
|
try:
|
||||||
from flaskwebgui import FlaskUI
|
from flaskwebgui import FlaskUI
|
||||||
|
|
||||||
FlaskUI(
|
FlaskUI(
|
||||||
app=self.app,
|
app=self.app,
|
||||||
socketio=self.socketio,
|
socketio=self.socketio,
|
||||||
@ -186,11 +186,14 @@ class InvokeAIWebServer:
|
|||||||
|
|
||||||
for path in image_paths:
|
for path in image_paths:
|
||||||
metadata = retrieve_metadata(path)
|
metadata = retrieve_metadata(path)
|
||||||
|
(width, height) = Image.open(path).size
|
||||||
image_array.append(
|
image_array.append(
|
||||||
{
|
{
|
||||||
'url': self.get_url_from_image_path(path),
|
'url': self.get_url_from_image_path(path),
|
||||||
'mtime': os.path.getmtime(path),
|
'mtime': os.path.getmtime(path),
|
||||||
'metadata': metadata['sd-metadata'],
|
'metadata': metadata['sd-metadata'],
|
||||||
|
'width': width,
|
||||||
|
'height': height,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -233,11 +236,16 @@ class InvokeAIWebServer:
|
|||||||
|
|
||||||
for path in image_paths:
|
for path in image_paths:
|
||||||
metadata = retrieve_metadata(path)
|
metadata = retrieve_metadata(path)
|
||||||
|
|
||||||
|
(width, height) = Image.open(path).size
|
||||||
|
|
||||||
image_array.append(
|
image_array.append(
|
||||||
{
|
{
|
||||||
'url': self.get_url_from_image_path(path),
|
'url': self.get_url_from_image_path(path),
|
||||||
'mtime': os.path.getmtime(path),
|
'mtime': os.path.getmtime(path),
|
||||||
'metadata': metadata['sd-metadata'],
|
'metadata': metadata['sd-metadata'],
|
||||||
|
'width': width,
|
||||||
|
'height': height,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -260,11 +268,24 @@ class InvokeAIWebServer:
|
|||||||
generation_parameters, esrgan_parameters, facetool_parameters
|
generation_parameters, esrgan_parameters, facetool_parameters
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
print(
|
# truncate long init_mask base64 if needed
|
||||||
f'>> Image generation requested: {generation_parameters}\nESRGAN parameters: {esrgan_parameters}\nFacetool parameters: {facetool_parameters}'
|
if 'init_mask' in generation_parameters:
|
||||||
)
|
printable_parameters = {
|
||||||
|
**generation_parameters,
|
||||||
|
'init_mask': generation_parameters['init_mask'][:20]
|
||||||
|
+ '...',
|
||||||
|
}
|
||||||
|
print(
|
||||||
|
f'>> Image generation requested: {printable_parameters}\nESRGAN parameters: {esrgan_parameters}\nFacetool parameters: {facetool_parameters}'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f'>> Image generation requested: {generation_parameters}\nESRGAN parameters: {esrgan_parameters}\nFacetool parameters: {facetool_parameters}'
|
||||||
|
)
|
||||||
self.generate_images(
|
self.generate_images(
|
||||||
generation_parameters, esrgan_parameters, facetool_parameters
|
generation_parameters,
|
||||||
|
esrgan_parameters,
|
||||||
|
facetool_parameters,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.socketio.emit('error', {'message': (str(e))})
|
self.socketio.emit('error', {'message': (str(e))})
|
||||||
@ -321,16 +342,24 @@ class InvokeAIWebServer:
|
|||||||
elif postprocessing_parameters['type'] == 'gfpgan':
|
elif postprocessing_parameters['type'] == 'gfpgan':
|
||||||
image = self.gfpgan.process(
|
image = self.gfpgan.process(
|
||||||
image=image,
|
image=image,
|
||||||
strength=postprocessing_parameters['facetool_strength'],
|
strength=postprocessing_parameters[
|
||||||
|
'facetool_strength'
|
||||||
|
],
|
||||||
seed=seed,
|
seed=seed,
|
||||||
)
|
)
|
||||||
elif postprocessing_parameters['type'] == 'codeformer':
|
elif postprocessing_parameters['type'] == 'codeformer':
|
||||||
image = self.codeformer.process(
|
image = self.codeformer.process(
|
||||||
image=image,
|
image=image,
|
||||||
strength=postprocessing_parameters['facetool_strength'],
|
strength=postprocessing_parameters[
|
||||||
fidelity=postprocessing_parameters['codeformer_fidelity'],
|
'facetool_strength'
|
||||||
|
],
|
||||||
|
fidelity=postprocessing_parameters[
|
||||||
|
'codeformer_fidelity'
|
||||||
|
],
|
||||||
seed=seed,
|
seed=seed,
|
||||||
device='cpu' if str(self.generate.device) == 'mps' else self.generate.device
|
device='cpu'
|
||||||
|
if str(self.generate.device) == 'mps'
|
||||||
|
else self.generate.device,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
@ -349,6 +378,8 @@ class InvokeAIWebServer:
|
|||||||
|
|
||||||
command = parameters_to_command(postprocessing_parameters)
|
command = parameters_to_command(postprocessing_parameters)
|
||||||
|
|
||||||
|
(width, height) = image.size
|
||||||
|
|
||||||
path = self.save_result_image(
|
path = self.save_result_image(
|
||||||
image,
|
image,
|
||||||
command,
|
command,
|
||||||
@ -371,6 +402,8 @@ class InvokeAIWebServer:
|
|||||||
'url': self.get_url_from_image_path(path),
|
'url': self.get_url_from_image_path(path),
|
||||||
'mtime': os.path.getmtime(path),
|
'mtime': os.path.getmtime(path),
|
||||||
'metadata': metadata,
|
'metadata': metadata,
|
||||||
|
'width': width,
|
||||||
|
'height': height,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -482,19 +515,41 @@ class InvokeAIWebServer:
|
|||||||
|
|
||||||
if 'init_img' in generation_parameters:
|
if 'init_img' in generation_parameters:
|
||||||
init_img_url = generation_parameters['init_img']
|
init_img_url = generation_parameters['init_img']
|
||||||
generation_parameters[
|
init_img_path = self.get_image_path_from_url(init_img_url)
|
||||||
'init_img'
|
generation_parameters['init_img'] = init_img_path
|
||||||
] = self.get_image_path_from_url(
|
|
||||||
generation_parameters['init_img']
|
# if 'init_mask' in generation_parameters:
|
||||||
)
|
# mask_img_url = generation_parameters['init_mask']
|
||||||
|
# generation_parameters[
|
||||||
|
# 'init_mask'
|
||||||
|
# ] = self.get_image_path_from_url(
|
||||||
|
# generation_parameters['init_mask']
|
||||||
|
# )
|
||||||
|
|
||||||
if 'init_mask' in generation_parameters:
|
if 'init_mask' in generation_parameters:
|
||||||
mask_img_url = generation_parameters['init_mask']
|
# grab an Image of the init image
|
||||||
generation_parameters[
|
original_image = Image.open(init_img_path)
|
||||||
'init_mask'
|
|
||||||
] = self.get_image_path_from_url(
|
# copy a region from it which we will inpaint
|
||||||
generation_parameters['init_mask']
|
cropped_init_image = copy_image_from_bounding_box(
|
||||||
|
original_image, **generation_parameters['bounding_box']
|
||||||
)
|
)
|
||||||
|
generation_parameters['init_img'] = cropped_init_image
|
||||||
|
|
||||||
|
# grab an Image of the mask
|
||||||
|
mask_image = Image.open(
|
||||||
|
io.BytesIO(
|
||||||
|
base64.decodebytes(
|
||||||
|
bytes(generation_parameters['init_mask'], 'utf-8')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# crop the mask image
|
||||||
|
cropped_mask_image = copy_image_from_bounding_box(
|
||||||
|
mask_image, **generation_parameters['bounding_box']
|
||||||
|
)
|
||||||
|
generation_parameters['init_mask'] = cropped_mask_image
|
||||||
|
|
||||||
totalSteps = self.calculate_real_steps(
|
totalSteps = self.calculate_real_steps(
|
||||||
steps=generation_parameters['steps'],
|
steps=generation_parameters['steps'],
|
||||||
@ -532,6 +587,8 @@ class InvokeAIWebServer:
|
|||||||
)
|
)
|
||||||
command = parameters_to_command(generation_parameters)
|
command = parameters_to_command(generation_parameters)
|
||||||
|
|
||||||
|
(width, height) = image.size
|
||||||
|
|
||||||
path = self.save_result_image(
|
path = self.save_result_image(
|
||||||
image,
|
image,
|
||||||
command,
|
command,
|
||||||
@ -548,6 +605,8 @@ class InvokeAIWebServer:
|
|||||||
'url': self.get_url_from_image_path(path),
|
'url': self.get_url_from_image_path(path),
|
||||||
'mtime': os.path.getmtime(path),
|
'mtime': os.path.getmtime(path),
|
||||||
'metadata': metadata,
|
'metadata': metadata,
|
||||||
|
'width': width,
|
||||||
|
'height': height
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.socketio.emit(
|
self.socketio.emit(
|
||||||
@ -625,7 +684,9 @@ class InvokeAIWebServer:
|
|||||||
if facetool_parameters['type'] == 'gfpgan':
|
if facetool_parameters['type'] == 'gfpgan':
|
||||||
progress.set_current_status('Restoring Faces (GFPGAN)')
|
progress.set_current_status('Restoring Faces (GFPGAN)')
|
||||||
elif facetool_parameters['type'] == 'codeformer':
|
elif facetool_parameters['type'] == 'codeformer':
|
||||||
progress.set_current_status('Restoring Faces (Codeformer)')
|
progress.set_current_status(
|
||||||
|
'Restoring Faces (Codeformer)'
|
||||||
|
)
|
||||||
|
|
||||||
progress.set_current_status_has_steps(False)
|
progress.set_current_status_has_steps(False)
|
||||||
self.socketio.emit(
|
self.socketio.emit(
|
||||||
@ -643,11 +704,17 @@ class InvokeAIWebServer:
|
|||||||
image = self.codeformer.process(
|
image = self.codeformer.process(
|
||||||
image=image,
|
image=image,
|
||||||
strength=facetool_parameters['strength'],
|
strength=facetool_parameters['strength'],
|
||||||
fidelity=facetool_parameters['codeformer_fidelity'],
|
fidelity=facetool_parameters[
|
||||||
|
'codeformer_fidelity'
|
||||||
|
],
|
||||||
seed=seed,
|
seed=seed,
|
||||||
device='cpu' if str(self.generate.device) == 'mps' else self.generate.device,
|
device='cpu'
|
||||||
|
if str(self.generate.device) == 'mps'
|
||||||
|
else self.generate.device,
|
||||||
)
|
)
|
||||||
all_parameters['codeformer_fidelity'] = facetool_parameters['codeformer_fidelity']
|
all_parameters[
|
||||||
|
'codeformer_fidelity'
|
||||||
|
] = facetool_parameters['codeformer_fidelity']
|
||||||
|
|
||||||
postprocessing = True
|
postprocessing = True
|
||||||
all_parameters['facetool_strength'] = facetool_parameters[
|
all_parameters['facetool_strength'] = facetool_parameters[
|
||||||
@ -663,12 +730,20 @@ class InvokeAIWebServer:
|
|||||||
)
|
)
|
||||||
eventlet.sleep(0)
|
eventlet.sleep(0)
|
||||||
|
|
||||||
|
# paste the inpainting image back onto the original
|
||||||
|
if 'init_mask' in generation_parameters:
|
||||||
|
image = paste_image_into_bounding_box(
|
||||||
|
Image.open(init_img_path),
|
||||||
|
image,
|
||||||
|
**generation_parameters['bounding_box'],
|
||||||
|
)
|
||||||
|
|
||||||
# restore the stashed URLS and discard the paths, we are about to send the result to client
|
# restore the stashed URLS and discard the paths, we are about to send the result to client
|
||||||
if 'init_img' in all_parameters:
|
if 'init_img' in all_parameters:
|
||||||
all_parameters['init_img'] = init_img_url
|
all_parameters['init_img'] = init_img_url
|
||||||
|
|
||||||
if 'init_mask' in all_parameters:
|
if 'init_mask' in all_parameters:
|
||||||
all_parameters['init_mask'] = mask_img_url
|
all_parameters['init_mask'] = '' #
|
||||||
|
|
||||||
metadata = self.parameters_to_generated_image_metadata(
|
metadata = self.parameters_to_generated_image_metadata(
|
||||||
all_parameters
|
all_parameters
|
||||||
@ -676,6 +751,8 @@ class InvokeAIWebServer:
|
|||||||
|
|
||||||
command = parameters_to_command(all_parameters)
|
command = parameters_to_command(all_parameters)
|
||||||
|
|
||||||
|
(width, height) = image.size
|
||||||
|
|
||||||
path = self.save_result_image(
|
path = self.save_result_image(
|
||||||
image,
|
image,
|
||||||
command,
|
command,
|
||||||
@ -705,6 +782,8 @@ class InvokeAIWebServer:
|
|||||||
'url': self.get_url_from_image_path(path),
|
'url': self.get_url_from_image_path(path),
|
||||||
'mtime': os.path.getmtime(path),
|
'mtime': os.path.getmtime(path),
|
||||||
'metadata': metadata,
|
'metadata': metadata,
|
||||||
|
'width': width,
|
||||||
|
'height': height,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
eventlet.sleep(0)
|
eventlet.sleep(0)
|
||||||
@ -766,12 +845,14 @@ class InvokeAIWebServer:
|
|||||||
# 'postprocessing' is either null or an
|
# 'postprocessing' is either null or an
|
||||||
if 'facetool_strength' in parameters:
|
if 'facetool_strength' in parameters:
|
||||||
facetool_parameters = {
|
facetool_parameters = {
|
||||||
'type': str(parameters['facetool_type']),
|
'type': str(parameters['facetool_type']),
|
||||||
'strength': float(parameters['facetool_strength']),
|
'strength': float(parameters['facetool_strength']),
|
||||||
}
|
}
|
||||||
|
|
||||||
if parameters['facetool_type'] == 'codeformer':
|
if parameters['facetool_type'] == 'codeformer':
|
||||||
facetool_parameters['fidelity'] = float(parameters['codeformer_fidelity'])
|
facetool_parameters['fidelity'] = float(
|
||||||
|
parameters['codeformer_fidelity']
|
||||||
|
)
|
||||||
|
|
||||||
postprocessing.append(facetool_parameters)
|
postprocessing.append(facetool_parameters)
|
||||||
|
|
||||||
@ -792,7 +873,9 @@ class InvokeAIWebServer:
|
|||||||
rfc_dict['sampler'] = parameters['sampler_name']
|
rfc_dict['sampler'] = parameters['sampler_name']
|
||||||
|
|
||||||
# display weighted subprompts (liable to change)
|
# display weighted subprompts (liable to change)
|
||||||
subprompts = split_weighted_subprompts(parameters['prompt'], skip_normalize=True)
|
subprompts = split_weighted_subprompts(
|
||||||
|
parameters['prompt'], skip_normalize=True
|
||||||
|
)
|
||||||
subprompts = [{'prompt': x[0], 'weight': x[1]} for x in subprompts]
|
subprompts = [{'prompt': x[0], 'weight': x[1]} for x in subprompts]
|
||||||
rfc_dict['prompt'] = subprompts
|
rfc_dict['prompt'] = subprompts
|
||||||
|
|
||||||
@ -817,13 +900,13 @@ class InvokeAIWebServer:
|
|||||||
rfc_dict['init_image_path'] = parameters[
|
rfc_dict['init_image_path'] = parameters[
|
||||||
'init_img'
|
'init_img'
|
||||||
] # TODO: Noncompliant
|
] # TODO: Noncompliant
|
||||||
if 'init_mask' in parameters:
|
# if 'init_mask' in parameters:
|
||||||
rfc_dict['mask_hash'] = calculate_init_img_hash(
|
# rfc_dict['mask_hash'] = calculate_init_img_hash(
|
||||||
self.get_image_path_from_url(parameters['init_mask'])
|
# self.get_image_path_from_url(parameters['init_mask'])
|
||||||
) # TODO: Noncompliant
|
# ) # TODO: Noncompliant
|
||||||
rfc_dict['mask_image_path'] = parameters[
|
# rfc_dict['mask_image_path'] = parameters[
|
||||||
'init_mask'
|
# 'init_mask'
|
||||||
] # TODO: Noncompliant
|
# ] # TODO: Noncompliant
|
||||||
else:
|
else:
|
||||||
rfc_dict['type'] = 'txt2img'
|
rfc_dict['type'] = 'txt2img'
|
||||||
|
|
||||||
@ -875,7 +958,9 @@ class InvokeAIWebServer:
|
|||||||
postprocessing_metadata['strength'] = parameters[
|
postprocessing_metadata['strength'] = parameters[
|
||||||
'facetool_strength'
|
'facetool_strength'
|
||||||
]
|
]
|
||||||
postprocessing_metadata['fidelity'] = parameters['codeformer_fidelity']
|
postprocessing_metadata['fidelity'] = parameters[
|
||||||
|
'codeformer_fidelity'
|
||||||
|
]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise TypeError(f"Invalid type: {parameters['type']}")
|
raise TypeError(f"Invalid type: {parameters['type']}")
|
||||||
@ -1119,3 +1204,29 @@ class Progress:
|
|||||||
|
|
||||||
class CanceledException(Exception):
|
class CanceledException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Crops an image to a bounding box.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def copy_image_from_bounding_box(image, x, y, width, height):
|
||||||
|
with image as im:
|
||||||
|
bounds = (x, y, x + width, y + height)
|
||||||
|
im_cropped = im.crop(bounds)
|
||||||
|
return im_cropped
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Pastes an image onto another with a bounding box.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def paste_image_into_bounding_box(
|
||||||
|
recipient_image, donor_image, x, y, width, height
|
||||||
|
):
|
||||||
|
with recipient_image as im:
|
||||||
|
bounds = (x, y, x + width, y + height)
|
||||||
|
im.paste(donor_image, bounds)
|
||||||
|
return recipient_image
|
||||||
|
@ -34,16 +34,6 @@ original unedited image and the masked (partially transparent) image:
|
|||||||
invoke> "man with cat on shoulder" -I./images/man.png -M./images/man-transparent.png
|
invoke> "man with cat on shoulder" -I./images/man.png -M./images/man-transparent.png
|
||||||
```
|
```
|
||||||
|
|
||||||
If you are using Photoshop to make your transparent masks, here is a
|
|
||||||
protocol contributed by III_Communication36 (Discord name):
|
|
||||||
|
|
||||||
Create your alpha channel for mask in photoshop, then run
|
|
||||||
image/adjust/threshold on that channel. Export as Save a copy using
|
|
||||||
superpng (3rd party free download plugin) making sure alpha channel
|
|
||||||
is selected. Then masking works as it should for the img2img
|
|
||||||
process 100%. Can feed just one image this way without needing to
|
|
||||||
feed the -M mask behind it
|
|
||||||
|
|
||||||
## **Masking using Text**
|
## **Masking using Text**
|
||||||
|
|
||||||
You can also create a mask using a text prompt to select the part of
|
You can also create a mask using a text prompt to select the part of
|
||||||
|
58
docs/features/WEBUIHOTKEYS.md
Normal file
58
docs/features/WEBUIHOTKEYS.md
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# **WebUI Hotkey List**
|
||||||
|
|
||||||
|
## General
|
||||||
|
|
||||||
|
| Setting | Hotkey |
|
||||||
|
| ------------ | ---------------------- |
|
||||||
|
| a | Set All Parameters |
|
||||||
|
| s | Set Seed |
|
||||||
|
| u | Upscale |
|
||||||
|
| r | Restoration |
|
||||||
|
| i | Show Metadata |
|
||||||
|
| Ddl | Delete Image |
|
||||||
|
| alt + a | Focus prompt input |
|
||||||
|
| shift + i | Send To Image to Image |
|
||||||
|
| ctrl + enter | Start processing |
|
||||||
|
| shift + x | cancel Processing |
|
||||||
|
| shift + d | Toggle Dark Mode |
|
||||||
|
| ` | Toggle console |
|
||||||
|
|
||||||
|
## Tabs
|
||||||
|
|
||||||
|
| Setting | Hotkey |
|
||||||
|
| ------- | ------------------------- |
|
||||||
|
| 1 | Go to Text To Image Tab |
|
||||||
|
| 2 | Go to Image to Image Tab |
|
||||||
|
| 3 | Go to Inpainting Tab |
|
||||||
|
| 4 | Go to Outpainting Tab |
|
||||||
|
| 5 | Go to Nodes Tab |
|
||||||
|
| 6 | Go to Post Processing Tab |
|
||||||
|
|
||||||
|
## Gallery
|
||||||
|
|
||||||
|
| Setting | Hotkey |
|
||||||
|
| ------------ | ------------------------------- |
|
||||||
|
| g | Toggle Gallery |
|
||||||
|
| left arrow | Go to previous image in gallery |
|
||||||
|
| right arrow | Go to next image in gallery |
|
||||||
|
| shift + p | Pin gallery |
|
||||||
|
| shift + up | Increase gallery image size |
|
||||||
|
| shift + down | Decrease gallery image size |
|
||||||
|
| shift + r | Reset image gallery size |
|
||||||
|
|
||||||
|
## Inpainting
|
||||||
|
|
||||||
|
| Setting | Hotkey |
|
||||||
|
| -------------------------- | --------------------- |
|
||||||
|
| [ | Decrease brush size |
|
||||||
|
| ] | Increase brush size |
|
||||||
|
| alt + [ | Decrease mask opacity |
|
||||||
|
| alt + ] | Increase mask opacity |
|
||||||
|
| b | Select brush |
|
||||||
|
| e | Select eraser |
|
||||||
|
| ctrl + z | Undo brush stroke |
|
||||||
|
| ctrl + shift + z, ctrl + y | Redo brush stroke |
|
||||||
|
| h | Hide mask |
|
||||||
|
| shift + m | Invert mask |
|
||||||
|
| shift + c | Clear mask |
|
||||||
|
| shift + j | Expand canvas |
|
517
frontend/dist/assets/index.38ff1a03.js
vendored
Normal file
517
frontend/dist/assets/index.38ff1a03.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/dist/assets/index.8391cc9a.css
vendored
Normal file
1
frontend/dist/assets/index.8391cc9a.css
vendored
Normal file
File diff suppressed because one or more lines are too long
6
frontend/dist/index.html
vendored
6
frontend/dist/index.html
vendored
@ -5,9 +5,9 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>InvokeAI - A Stable Diffusion Toolkit</title>
|
<title>InvokeAI - A Stable Diffusion Toolkit</title>
|
||||||
<link rel="shortcut icon" type="icon" href="./assets/favicon.0d253ced.ico" />
|
<link rel="shortcut icon" type="icon" href="/assets/favicon.0d253ced.ico" />
|
||||||
<script type="module" crossorigin src="./assets/index.0a6593a2.js"></script>
|
<script type="module" crossorigin src="/assets/index.38ff1a03.js"></script>
|
||||||
<link rel="stylesheet" href="./assets/index.193aec6f.css">
|
<link rel="stylesheet" href="/assets/index.8391cc9a.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
@ -15,27 +15,36 @@
|
|||||||
"@emotion/react": "^11.10.4",
|
"@emotion/react": "^11.10.4",
|
||||||
"@emotion/styled": "^11.10.4",
|
"@emotion/styled": "^11.10.4",
|
||||||
"@radix-ui/react-context-menu": "^2.0.1",
|
"@radix-ui/react-context-menu": "^2.0.1",
|
||||||
|
"@radix-ui/react-slider": "^1.1.0",
|
||||||
|
"@radix-ui/react-tooltip": "^1.0.2",
|
||||||
"@reduxjs/toolkit": "^1.8.5",
|
"@reduxjs/toolkit": "^1.8.5",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
|
"add": "^2.0.6",
|
||||||
"dateformat": "^5.0.3",
|
"dateformat": "^5.0.3",
|
||||||
"framer-motion": "^7.2.1",
|
"framer-motion": "^7.2.1",
|
||||||
|
"konva": "^8.3.13",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"re-resizable": "^6.9.9",
|
"re-resizable": "^6.9.9",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
"react-colorful": "^5.6.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-dropzone": "^14.2.2",
|
"react-dropzone": "^14.2.2",
|
||||||
"react-hotkeys-hook": "^3.4.7",
|
"react-hotkeys-hook": "^3.4.7",
|
||||||
"react-icons": "^4.4.0",
|
"react-icons": "^4.4.0",
|
||||||
|
"react-konva": "^18.2.3",
|
||||||
"react-redux": "^8.0.2",
|
"react-redux": "^8.0.2",
|
||||||
|
"react-transition-group": "^4.4.5",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"socket.io": "^4.5.2",
|
"socket.io": "^4.5.2",
|
||||||
"socket.io-client": "^4.5.2",
|
"socket.io-client": "^4.5.2",
|
||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0",
|
||||||
|
"yarn": "^1.22.19"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/dateformat": "^5.0.0",
|
"@types/dateformat": "^5.0.0",
|
||||||
"@types/react": "^18.0.17",
|
"@types/react": "^18.0.17",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
|
"@types/react-transition-group": "^4.4.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.36.2",
|
"@typescript-eslint/eslint-plugin": "^5.36.2",
|
||||||
"@typescript-eslint/parser": "^5.36.2",
|
"@typescript-eslint/parser": "^5.36.2",
|
||||||
"@vitejs/plugin-react": "^2.0.1",
|
"@vitejs/plugin-react": "^2.0.1",
|
||||||
|
4
frontend/src/app/invokeai.d.ts
vendored
4
frontend/src/app/invokeai.d.ts
vendored
@ -111,6 +111,8 @@ export declare type Image = {
|
|||||||
url: string;
|
url: string;
|
||||||
mtime: number;
|
mtime: number;
|
||||||
metadata: Metadata;
|
metadata: Metadata;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
// GalleryImages is an array of Image.
|
// GalleryImages is an array of Image.
|
||||||
@ -154,6 +156,8 @@ export declare type ImageResultResponse = {
|
|||||||
url: string;
|
url: string;
|
||||||
mtime: number;
|
mtime: number;
|
||||||
metadata: Metadata;
|
metadata: Metadata;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export declare type ErrorResponse = {
|
export declare type ErrorResponse = {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { createAction } from '@reduxjs/toolkit';
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
|
import { InvokeTabName } from '../../features/tabs/InvokeTabs';
|
||||||
import * as InvokeAI from '../invokeai';
|
import * as InvokeAI from '../invokeai';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -8,13 +9,13 @@ import * as InvokeAI from '../invokeai';
|
|||||||
* by the middleware.
|
* by the middleware.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const generateImage = createAction<undefined>('socketio/generateImage');
|
export const generateImage = createAction<InvokeTabName>(
|
||||||
|
'socketio/generateImage'
|
||||||
|
);
|
||||||
export const runESRGAN = createAction<InvokeAI.Image>('socketio/runESRGAN');
|
export const runESRGAN = createAction<InvokeAI.Image>('socketio/runESRGAN');
|
||||||
export const runFacetool = createAction<InvokeAI.Image>('socketio/runFacetool');
|
export const runFacetool = createAction<InvokeAI.Image>('socketio/runFacetool');
|
||||||
export const deleteImage = createAction<InvokeAI.Image>('socketio/deleteImage');
|
export const deleteImage = createAction<InvokeAI.Image>('socketio/deleteImage');
|
||||||
export const requestImages = createAction<undefined>(
|
export const requestImages = createAction<undefined>('socketio/requestImages');
|
||||||
'socketio/requestImages'
|
|
||||||
);
|
|
||||||
export const requestNewImages = createAction<undefined>(
|
export const requestNewImages = createAction<undefined>(
|
||||||
'socketio/requestNewImages'
|
'socketio/requestNewImages'
|
||||||
);
|
);
|
||||||
|
@ -1,13 +1,19 @@
|
|||||||
import { AnyAction, Dispatch, MiddlewareAPI } from '@reduxjs/toolkit';
|
import { AnyAction, Dispatch, MiddlewareAPI } from '@reduxjs/toolkit';
|
||||||
import dateFormat from 'dateformat';
|
import dateFormat from 'dateformat';
|
||||||
import { Socket } from 'socket.io-client';
|
import { Socket } from 'socket.io-client';
|
||||||
import { frontendToBackendParameters } from '../../common/util/parameterTranslation';
|
import {
|
||||||
|
frontendToBackendParameters,
|
||||||
|
FrontendToBackendParametersConfig,
|
||||||
|
} from '../../common/util/parameterTranslation';
|
||||||
import {
|
import {
|
||||||
addLogEntry,
|
addLogEntry,
|
||||||
|
errorOccurred,
|
||||||
setIsProcessing,
|
setIsProcessing,
|
||||||
} from '../../features/system/systemSlice';
|
} from '../../features/system/systemSlice';
|
||||||
import { tabMap, tab_dict } from '../../features/tabs/InvokeTabs';
|
import { inpaintingImageElementRef } from '../../features/tabs/Inpainting/InpaintingCanvas';
|
||||||
|
import { InvokeTabName } from '../../features/tabs/InvokeTabs';
|
||||||
import * as InvokeAI from '../invokeai';
|
import * as InvokeAI from '../invokeai';
|
||||||
|
import { RootState } from '../store';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an object containing all functions which use `socketio.emit()`.
|
* Returns an object containing all functions which use `socketio.emit()`.
|
||||||
@ -21,17 +27,56 @@ const makeSocketIOEmitters = (
|
|||||||
const { dispatch, getState } = store;
|
const { dispatch, getState } = store;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
emitGenerateImage: () => {
|
emitGenerateImage: (generationMode: InvokeTabName) => {
|
||||||
dispatch(setIsProcessing(true));
|
dispatch(setIsProcessing(true));
|
||||||
|
|
||||||
const options = { ...getState().options };
|
const state: RootState = getState();
|
||||||
|
|
||||||
if (tabMap[options.activeTab] !== 'img2img') {
|
const {
|
||||||
options.shouldUseInitImage = false;
|
options: optionsState,
|
||||||
|
system: systemState,
|
||||||
|
inpainting: inpaintingState,
|
||||||
|
gallery: galleryState,
|
||||||
|
} = state;
|
||||||
|
|
||||||
|
const frontendToBackendParametersConfig: FrontendToBackendParametersConfig =
|
||||||
|
{
|
||||||
|
generationMode,
|
||||||
|
optionsState,
|
||||||
|
inpaintingState,
|
||||||
|
systemState,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (generationMode === 'inpainting') {
|
||||||
|
if (
|
||||||
|
!inpaintingImageElementRef.current ||
|
||||||
|
!inpaintingState.imageToInpaint?.url
|
||||||
|
) {
|
||||||
|
dispatch(
|
||||||
|
addLogEntry({
|
||||||
|
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||||
|
message: 'Inpainting image not loaded, cannot generate image.',
|
||||||
|
level: 'error',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
dispatch(errorOccurred());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
frontendToBackendParametersConfig.imageToProcessUrl =
|
||||||
|
inpaintingState.imageToInpaint.url;
|
||||||
|
|
||||||
|
frontendToBackendParametersConfig.maskImageElement =
|
||||||
|
inpaintingImageElementRef.current;
|
||||||
|
} else if (!['txt2img', 'img2img'].includes(generationMode)) {
|
||||||
|
if (!galleryState.currentImage?.url) return;
|
||||||
|
|
||||||
|
frontendToBackendParametersConfig.imageToProcessUrl =
|
||||||
|
galleryState.currentImage.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { generationParameters, esrganParameters, facetoolParameters } =
|
const { generationParameters, esrganParameters, facetoolParameters } =
|
||||||
frontendToBackendParameters(options, getState().system);
|
frontendToBackendParameters(frontendToBackendParametersConfig);
|
||||||
|
|
||||||
socketio.emit(
|
socketio.emit(
|
||||||
'generateImage',
|
'generateImage',
|
||||||
@ -40,6 +85,14 @@ const makeSocketIOEmitters = (
|
|||||||
facetoolParameters
|
facetoolParameters
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// we need to truncate the init_mask base64 else it takes up the whole log
|
||||||
|
// TODO: handle maintaining masks for reproducibility in future
|
||||||
|
if (generationParameters.init_mask) {
|
||||||
|
generationParameters.init_mask = generationParameters.init_mask
|
||||||
|
.substr(0, 20)
|
||||||
|
.concat('...');
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
addLogEntry({
|
addLogEntry({
|
||||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||||
|
@ -79,21 +79,16 @@ const makeSocketIOListeners = (
|
|||||||
*/
|
*/
|
||||||
onGenerationResult: (data: InvokeAI.ImageResultResponse) => {
|
onGenerationResult: (data: InvokeAI.ImageResultResponse) => {
|
||||||
try {
|
try {
|
||||||
const { url, mtime, metadata } = data;
|
|
||||||
const newUuid = uuidv4();
|
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
addImage({
|
addImage({
|
||||||
uuid: newUuid,
|
uuid: uuidv4(),
|
||||||
url,
|
...data,
|
||||||
mtime,
|
|
||||||
metadata: metadata,
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
dispatch(
|
dispatch(
|
||||||
addLogEntry({
|
addLogEntry({
|
||||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||||
message: `Image generated: ${url}`,
|
message: `Image generated: ${data.url}`,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -105,20 +100,16 @@ const makeSocketIOListeners = (
|
|||||||
*/
|
*/
|
||||||
onIntermediateResult: (data: InvokeAI.ImageResultResponse) => {
|
onIntermediateResult: (data: InvokeAI.ImageResultResponse) => {
|
||||||
try {
|
try {
|
||||||
const uuid = uuidv4();
|
|
||||||
const { url, metadata, mtime } = data;
|
|
||||||
dispatch(
|
dispatch(
|
||||||
setIntermediateImage({
|
setIntermediateImage({
|
||||||
uuid,
|
uuid: uuidv4(),
|
||||||
url,
|
...data,
|
||||||
mtime,
|
|
||||||
metadata,
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
dispatch(
|
dispatch(
|
||||||
addLogEntry({
|
addLogEntry({
|
||||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||||
message: `Intermediate image generated: ${url}`,
|
message: `Intermediate image generated: ${data.url}`,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -130,21 +121,17 @@ const makeSocketIOListeners = (
|
|||||||
*/
|
*/
|
||||||
onPostprocessingResult: (data: InvokeAI.ImageResultResponse) => {
|
onPostprocessingResult: (data: InvokeAI.ImageResultResponse) => {
|
||||||
try {
|
try {
|
||||||
const { url, metadata, mtime } = data;
|
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
addImage({
|
addImage({
|
||||||
uuid: uuidv4(),
|
uuid: uuidv4(),
|
||||||
url,
|
...data,
|
||||||
mtime,
|
|
||||||
metadata,
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
addLogEntry({
|
addLogEntry({
|
||||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||||
message: `Postprocessed: ${url}`,
|
message: `Postprocessed: ${data.url}`,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -200,12 +187,14 @@ const makeSocketIOListeners = (
|
|||||||
|
|
||||||
// Generate a UUID for each image
|
// Generate a UUID for each image
|
||||||
const preparedImages = images.map((image): InvokeAI.Image => {
|
const preparedImages = images.map((image): InvokeAI.Image => {
|
||||||
const { url, metadata, mtime } = image;
|
const { url, metadata, mtime, width, height } = image;
|
||||||
return {
|
return {
|
||||||
uuid: uuidv4(),
|
uuid: uuidv4(),
|
||||||
url,
|
url,
|
||||||
mtime,
|
mtime,
|
||||||
metadata,
|
metadata,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -121,7 +121,7 @@ export const socketioMiddleware = () => {
|
|||||||
*/
|
*/
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'socketio/generateImage': {
|
case 'socketio/generateImage': {
|
||||||
emitGenerateImage();
|
emitGenerateImage(action.payload);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import storage from 'redux-persist/lib/storage'; // defaults to localStorage for
|
|||||||
|
|
||||||
import optionsReducer from '../features/options/optionsSlice';
|
import optionsReducer from '../features/options/optionsSlice';
|
||||||
import galleryReducer from '../features/gallery/gallerySlice';
|
import galleryReducer from '../features/gallery/gallerySlice';
|
||||||
|
import inpaintingReducer from '../features/tabs/Inpainting/inpaintingSlice';
|
||||||
|
|
||||||
import systemReducer from '../features/system/systemSlice';
|
import systemReducer from '../features/system/systemSlice';
|
||||||
import { socketioMiddleware } from './socketio/middleware';
|
import { socketioMiddleware } from './socketio/middleware';
|
||||||
@ -32,7 +33,7 @@ import { socketioMiddleware } from './socketio/middleware';
|
|||||||
const rootPersistConfig = {
|
const rootPersistConfig = {
|
||||||
key: 'root',
|
key: 'root',
|
||||||
storage,
|
storage,
|
||||||
blacklist: ['gallery', 'system'],
|
blacklist: ['gallery', 'system', 'inpainting'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const systemPersistConfig = {
|
const systemPersistConfig = {
|
||||||
@ -53,10 +54,28 @@ const systemPersistConfig = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const galleryPersistConfig = {
|
||||||
|
key: 'gallery',
|
||||||
|
storage,
|
||||||
|
whitelist: [
|
||||||
|
'shouldPinGallery',
|
||||||
|
'shouldShowGallery',
|
||||||
|
'galleryScrollPosition',
|
||||||
|
'galleryImageMinimumWidth',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const inpaintingPersistConfig = {
|
||||||
|
key: 'inpainting',
|
||||||
|
storage,
|
||||||
|
blacklist: ['pastLines', 'futuresLines', 'cursorPosition'],
|
||||||
|
};
|
||||||
|
|
||||||
const reducers = combineReducers({
|
const reducers = combineReducers({
|
||||||
options: optionsReducer,
|
options: optionsReducer,
|
||||||
gallery: galleryReducer,
|
gallery: persistReducer(galleryPersistConfig, galleryReducer),
|
||||||
system: persistReducer(systemPersistConfig, systemReducer),
|
system: persistReducer(systemPersistConfig, systemReducer),
|
||||||
|
inpainting: persistReducer(inpaintingPersistConfig, inpaintingReducer),
|
||||||
});
|
});
|
||||||
|
|
||||||
const persistedReducer = persistReducer(rootPersistConfig, reducers);
|
const persistedReducer = persistReducer(rootPersistConfig, reducers);
|
||||||
|
@ -25,7 +25,10 @@ const systemSelector = createSelector(
|
|||||||
const GuidePopover = ({ children, feature }: GuideProps) => {
|
const GuidePopover = ({ children, feature }: GuideProps) => {
|
||||||
const shouldDisplayGuides = useAppSelector(systemSelector);
|
const shouldDisplayGuides = useAppSelector(systemSelector);
|
||||||
const { text } = FEATURES[feature];
|
const { text } = FEATURES[feature];
|
||||||
return shouldDisplayGuides ? (
|
|
||||||
|
if (!shouldDisplayGuides) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
<Popover trigger={'hover'}>
|
<Popover trigger={'hover'}>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Box>{children}</Box>
|
<Box>{children}</Box>
|
||||||
@ -40,8 +43,6 @@ const GuidePopover = ({ children, feature }: GuideProps) => {
|
|||||||
<div className="guide-popover-guide-content">{text}</div>
|
<div className="guide-popover-guide-content">{text}</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
24
frontend/src/common/components/IAICheckbox.scss
Normal file
24
frontend/src/common/components/IAICheckbox.scss
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
.invokeai__checkbox {
|
||||||
|
.chakra-checkbox__label {
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chakra-checkbox__control {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
background-color: var(--input-checkbox-bg);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 0.6rem;
|
||||||
|
height: 0.6rem;
|
||||||
|
stroke-width: 3px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-checked] {
|
||||||
|
color: var(--text-color);
|
||||||
|
background-color: var(--input-checkbox-checked-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
frontend/src/common/components/IAICheckbox.tsx
Normal file
17
frontend/src/common/components/IAICheckbox.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Checkbox, CheckboxProps } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
type IAICheckboxProps = CheckboxProps & {
|
||||||
|
label: string;
|
||||||
|
styleClass?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const IAICheckbox = (props: IAICheckboxProps) => {
|
||||||
|
const { label, styleClass, ...rest } = props;
|
||||||
|
return (
|
||||||
|
<Checkbox className={`invokeai__checkbox ${styleClass}`} {...rest}>
|
||||||
|
{label}
|
||||||
|
</Checkbox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICheckbox;
|
8
frontend/src/common/components/IAIColorPicker.scss
Normal file
8
frontend/src/common/components/IAIColorPicker.scss
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.invokeai__color-picker {
|
||||||
|
.react-colorful__hue-pointer,
|
||||||
|
.react-colorful__saturation-pointer {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
border-color: var(--white);
|
||||||
|
}
|
||||||
|
}
|
19
frontend/src/common/components/IAIColorPicker.tsx
Normal file
19
frontend/src/common/components/IAIColorPicker.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { RgbaColorPicker } from 'react-colorful';
|
||||||
|
import { ColorPickerBaseProps, RgbaColor } from 'react-colorful/dist/types';
|
||||||
|
|
||||||
|
type IAIColorPickerProps = ColorPickerBaseProps<RgbaColor> & {
|
||||||
|
styleClass?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const IAIColorPicker = (props: IAIColorPickerProps) => {
|
||||||
|
const { styleClass, ...rest } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RgbaColorPicker
|
||||||
|
className={`invokeai__color-picker ${styleClass}`}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAIColorPicker;
|
20
frontend/src/common/components/IAIIconButton.scss
Normal file
20
frontend/src/common/components/IAIIconButton.scss
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
@use '../../styles/Mixins/' as *;
|
||||||
|
|
||||||
|
.icon-button {
|
||||||
|
background-color: var(--btn-grey);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--btn-grey-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-selected=true] {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--accent-color-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&[disabled] {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
@ -8,20 +8,28 @@ import {
|
|||||||
interface Props extends IconButtonProps {
|
interface Props extends IconButtonProps {
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
tooltipPlacement?: PlacementWithLogical | undefined;
|
tooltipPlacement?: PlacementWithLogical | undefined;
|
||||||
|
styleClass?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reusable customized button component. Originally was more customized - now probably unecessary.
|
* Reusable customized button component. Originally was more customized - now probably unecessary.
|
||||||
*
|
|
||||||
* TODO: Get rid of this.
|
|
||||||
*/
|
*/
|
||||||
const IAIIconButton = (props: Props) => {
|
const IAIIconButton = (props: Props) => {
|
||||||
const { tooltip = '', tooltipPlacement = 'bottom', onClick, ...rest } = props;
|
const {
|
||||||
|
tooltip = '',
|
||||||
|
tooltipPlacement = 'top',
|
||||||
|
styleClass,
|
||||||
|
onClick,
|
||||||
|
cursor,
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip label={tooltip} hasArrow placement={tooltipPlacement}>
|
<Tooltip label={tooltip} hasArrow placement={tooltipPlacement}>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
className={`icon-button ${styleClass}`}
|
||||||
{...rest}
|
{...rest}
|
||||||
cursor={onClick ? 'pointer' : 'unset'}
|
cursor={cursor ? cursor : onClick ? 'pointer' : 'unset'}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -17,8 +17,8 @@
|
|||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border: 2px solid var(--prompt-border-color);
|
border: 2px solid var(--input-border-color);
|
||||||
box-shadow: 0 0 10px 0 var(--prompt-box-shadow-color);
|
box-shadow: 0 0 10px 0 var(--input-box-shadow-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
|
@ -1,15 +1,32 @@
|
|||||||
.number-input {
|
.invokeai__number-input-form-control {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: max-content auto;
|
grid-template-columns: max-content auto;
|
||||||
column-gap: 1rem;
|
column-gap: 1rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.number-input-label {
|
.invokeai__number-input-form-label {
|
||||||
color: var(--text-color-secondary);
|
color: var(--text-color-secondary);
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 0;
|
||||||
|
flex-grow: 2;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&[data-focus] + .invokeai__number-input-root {
|
||||||
|
outline: none;
|
||||||
|
border: 2px solid var(--input-border-color);
|
||||||
|
box-shadow: 0 0 10px 0 var(--input-box-shadow-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[aria-invalid='true'] + .invokeai__number-input-root {
|
||||||
|
outline: none;
|
||||||
|
border: 2px solid var(--border-color-invalid);
|
||||||
|
box-shadow: 0 0 10px 0 var(--box-shadow-color-invalid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.number-input-field {
|
.invokeai__number-input-root {
|
||||||
|
height: 2rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto max-content;
|
grid-template-columns: auto max-content;
|
||||||
column-gap: 0.5rem;
|
column-gap: 0.5rem;
|
||||||
@ -19,34 +36,45 @@
|
|||||||
border-radius: 0.2rem;
|
border-radius: 0.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.number-input-entry {
|
.invokeai__number-input-field {
|
||||||
border: none;
|
border: none;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-inline-end: 0;
|
height: auto;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border: 2px solid var(--prompt-border-color);
|
box-shadow: none;
|
||||||
box-shadow: 0 0 10px 0 var(--prompt-box-shadow-color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.invokeai__number-input-stepper {
|
||||||
.number-input-stepper {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
padding-right: 0.7rem;
|
padding-right: 0.5rem;
|
||||||
|
|
||||||
svg {
|
.invokeai__number-input-stepper-button {
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.number-input-stepper-button {
|
|
||||||
border: none;
|
border: none;
|
||||||
|
// expand arrow hitbox
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
margin: 0 -0.5rem;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
path {
|
||||||
|
// fill: ;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,12 @@ import {
|
|||||||
NumberDecrementStepper,
|
NumberDecrementStepper,
|
||||||
NumberInputProps,
|
NumberInputProps,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
|
NumberInputFieldProps,
|
||||||
|
NumberInputStepperProps,
|
||||||
|
FormControlProps,
|
||||||
|
FormLabelProps,
|
||||||
|
TooltipProps,
|
||||||
|
Tooltip,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { FocusEvent, useEffect, useState } from 'react';
|
import { FocusEvent, useEffect, useState } from 'react';
|
||||||
@ -23,6 +29,12 @@ interface Props extends Omit<NumberInputProps, 'onChange'> {
|
|||||||
max: number;
|
max: number;
|
||||||
clamp?: boolean;
|
clamp?: boolean;
|
||||||
isInteger?: boolean;
|
isInteger?: boolean;
|
||||||
|
formControlProps?: FormControlProps;
|
||||||
|
formLabelProps?: FormLabelProps;
|
||||||
|
numberInputProps?: NumberInputProps;
|
||||||
|
numberInputFieldProps?: NumberInputFieldProps;
|
||||||
|
numberInputStepperProps?: NumberInputStepperProps;
|
||||||
|
tooltipProps?: Omit<TooltipProps, 'children'>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -34,8 +46,6 @@ const IAINumberInput = (props: Props) => {
|
|||||||
styleClass,
|
styleClass,
|
||||||
isDisabled = false,
|
isDisabled = false,
|
||||||
showStepper = true,
|
showStepper = true,
|
||||||
fontSize = '1rem',
|
|
||||||
size = 'sm',
|
|
||||||
width,
|
width,
|
||||||
textAlign,
|
textAlign,
|
||||||
isInvalid,
|
isInvalid,
|
||||||
@ -44,6 +54,11 @@ const IAINumberInput = (props: Props) => {
|
|||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
isInteger = true,
|
isInteger = true,
|
||||||
|
formControlProps,
|
||||||
|
formLabelProps,
|
||||||
|
numberInputFieldProps,
|
||||||
|
numberInputStepperProps,
|
||||||
|
tooltipProps,
|
||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
@ -65,7 +80,10 @@ const IAINumberInput = (props: Props) => {
|
|||||||
* from the current value.
|
* from the current value.
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!valueAsString.match(numberStringRegex) && value !== Number(valueAsString)) {
|
if (
|
||||||
|
!valueAsString.match(numberStringRegex) &&
|
||||||
|
value !== Number(valueAsString)
|
||||||
|
) {
|
||||||
setValueAsString(String(value));
|
setValueAsString(String(value));
|
||||||
}
|
}
|
||||||
}, [value, valueAsString]);
|
}, [value, valueAsString]);
|
||||||
@ -94,47 +112,51 @@ const IAINumberInput = (props: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormControl
|
<Tooltip {...tooltipProps}>
|
||||||
isDisabled={isDisabled}
|
<FormControl
|
||||||
isInvalid={isInvalid}
|
isDisabled={isDisabled}
|
||||||
className={`number-input ${styleClass}`}
|
isInvalid={isInvalid}
|
||||||
>
|
className={`invokeai__number-input-form-control ${styleClass}`}
|
||||||
{label && (
|
{...formControlProps}
|
||||||
|
>
|
||||||
<FormLabel
|
<FormLabel
|
||||||
fontSize={fontSize}
|
className="invokeai__number-input-form-label"
|
||||||
marginBottom={1}
|
style={{ display: label ? 'block' : 'none' }}
|
||||||
flexGrow={2}
|
{...formLabelProps}
|
||||||
whiteSpace="nowrap"
|
|
||||||
className="number-input-label"
|
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
)}
|
<NumberInput
|
||||||
<NumberInput
|
className="invokeai__number-input-root"
|
||||||
size={size}
|
value={valueAsString}
|
||||||
{...rest}
|
keepWithinRange={true}
|
||||||
className="number-input-field"
|
clampValueOnBlur={false}
|
||||||
value={valueAsString}
|
onChange={handleOnChange}
|
||||||
keepWithinRange={true}
|
onBlur={handleBlur}
|
||||||
clampValueOnBlur={false}
|
|
||||||
onChange={handleOnChange}
|
|
||||||
onBlur={handleBlur}
|
|
||||||
>
|
|
||||||
<NumberInputField
|
|
||||||
fontSize={fontSize}
|
|
||||||
className="number-input-entry"
|
|
||||||
width={width}
|
width={width}
|
||||||
textAlign={textAlign}
|
{...rest}
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="number-input-stepper"
|
|
||||||
style={showStepper ? { display: 'block' } : { display: 'none' }}
|
|
||||||
>
|
>
|
||||||
<NumberIncrementStepper className="number-input-stepper-button" />
|
<NumberInputField
|
||||||
<NumberDecrementStepper className="number-input-stepper-button" />
|
className="invokeai__number-input-field"
|
||||||
</div>
|
textAlign={textAlign}
|
||||||
</NumberInput>
|
{...numberInputFieldProps}
|
||||||
</FormControl>
|
/>
|
||||||
|
<div
|
||||||
|
className="invokeai__number-input-stepper"
|
||||||
|
style={showStepper ? { display: 'block' } : { display: 'none' }}
|
||||||
|
>
|
||||||
|
<NumberIncrementStepper
|
||||||
|
{...numberInputStepperProps}
|
||||||
|
className="invokeai__number-input-stepper-button"
|
||||||
|
/>
|
||||||
|
<NumberDecrementStepper
|
||||||
|
{...numberInputStepperProps}
|
||||||
|
className="invokeai__number-input-stepper-button"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</NumberInput>
|
||||||
|
</FormControl>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
12
frontend/src/common/components/IAIPopover.scss
Normal file
12
frontend/src/common/components/IAIPopover.scss
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
.invokeai__popover-content {
|
||||||
|
min-width: unset;
|
||||||
|
width: unset !important;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.5rem !important;
|
||||||
|
background-color: var(--background-color) !important;
|
||||||
|
border: 2px solid var(--border-color) !important;
|
||||||
|
|
||||||
|
.invokeai__popover-arrow {
|
||||||
|
background-color: var(--background-color) !important;
|
||||||
|
}
|
||||||
|
}
|
39
frontend/src/common/components/IAIPopover.tsx
Normal file
39
frontend/src/common/components/IAIPopover.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverArrow,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
Box,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { PopoverProps } from '@chakra-ui/react';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
type IAIPopoverProps = PopoverProps & {
|
||||||
|
triggerComponent: ReactNode;
|
||||||
|
children: ReactNode;
|
||||||
|
styleClass?: string;
|
||||||
|
hasArrow?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const IAIPopover = (props: IAIPopoverProps) => {
|
||||||
|
const {
|
||||||
|
triggerComponent,
|
||||||
|
children,
|
||||||
|
styleClass,
|
||||||
|
hasArrow = true,
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
|
return (
|
||||||
|
<Popover {...rest}>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Box>{triggerComponent}</Box>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className={`invokeai__popover-content ${styleClass}`}>
|
||||||
|
{hasArrow && <PopoverArrow className={'invokeai__popover-arrow'} />}
|
||||||
|
{children}
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAIPopover;
|
@ -1,28 +1,32 @@
|
|||||||
.iai-select {
|
@use '../../styles/Mixins/' as *;
|
||||||
|
|
||||||
|
.invokeai__select {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, max-content);
|
grid-template-columns: repeat(2, max-content);
|
||||||
column-gap: 1rem;
|
column-gap: 1rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
|
|
||||||
.iai-select-label {
|
.invokeai__select-label {
|
||||||
color: var(--text-color-secondary);
|
color: var(--text-color-secondary);
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.iai-select-picker {
|
.invokeai__select-picker {
|
||||||
border: 2px solid var(--border-color);
|
border: 2px solid var(--border-color);
|
||||||
background-color: var(--background-color-secondary);
|
background-color: var(--background-color-secondary);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
height: 2rem;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border: 2px solid var(--prompt-border-color);
|
border: 2px solid var(--input-border-color);
|
||||||
box-shadow: 0 0 10px 0 var(--prompt-box-shadow-color);
|
box-shadow: 0 0 10px 0 var(--input-box-shadow-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.iai-select-option {
|
.invokeai__select-option {
|
||||||
background-color: var(--background-color-secondary);
|
background-color: var(--background-color-secondary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,13 +21,13 @@ const IAISelect = (props: Props) => {
|
|||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
return (
|
return (
|
||||||
<FormControl isDisabled={isDisabled} className={`iai-select ${styleClass}`}>
|
<FormControl isDisabled={isDisabled} className={`invokeai__select ${styleClass}`}>
|
||||||
<FormLabel
|
<FormLabel
|
||||||
fontSize={fontSize}
|
fontSize={fontSize}
|
||||||
marginBottom={1}
|
marginBottom={1}
|
||||||
flexGrow={2}
|
flexGrow={2}
|
||||||
whiteSpace="nowrap"
|
whiteSpace="nowrap"
|
||||||
className="iai-select-label"
|
className="invokeai__select-label"
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
@ -35,11 +35,11 @@ const IAISelect = (props: Props) => {
|
|||||||
fontSize={fontSize}
|
fontSize={fontSize}
|
||||||
size={size}
|
size={size}
|
||||||
{...rest}
|
{...rest}
|
||||||
className="iai-select-picker"
|
className="invokeai__select-picker"
|
||||||
>
|
>
|
||||||
{validValues.map((opt) => {
|
{validValues.map((opt) => {
|
||||||
return typeof opt === 'string' || typeof opt === 'number' ? (
|
return typeof opt === 'string' || typeof opt === 'number' ? (
|
||||||
<option key={opt} value={opt} className="iai-select-option">
|
<option key={opt} value={opt} className="invokeai__select-option">
|
||||||
{opt}
|
{opt}
|
||||||
</option>
|
</option>
|
||||||
) : (
|
) : (
|
||||||
|
40
frontend/src/common/components/IAISlider.scss
Normal file
40
frontend/src/common/components/IAISlider.scss
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
@use '../../styles/Mixins/' as *;
|
||||||
|
|
||||||
|
.invokeai__slider-form-control {
|
||||||
|
display: flex;
|
||||||
|
column-gap: 1rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: max-content;
|
||||||
|
padding-right: 0.25rem;
|
||||||
|
|
||||||
|
.invokeai__slider-inner-container {
|
||||||
|
display: flex;
|
||||||
|
column-gap: 0.5rem;
|
||||||
|
|
||||||
|
.invokeai__slider-form-label {
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
margin: 0;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
margin-bottom: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invokeai__slider-root {
|
||||||
|
.invokeai__slider-filled-track {
|
||||||
|
background-color: var(--accent-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.invokeai__slider-track {
|
||||||
|
background-color: var(--text-color-secondary);
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invokeai__slider-thumb {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.invokeai__slider-thumb-tooltip {
|
||||||
|
}
|
88
frontend/src/common/components/IAISlider.tsx
Normal file
88
frontend/src/common/components/IAISlider.tsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import {
|
||||||
|
Slider,
|
||||||
|
SliderTrack,
|
||||||
|
SliderFilledTrack,
|
||||||
|
SliderThumb,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Tooltip,
|
||||||
|
SliderProps,
|
||||||
|
FormControlProps,
|
||||||
|
FormLabelProps,
|
||||||
|
SliderTrackProps,
|
||||||
|
SliderThumbProps,
|
||||||
|
TooltipProps,
|
||||||
|
SliderInnerTrackProps,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
|
||||||
|
type IAISliderProps = SliderProps & {
|
||||||
|
label?: string;
|
||||||
|
styleClass?: string;
|
||||||
|
formControlProps?: FormControlProps;
|
||||||
|
formLabelProps?: FormLabelProps;
|
||||||
|
sliderTrackProps?: SliderTrackProps;
|
||||||
|
sliderInnerTrackProps?: SliderInnerTrackProps;
|
||||||
|
sliderThumbProps?: SliderThumbProps;
|
||||||
|
sliderThumbTooltipProps?: Omit<TooltipProps, 'children'>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const IAISlider = (props: IAISliderProps) => {
|
||||||
|
const {
|
||||||
|
label,
|
||||||
|
styleClass,
|
||||||
|
formControlProps,
|
||||||
|
formLabelProps,
|
||||||
|
sliderTrackProps,
|
||||||
|
sliderInnerTrackProps,
|
||||||
|
sliderThumbProps,
|
||||||
|
sliderThumbTooltipProps,
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
className={`invokeai__slider-form-control ${styleClass}`}
|
||||||
|
{...formControlProps}
|
||||||
|
>
|
||||||
|
<div className="invokeai__slider-inner-container">
|
||||||
|
<FormLabel
|
||||||
|
className={`invokeai__slider-form-label`}
|
||||||
|
whiteSpace="nowrap"
|
||||||
|
{...formLabelProps}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<Slider
|
||||||
|
className={`invokeai__slider-root`}
|
||||||
|
aria-label={label}
|
||||||
|
focusThumbOnChange={false}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<SliderTrack
|
||||||
|
className={`invokeai__slider-track`}
|
||||||
|
{...sliderTrackProps}
|
||||||
|
>
|
||||||
|
<SliderFilledTrack
|
||||||
|
className={`invokeai__slider-filled-track`}
|
||||||
|
{...sliderInnerTrackProps}
|
||||||
|
/>
|
||||||
|
</SliderTrack>
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
className={`invokeai__slider-thumb-tooltip`}
|
||||||
|
placement="top"
|
||||||
|
hasArrow
|
||||||
|
{...sliderThumbTooltipProps}
|
||||||
|
>
|
||||||
|
<SliderThumb
|
||||||
|
className={`invokeai__slider-thumb`}
|
||||||
|
{...sliderThumbProps}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Slider>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAISlider;
|
@ -1,18 +1,32 @@
|
|||||||
.chakra-switch,
|
.invokeai__switch-form-control {
|
||||||
.switch-button {
|
.invokeai__switch-form-label {
|
||||||
span {
|
display: flex;
|
||||||
background-color: var(--switch-bg-color);
|
column-gap: 1rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 0.1rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
span {
|
.invokeai__switch-root {
|
||||||
background-color: var(--white);
|
span {
|
||||||
}
|
background-color: var(--switch-bg-color);
|
||||||
}
|
span {
|
||||||
|
background-color: var(--white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
span[data-checked] {
|
&[data-checked] {
|
||||||
background: var(--switch-bg-active-color);
|
span {
|
||||||
|
background: var(--switch-bg-active-color);
|
||||||
|
|
||||||
span {
|
span {
|
||||||
background-color: var(--white);
|
background-color: var(--white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,20 +24,24 @@ const IAISwitch = (props: Props) => {
|
|||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
return (
|
return (
|
||||||
<FormControl isDisabled={isDisabled} width={width}>
|
<FormControl
|
||||||
<Flex justifyContent={'space-between'} alignItems={'center'}>
|
isDisabled={isDisabled}
|
||||||
{label && (
|
width={width}
|
||||||
<FormLabel
|
className="invokeai__switch-form-control"
|
||||||
fontSize={fontSize}
|
>
|
||||||
marginBottom={1}
|
<FormLabel
|
||||||
flexGrow={2}
|
className="invokeai__switch-form-label"
|
||||||
whiteSpace="nowrap"
|
fontSize={fontSize}
|
||||||
>
|
whiteSpace="nowrap"
|
||||||
{label}
|
>
|
||||||
</FormLabel>
|
{label}
|
||||||
)}
|
<Switch
|
||||||
<Switch size={size} className="switch-button" {...rest} />
|
className="invokeai__switch-root"
|
||||||
</Flex>
|
size={size}
|
||||||
|
// className="switch-button"
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
62
frontend/src/common/components/radix-ui/IAISlider.scss
Normal file
62
frontend/src/common/components/radix-ui/IAISlider.scss
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
.invokeai__slider-root {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
user-select: none;
|
||||||
|
touch-action: none;
|
||||||
|
width: 200px;
|
||||||
|
|
||||||
|
&[data-orientation='horizontal'] {
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-orientation='vertical'] {
|
||||||
|
width: 20px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invokeai__slider-track {
|
||||||
|
background-color: black;
|
||||||
|
position: relative;
|
||||||
|
flex-grow: 1;
|
||||||
|
border-radius: 9999px;
|
||||||
|
|
||||||
|
&[data-orientation='horizontal'] {
|
||||||
|
height: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-orientation='vertical'] {
|
||||||
|
width: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invokeai__slider-range {
|
||||||
|
position: absolute;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 9999px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.invokeai__slider-thumb {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.invokeai__slider-thumb-div {
|
||||||
|
all: unset;
|
||||||
|
display: block;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
background-color: white;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 2, 10, 0.3);
|
||||||
|
border-radius: 100%;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: violet;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
box-shadow: 0 0 0 5px rgba(0, 2, 10, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
46
frontend/src/common/components/radix-ui/IAISlider.tsx
Normal file
46
frontend/src/common/components/radix-ui/IAISlider.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { Tooltip } from '@chakra-ui/react';
|
||||||
|
import * as Slider from '@radix-ui/react-slider';
|
||||||
|
import React from 'react';
|
||||||
|
import IAITooltip from './IAITooltip';
|
||||||
|
|
||||||
|
type IAISliderProps = Slider.SliderProps & {
|
||||||
|
value: number[];
|
||||||
|
tooltipLabel?: string;
|
||||||
|
orientation?: 'horizontal' | 'vertial';
|
||||||
|
trackProps?: Slider.SliderTrackProps;
|
||||||
|
rangeProps?: Slider.SliderRangeProps;
|
||||||
|
thumbProps?: Slider.SliderThumbProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
const _IAISlider = (props: IAISliderProps) => {
|
||||||
|
const {
|
||||||
|
value,
|
||||||
|
tooltipLabel,
|
||||||
|
orientation,
|
||||||
|
trackProps,
|
||||||
|
rangeProps,
|
||||||
|
thumbProps,
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
|
return (
|
||||||
|
<Slider.Root
|
||||||
|
className="invokeai__slider-root"
|
||||||
|
{...rest}
|
||||||
|
data-orientation={orientation || 'horizontal'}
|
||||||
|
>
|
||||||
|
<Slider.Track {...trackProps} className="invokeai__slider-track">
|
||||||
|
<Slider.Range {...rangeProps} className="invokeai__slider-range" />
|
||||||
|
</Slider.Track>
|
||||||
|
<Tooltip label={tooltipLabel ?? value[0]} placement="top">
|
||||||
|
<Slider.Thumb {...thumbProps} className="invokeai__slider-thumb">
|
||||||
|
<div className="invokeai__slider-thumb-div" />
|
||||||
|
{/*<IAITooltip trigger={<div className="invokeai__slider-thumb-div" />}>
|
||||||
|
{value && value[0]}
|
||||||
|
</IAITooltip>*/}
|
||||||
|
</Slider.Thumb>
|
||||||
|
</Tooltip>
|
||||||
|
</Slider.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default _IAISlider;
|
8
frontend/src/common/components/radix-ui/IAITooltip.scss
Normal file
8
frontend/src/common/components/radix-ui/IAITooltip.scss
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.invokeai__tooltip-content {
|
||||||
|
padding: 0.5rem;
|
||||||
|
background-color: grey;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
.invokeai__tooltip-arrow {
|
||||||
|
background-color: grey;
|
||||||
|
}
|
||||||
|
}
|
35
frontend/src/common/components/radix-ui/IAITooltip.tsx
Normal file
35
frontend/src/common/components/radix-ui/IAITooltip.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
type IAITooltipProps = Tooltip.TooltipProps & {
|
||||||
|
trigger: ReactNode;
|
||||||
|
children: ReactNode;
|
||||||
|
triggerProps?: Tooltip.TooltipTriggerProps;
|
||||||
|
contentProps?: Tooltip.TooltipContentProps;
|
||||||
|
arrowProps?: Tooltip.TooltipArrowProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
const IAITooltip = (props: IAITooltipProps) => {
|
||||||
|
const { trigger, children, triggerProps, contentProps, arrowProps, ...rest } =
|
||||||
|
props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip.Provider>
|
||||||
|
<Tooltip.Root {...rest} delayDuration={0}>
|
||||||
|
<Tooltip.Trigger {...triggerProps}>{trigger}</Tooltip.Trigger>
|
||||||
|
<Tooltip.Portal>
|
||||||
|
<Tooltip.Content
|
||||||
|
{...contentProps}
|
||||||
|
onPointerDownOutside={(e: any) => {e.preventDefault()}}
|
||||||
|
className="invokeai__tooltip-content"
|
||||||
|
>
|
||||||
|
<Tooltip.Arrow {...arrowProps} className="invokeai__tooltip-arrow" />
|
||||||
|
{children}
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Portal>
|
||||||
|
</Tooltip.Root>
|
||||||
|
</Tooltip.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAITooltip;
|
@ -3,9 +3,12 @@ import { isEqual } from 'lodash';
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useAppSelector } from '../../app/store';
|
import { useAppSelector } from '../../app/store';
|
||||||
import { RootState } from '../../app/store';
|
import { RootState } from '../../app/store';
|
||||||
|
import { GalleryState } from '../../features/gallery/gallerySlice';
|
||||||
import { OptionsState } from '../../features/options/optionsSlice';
|
import { OptionsState } from '../../features/options/optionsSlice';
|
||||||
|
|
||||||
import { SystemState } from '../../features/system/systemSlice';
|
import { SystemState } from '../../features/system/systemSlice';
|
||||||
|
import { InpaintingState } from '../../features/tabs/Inpainting/inpaintingSlice';
|
||||||
|
import { tabMap } from '../../features/tabs/InvokeTabs';
|
||||||
import { validateSeedWeights } from '../util/seedWeightPairs';
|
import { validateSeedWeights } from '../util/seedWeightPairs';
|
||||||
|
|
||||||
export const optionsSelector = createSelector(
|
export const optionsSelector = createSelector(
|
||||||
@ -18,7 +21,7 @@ export const optionsSelector = createSelector(
|
|||||||
maskPath: options.maskPath,
|
maskPath: options.maskPath,
|
||||||
initialImagePath: options.initialImagePath,
|
initialImagePath: options.initialImagePath,
|
||||||
seed: options.seed,
|
seed: options.seed,
|
||||||
activeTab: options.activeTab,
|
activeTabName: tabMap[options.activeTab],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -43,31 +46,66 @@ export const systemSelector = createSelector(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const inpaintingSelector = createSelector(
|
||||||
|
(state: RootState) => state.inpainting,
|
||||||
|
(inpainting: InpaintingState) => {
|
||||||
|
return {
|
||||||
|
isMaskEmpty: inpainting.lines.length === 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const gallerySelector = createSelector(
|
||||||
|
(state: RootState) => state.gallery,
|
||||||
|
(gallery: GalleryState) => {
|
||||||
|
return {
|
||||||
|
hasCurrentImage: Boolean(gallery.currentImage),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks relevant pieces of state to confirm generation will not deterministically fail.
|
* Checks relevant pieces of state to confirm generation will not deterministically fail.
|
||||||
* This is used to prevent the 'Generate' button from being clicked.
|
* This is used to prevent the 'Generate' button from being clicked.
|
||||||
*/
|
*/
|
||||||
const useCheckParameters = (): boolean => {
|
const useCheckParameters = (): boolean => {
|
||||||
const { prompt } = useAppSelector(optionsSelector);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
prompt,
|
||||||
shouldGenerateVariations,
|
shouldGenerateVariations,
|
||||||
seedWeights,
|
seedWeights,
|
||||||
maskPath,
|
maskPath,
|
||||||
initialImagePath,
|
initialImagePath,
|
||||||
seed,
|
seed,
|
||||||
activeTab,
|
activeTabName,
|
||||||
} = useAppSelector(optionsSelector);
|
} = useAppSelector(optionsSelector);
|
||||||
|
|
||||||
const { isProcessing, isConnected } = useAppSelector(systemSelector);
|
const { isProcessing, isConnected } = useAppSelector(systemSelector);
|
||||||
|
|
||||||
|
const { isMaskEmpty } = useAppSelector(inpaintingSelector);
|
||||||
|
|
||||||
|
const { hasCurrentImage } = useAppSelector(gallerySelector);
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
// Cannot generate without a prompt
|
// Cannot generate without a prompt
|
||||||
if (!prompt || Boolean(prompt.match(/^[\s\r\n]+$/))) {
|
if (!prompt || Boolean(prompt.match(/^[\s\r\n]+$/))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prompt && !initialImagePath && activeTab === 1) {
|
if (activeTabName === 'img2img' && !initialImagePath) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeTabName === 'inpainting' && (!hasCurrentImage || isMaskEmpty)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,7 +144,9 @@ const useCheckParameters = (): boolean => {
|
|||||||
shouldGenerateVariations,
|
shouldGenerateVariations,
|
||||||
seedWeights,
|
seedWeights,
|
||||||
seed,
|
seed,
|
||||||
activeTab,
|
activeTabName,
|
||||||
|
hasCurrentImage,
|
||||||
|
isMaskEmpty,
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,22 +1,38 @@
|
|||||||
/*
|
|
||||||
These functions translate frontend state into parameters
|
|
||||||
suitable for consumption by the backend, and vice-versa.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from '../../app/constants';
|
import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from '../../app/constants';
|
||||||
import { OptionsState } from '../../features/options/optionsSlice';
|
import { OptionsState } from '../../features/options/optionsSlice';
|
||||||
import { SystemState } from '../../features/system/systemSlice';
|
import { SystemState } from '../../features/system/systemSlice';
|
||||||
|
|
||||||
import {
|
import { stringToSeedWeightsArray } from './seedWeightPairs';
|
||||||
seedWeightsToString,
|
|
||||||
stringToSeedWeightsArray,
|
|
||||||
} from './seedWeightPairs';
|
|
||||||
import randomInt from './randomInt';
|
import randomInt from './randomInt';
|
||||||
|
import { InvokeTabName } from '../../features/tabs/InvokeTabs';
|
||||||
|
import { InpaintingState } from '../../features/tabs/Inpainting/inpaintingSlice';
|
||||||
|
import generateMask from '../../features/tabs/Inpainting/util/generateMask';
|
||||||
|
|
||||||
|
export type FrontendToBackendParametersConfig = {
|
||||||
|
generationMode: InvokeTabName;
|
||||||
|
optionsState: OptionsState;
|
||||||
|
inpaintingState: InpaintingState;
|
||||||
|
systemState: SystemState;
|
||||||
|
imageToProcessUrl?: string;
|
||||||
|
maskImageElement?: HTMLImageElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translates/formats frontend state into parameters suitable
|
||||||
|
* for consumption by the API.
|
||||||
|
*/
|
||||||
export const frontendToBackendParameters = (
|
export const frontendToBackendParameters = (
|
||||||
optionsState: OptionsState,
|
config: FrontendToBackendParametersConfig
|
||||||
systemState: SystemState
|
|
||||||
): { [key: string]: any } => {
|
): { [key: string]: any } => {
|
||||||
|
const {
|
||||||
|
generationMode,
|
||||||
|
optionsState,
|
||||||
|
inpaintingState,
|
||||||
|
systemState,
|
||||||
|
imageToProcessUrl,
|
||||||
|
maskImageElement,
|
||||||
|
} = config;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
prompt,
|
prompt,
|
||||||
iterations,
|
iterations,
|
||||||
@ -30,10 +46,8 @@ export const frontendToBackendParameters = (
|
|||||||
seed,
|
seed,
|
||||||
seamless,
|
seamless,
|
||||||
hiresFix,
|
hiresFix,
|
||||||
shouldUseInitImage,
|
|
||||||
img2imgStrength,
|
img2imgStrength,
|
||||||
initialImagePath,
|
initialImagePath,
|
||||||
maskPath,
|
|
||||||
shouldFitToWidthHeight,
|
shouldFitToWidthHeight,
|
||||||
shouldGenerateVariations,
|
shouldGenerateVariations,
|
||||||
variationAmount,
|
variationAmount,
|
||||||
@ -61,8 +75,6 @@ export const frontendToBackendParameters = (
|
|||||||
width,
|
width,
|
||||||
sampler_name: sampler,
|
sampler_name: sampler,
|
||||||
seed,
|
seed,
|
||||||
seamless,
|
|
||||||
hires_fix: hiresFix,
|
|
||||||
progress_images: shouldDisplayInProgress,
|
progress_images: shouldDisplayInProgress,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -70,13 +82,45 @@ export const frontendToBackendParameters = (
|
|||||||
? randomInt(NUMPY_RAND_MIN, NUMPY_RAND_MAX)
|
? randomInt(NUMPY_RAND_MIN, NUMPY_RAND_MAX)
|
||||||
: seed;
|
: seed;
|
||||||
|
|
||||||
if (shouldUseInitImage) {
|
// parameters common to txt2img and img2img
|
||||||
|
if (['txt2img', 'img2img'].includes(generationMode)) {
|
||||||
|
generationParameters.seamless = seamless;
|
||||||
|
generationParameters.hires_fix = hiresFix;
|
||||||
|
}
|
||||||
|
|
||||||
|
// img2img exclusive parameters
|
||||||
|
if (generationMode === 'img2img') {
|
||||||
generationParameters.init_img = initialImagePath;
|
generationParameters.init_img = initialImagePath;
|
||||||
generationParameters.strength = img2imgStrength;
|
generationParameters.strength = img2imgStrength;
|
||||||
generationParameters.fit = shouldFitToWidthHeight;
|
generationParameters.fit = shouldFitToWidthHeight;
|
||||||
if (maskPath) {
|
}
|
||||||
generationParameters.init_mask = maskPath;
|
|
||||||
}
|
// inpainting exclusive parameters
|
||||||
|
if (generationMode === 'inpainting' && maskImageElement) {
|
||||||
|
const {
|
||||||
|
lines,
|
||||||
|
boundingBoxCoordinate: { x, y },
|
||||||
|
boundingBoxDimensions: { width, height },
|
||||||
|
} = inpaintingState;
|
||||||
|
|
||||||
|
const boundingBox = {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
|
||||||
|
generationParameters.init_img = imageToProcessUrl;
|
||||||
|
generationParameters.strength = img2imgStrength;
|
||||||
|
generationParameters.fit = false;
|
||||||
|
|
||||||
|
const maskDataURL = generateMask(maskImageElement, lines, boundingBox);
|
||||||
|
|
||||||
|
generationParameters.init_mask = maskDataURL.split(
|
||||||
|
'data:image/png;base64,'
|
||||||
|
)[1];
|
||||||
|
|
||||||
|
generationParameters.bounding_box = boundingBox;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldGenerateVariations) {
|
if (shouldGenerateVariations) {
|
||||||
@ -105,7 +149,7 @@ export const frontendToBackendParameters = (
|
|||||||
strength: facetoolStrength,
|
strength: facetoolStrength,
|
||||||
};
|
};
|
||||||
if (facetoolType === 'codeformer') {
|
if (facetoolType === 'codeformer') {
|
||||||
facetoolParameters.codeformer_fidelity = codeformerFidelity
|
facetoolParameters.codeformer_fidelity = codeformerFidelity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
3
frontend/src/common/util/roundDownToMultiple.ts
Normal file
3
frontend/src/common/util/roundDownToMultiple.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const roundDownToMultiple = (num: number, multiple: number): number => {
|
||||||
|
return Math.floor(num / multiple) * multiple;
|
||||||
|
};
|
@ -17,12 +17,22 @@ import { SystemState } from '../system/systemSlice';
|
|||||||
import IAIButton from '../../common/components/IAIButton';
|
import IAIButton from '../../common/components/IAIButton';
|
||||||
import { runESRGAN, runFacetool } from '../../app/socketio/actions';
|
import { runESRGAN, runFacetool } from '../../app/socketio/actions';
|
||||||
import IAIIconButton from '../../common/components/IAIIconButton';
|
import IAIIconButton from '../../common/components/IAIIconButton';
|
||||||
import { MdDelete, MdFace, MdHd, MdImage, MdInfo } from 'react-icons/md';
|
import {
|
||||||
|
MdDelete,
|
||||||
|
MdFace,
|
||||||
|
MdHd,
|
||||||
|
MdImage,
|
||||||
|
MdInfo,
|
||||||
|
MdSettings,
|
||||||
|
} from 'react-icons/md';
|
||||||
import InvokePopover from './InvokePopover';
|
import InvokePopover from './InvokePopover';
|
||||||
import UpscaleOptions from '../options/AdvancedOptions/Upscale/UpscaleOptions';
|
import UpscaleOptions from '../options/AdvancedOptions/Upscale/UpscaleOptions';
|
||||||
import FaceRestoreOptions from '../options/AdvancedOptions/FaceRestore/FaceRestoreOptions';
|
import FaceRestoreOptions from '../options/AdvancedOptions/FaceRestore/FaceRestoreOptions';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useToast } from '@chakra-ui/react';
|
import { useToast } from '@chakra-ui/react';
|
||||||
|
import { FaPaintBrush, FaSeedling } from 'react-icons/fa';
|
||||||
|
import { setImageToInpaint } from '../tabs/Inpainting/inpaintingSlice';
|
||||||
|
import { hoverableImageSelector } from './gallerySliceSelectors';
|
||||||
|
|
||||||
const systemSelector = createSelector(
|
const systemSelector = createSelector(
|
||||||
(state: RootState) => state.system,
|
(state: RootState) => state.system,
|
||||||
@ -51,6 +61,7 @@ type CurrentImageButtonsProps = {
|
|||||||
*/
|
*/
|
||||||
const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const { activeTabName } = useAppSelector(hoverableImageSelector);
|
||||||
|
|
||||||
const shouldShowImageDetails = useAppSelector(
|
const shouldShowImageDetails = useAppSelector(
|
||||||
(state: RootState) => state.options.shouldShowImageDetails
|
(state: RootState) => state.options.shouldShowImageDetails
|
||||||
@ -221,6 +232,19 @@ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
|||||||
const handleClickShowImageDetails = () =>
|
const handleClickShowImageDetails = () =>
|
||||||
dispatch(setShouldShowImageDetails(!shouldShowImageDetails));
|
dispatch(setShouldShowImageDetails(!shouldShowImageDetails));
|
||||||
|
|
||||||
|
const handleSendToInpainting = () => {
|
||||||
|
dispatch(setImageToInpaint(image));
|
||||||
|
if (activeTabName !== 'inpainting') {
|
||||||
|
dispatch(setActiveTab('inpainting'));
|
||||||
|
}
|
||||||
|
toast({
|
||||||
|
title: 'Sent to Inpainting',
|
||||||
|
status: 'success',
|
||||||
|
duration: 2500,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'i',
|
'i',
|
||||||
() => {
|
() => {
|
||||||
@ -247,7 +271,32 @@ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
|||||||
onClick={handleClickUseAsInitialImage}
|
onClick={handleClickUseAsInitialImage}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<IAIButton
|
<IAIIconButton
|
||||||
|
icon={<FaPaintBrush />}
|
||||||
|
tooltip="Send To Inpainting"
|
||||||
|
aria-label="Send To Inpainting"
|
||||||
|
onClick={handleSendToInpainting}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IAIIconButton
|
||||||
|
icon={<MdSettings />}
|
||||||
|
tooltip="Use All"
|
||||||
|
aria-label="Use All"
|
||||||
|
isDisabled={
|
||||||
|
!['txt2img', 'img2img'].includes(image?.metadata?.image?.type)
|
||||||
|
}
|
||||||
|
onClick={handleClickUseAllParameters}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IAIIconButton
|
||||||
|
icon={<FaSeedling />}
|
||||||
|
tooltip="Use Seed"
|
||||||
|
aria-label="Use Seed"
|
||||||
|
isDisabled={!image?.metadata?.image?.seed}
|
||||||
|
onClick={handleClickUseSeed}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* <IAIButton
|
||||||
label="Use All"
|
label="Use All"
|
||||||
isDisabled={
|
isDisabled={
|
||||||
!['txt2img', 'img2img'].includes(image?.metadata?.image?.type)
|
!['txt2img', 'img2img'].includes(image?.metadata?.image?.type)
|
||||||
@ -259,7 +308,7 @@ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
|||||||
label="Use Seed"
|
label="Use Seed"
|
||||||
isDisabled={!image?.metadata?.image?.seed}
|
isDisabled={!image?.metadata?.image?.seed}
|
||||||
onClick={handleClickUseSeed}
|
onClick={handleClickUseSeed}
|
||||||
/>
|
/> */}
|
||||||
|
|
||||||
<InvokePopover
|
<InvokePopover
|
||||||
title="Restore Faces"
|
title="Restore Faces"
|
||||||
|
@ -1,29 +1,19 @@
|
|||||||
@use '../../styles/Mixins/' as *;
|
@use '../../styles/Mixins/' as *;
|
||||||
|
|
||||||
.current-image-display {
|
.current-image-display {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-areas:
|
flex-direction: column;
|
||||||
'current-image-tools'
|
height: 100%;
|
||||||
'current-image-preview';
|
|
||||||
grid-template-rows: auto 1fr;
|
|
||||||
justify-items: center;
|
|
||||||
background-color: var(--background-color-secondary);
|
background-color: var(--background-color-secondary);
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-image-tools {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: grid;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-image-options {
|
.current-image-options {
|
||||||
display: grid;
|
width: 100%;
|
||||||
grid-auto-flow: column;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
column-gap: 0.5rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
height: fit-content;
|
|
||||||
gap: 0.5rem;
|
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@include Button(
|
@include Button(
|
||||||
@ -35,13 +25,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.current-image-viewer {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.current-image-preview {
|
.current-image-preview {
|
||||||
|
position: absolute;
|
||||||
|
top:0;
|
||||||
grid-area: current-image-preview;
|
grid-area: current-image-preview;
|
||||||
position: relative;
|
position: relative;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
grid-template-areas: 'current-image-content';
|
grid-template-areas: 'current-image-content';
|
||||||
|
|
||||||
img {
|
img {
|
||||||
@ -49,8 +48,8 @@
|
|||||||
background-color: var(--img2img-img-bg-color);
|
background-color: var(--img2img-img-bg-color);
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
width: auto;
|
// width: auto;
|
||||||
height: $app-gallery-height;
|
// height: $app-gallery-height;
|
||||||
max-height: $app-gallery-height;
|
max-height: $app-gallery-height;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,13 +23,15 @@ const CurrentImageDisplay = () => {
|
|||||||
<div className="current-image-tools">
|
<div className="current-image-tools">
|
||||||
<CurrentImageButtons image={imageToDisplay} />
|
<CurrentImageButtons image={imageToDisplay} />
|
||||||
</div>
|
</div>
|
||||||
<CurrentImagePreview imageToDisplay={imageToDisplay} />
|
<div className="current-image-viewer">
|
||||||
{shouldShowImageDetails && (
|
<CurrentImagePreview imageToDisplay={imageToDisplay} />
|
||||||
<ImageMetadataViewer
|
{shouldShowImageDetails && (
|
||||||
image={imageToDisplay}
|
<ImageMetadataViewer
|
||||||
styleClass="current-image-metadata"
|
image={imageToDisplay}
|
||||||
/>
|
styleClass="current-image-metadata"
|
||||||
)}
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="current-image-display-placeholder">
|
<div className="current-image-display-placeholder">
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { IconButton, Image } from '@chakra-ui/react';
|
import { IconButton, Image } from '@chakra-ui/react';
|
||||||
import React, { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa';
|
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa';
|
||||||
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
|
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
|
||||||
import { GalleryState, selectNextImage, selectPrevImage } from './gallerySlice';
|
import { GalleryState, selectNextImage, selectPrevImage } from './gallerySlice';
|
||||||
|
@ -22,6 +22,8 @@ import {
|
|||||||
import * as InvokeAI from '../../app/invokeai';
|
import * as InvokeAI from '../../app/invokeai';
|
||||||
import * as ContextMenu from '@radix-ui/react-context-menu';
|
import * as ContextMenu from '@radix-ui/react-context-menu';
|
||||||
import { tabMap } from '../tabs/InvokeTabs';
|
import { tabMap } from '../tabs/InvokeTabs';
|
||||||
|
import { setImageToInpaint } from '../tabs/Inpainting/inpaintingSlice';
|
||||||
|
import { hoverableImageSelector } from './gallerySliceSelectors';
|
||||||
|
|
||||||
interface HoverableImageProps {
|
interface HoverableImageProps {
|
||||||
image: InvokeAI.Image;
|
image: InvokeAI.Image;
|
||||||
@ -38,9 +40,7 @@ const memoEqualityCheck = (
|
|||||||
*/
|
*/
|
||||||
const HoverableImage = memo((props: HoverableImageProps) => {
|
const HoverableImage = memo((props: HoverableImageProps) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const activeTab = useAppSelector(
|
const { activeTabName } = useAppSelector(hoverableImageSelector);
|
||||||
(state: RootState) => state.options.activeTab
|
|
||||||
);
|
|
||||||
|
|
||||||
const [isHovered, setIsHovered] = useState<boolean>(false);
|
const [isHovered, setIsHovered] = useState<boolean>(false);
|
||||||
|
|
||||||
@ -75,8 +75,8 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
|
|
||||||
const handleSendToImageToImage = () => {
|
const handleSendToImageToImage = () => {
|
||||||
dispatch(setInitialImagePath(image.url));
|
dispatch(setInitialImagePath(image.url));
|
||||||
if (activeTab !== 1) {
|
if (activeTabName !== 'img2img') {
|
||||||
dispatch(setActiveTab(1));
|
dispatch(setActiveTab('img2img'));
|
||||||
}
|
}
|
||||||
toast({
|
toast({
|
||||||
title: 'Sent to Image To Image',
|
title: 'Sent to Image To Image',
|
||||||
@ -86,6 +86,19 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSendToInpainting = () => {
|
||||||
|
dispatch(setImageToInpaint(image));
|
||||||
|
if (activeTabName !== 'inpainting') {
|
||||||
|
dispatch(setActiveTab('inpainting'));
|
||||||
|
}
|
||||||
|
toast({
|
||||||
|
title: 'Sent to Inpainting',
|
||||||
|
status: 'success',
|
||||||
|
duration: 2500,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleUseAllParameters = () => {
|
const handleUseAllParameters = () => {
|
||||||
dispatch(setAllTextToImageParameters(metadata));
|
dispatch(setAllTextToImageParameters(metadata));
|
||||||
toast({
|
toast({
|
||||||
@ -200,6 +213,9 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
<ContextMenu.Item onClickCapture={handleSendToImageToImage}>
|
<ContextMenu.Item onClickCapture={handleSendToImageToImage}>
|
||||||
Send to Image To Image
|
Send to Image To Image
|
||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
|
<ContextMenu.Item onClickCapture={handleSendToInpainting}>
|
||||||
|
Send to Inpainting
|
||||||
|
</ContextMenu.Item>
|
||||||
<DeleteImageModal image={image}>
|
<DeleteImageModal image={image}>
|
||||||
<ContextMenu.Item data-warning>Delete Image</ContextMenu.Item>
|
<ContextMenu.Item data-warning>Delete Image</ContextMenu.Item>
|
||||||
</DeleteImageModal>
|
</DeleteImageModal>
|
||||||
|
@ -1,64 +1,128 @@
|
|||||||
@use '../../styles/Mixins/' as *;
|
@use '../../styles/Mixins/' as *;
|
||||||
|
|
||||||
|
.image-gallery-area-enter {
|
||||||
|
transform: translateX(150%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-gallery-area-enter-active {
|
||||||
|
transform: translateX(0);
|
||||||
|
transition: all 120ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-gallery-area-exit {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-gallery-area-exit-active {
|
||||||
|
transform: translateX(150%);
|
||||||
|
transition: all 120ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
.image-gallery-area {
|
.image-gallery-area {
|
||||||
.image-gallery-popup-btn {
|
z-index: 10;
|
||||||
position: absolute;
|
&[data-pinned='false'] {
|
||||||
top: 50%;
|
position: fixed;
|
||||||
right: 1rem;
|
height: 100vh;
|
||||||
border-radius: 0.5rem 0 0 0.5rem;
|
top: 0;
|
||||||
padding: 0 0.5rem;
|
right: 0;
|
||||||
@include Button(
|
|
||||||
$btn-width: 1rem,
|
.image-gallery-popup {
|
||||||
$btn-height: 6rem,
|
border-radius: 0;
|
||||||
$icon-size: 20px,
|
.image-gallery-container {
|
||||||
$btn-color: var(--btn-grey),
|
max-height: calc($app-height + 5rem);
|
||||||
$btn-color-hover: var(--btn-grey-hover)
|
}
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.image-gallery-popup {
|
.image-gallery-popup {
|
||||||
background-color: var(--tab-color);
|
background-color: var(--tab-color);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
animation: slideOut 0.3s ease-out;
|
display: flex;
|
||||||
display: flex;
|
flex-direction: column;
|
||||||
flex-direction: column;
|
row-gap: 1rem;
|
||||||
row-gap: 1rem;
|
border-radius: 0.5rem;
|
||||||
border-radius: 0.5rem;
|
border-left-width: 0.3rem;
|
||||||
border-left-width: 0.2rem;
|
|
||||||
min-width: 300px;
|
|
||||||
border-color: var(--gallery-resizeable-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-gallery-header {
|
border-color: var(--resizeable-handle-border-color);
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
h1 {
|
&[data-resize-alert='true'] {
|
||||||
font-weight: bold;
|
border-color: var(--status-bad-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-gallery-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 0.5rem;
|
||||||
|
|
||||||
|
.image-gallery-icon-btn {
|
||||||
|
background-color: var(--btn-load-more) !important;
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--btn-load-more-hover) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-gallery-size-popover {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, max-content);
|
||||||
|
column-gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-gallery-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: $app-gallery-popover-height;
|
||||||
|
overflow-y: scroll;
|
||||||
|
@include HideScrollbar;
|
||||||
|
|
||||||
|
.image-gallery-container-placeholder {
|
||||||
|
display: flex;
|
||||||
|
background-color: var(--background-color-secondary);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
place-items: center;
|
||||||
|
padding: 2rem 0;
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: var(--subtext-color-bright);
|
||||||
|
font-family: Inter;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 5rem;
|
||||||
|
height: 5rem;
|
||||||
|
color: var(--svg-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-gallery-load-more-btn {
|
||||||
|
background-color: var(--btn-load-more) !important;
|
||||||
|
font-size: 0.85rem !important;
|
||||||
|
padding: 0.5rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--btn-load-more) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--btn-load-more-hover) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-gallery-close-btn {
|
|
||||||
background-color: var(--btn-load-more) !important;
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--btn-load-more-hover) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-gallery-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
max-height: $app-gallery-popover-height;
|
|
||||||
overflow-y: scroll;
|
|
||||||
@include HideScrollbar;
|
|
||||||
}
|
|
||||||
|
|
||||||
// from https://css-tricks.com/a-grid-of-logos-in-squares/
|
// from https://css-tricks.com/a-grid-of-logos-in-squares/
|
||||||
.image-gallery {
|
.image-gallery {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(80px, auto));
|
// grid-template-columns: repeat(auto-fill, minmax(80px, auto));
|
||||||
grid-gap: 0.5rem;
|
grid-gap: 0.5rem;
|
||||||
.hoverable-image {
|
.hoverable-image {
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
@ -86,38 +150,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-gallery-load-more-btn {
|
|
||||||
background-color: var(--btn-load-more) !important;
|
|
||||||
font-size: 0.85rem !important;
|
|
||||||
font-family: Inter;
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--btn-load-more) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--btn-load-more-hover) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-gallery-container-placeholder {
|
|
||||||
display: flex;
|
|
||||||
background-color: var(--background-color-secondary);
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
place-items: center;
|
|
||||||
padding: 2rem 0;
|
|
||||||
|
|
||||||
p {
|
|
||||||
color: var(--subtext-color-bright);
|
|
||||||
font-family: Inter;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 5rem;
|
|
||||||
height: 5rem;
|
|
||||||
color: var(--svg-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,36 +1,121 @@
|
|||||||
import { Button, IconButton } from '@chakra-ui/button';
|
import { Button } from '@chakra-ui/button';
|
||||||
import { Resizable } from 're-resizable';
|
import { NumberSize, Resizable, Size } from 're-resizable';
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { MdClear, MdPhotoLibrary } from 'react-icons/md';
|
import { MdClear, MdPhotoLibrary } from 'react-icons/md';
|
||||||
|
import { BsPinAngleFill } from 'react-icons/bs';
|
||||||
import { requestImages } from '../../app/socketio/actions';
|
import { requestImages } from '../../app/socketio/actions';
|
||||||
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
|
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||||
import IAIIconButton from '../../common/components/IAIIconButton';
|
import IAIIconButton from '../../common/components/IAIIconButton';
|
||||||
import { selectNextImage, selectPrevImage } from './gallerySlice';
|
import {
|
||||||
|
selectNextImage,
|
||||||
|
selectPrevImage,
|
||||||
|
setGalleryImageMinimumWidth,
|
||||||
|
setGalleryScrollPosition,
|
||||||
|
setShouldPinGallery,
|
||||||
|
} from './gallerySlice';
|
||||||
import HoverableImage from './HoverableImage';
|
import HoverableImage from './HoverableImage';
|
||||||
import { setShouldShowGallery } from '../options/optionsSlice';
|
import { setShouldShowGallery } from '../gallery/gallerySlice';
|
||||||
|
import { Spacer, useToast } from '@chakra-ui/react';
|
||||||
|
import { CSSTransition } from 'react-transition-group';
|
||||||
|
import { Direction } from 're-resizable/lib/resizer';
|
||||||
|
import { imageGallerySelector } from './gallerySliceSelectors';
|
||||||
|
import { FaWrench } from 'react-icons/fa';
|
||||||
|
import IAIPopover from '../../common/components/IAIPopover';
|
||||||
|
import IAISlider from '../../common/components/IAISlider';
|
||||||
|
import { BiReset } from 'react-icons/bi';
|
||||||
|
|
||||||
export default function ImageGallery() {
|
export default function ImageGallery() {
|
||||||
const { images, currentImageUuid, areMoreImagesAvailable } = useAppSelector(
|
|
||||||
(state: RootState) => state.gallery
|
|
||||||
);
|
|
||||||
|
|
||||||
const shouldShowGallery = useAppSelector(
|
|
||||||
(state: RootState) => state.options.shouldShowGallery
|
|
||||||
);
|
|
||||||
|
|
||||||
const activeTab = useAppSelector(
|
|
||||||
(state: RootState) => state.options.activeTab
|
|
||||||
);
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
const handleShowGalleryToggle = () => {
|
const {
|
||||||
dispatch(setShouldShowGallery(!shouldShowGallery));
|
images,
|
||||||
|
currentImageUuid,
|
||||||
|
areMoreImagesAvailable,
|
||||||
|
shouldPinGallery,
|
||||||
|
shouldShowGallery,
|
||||||
|
galleryScrollPosition,
|
||||||
|
galleryImageMinimumWidth,
|
||||||
|
galleryGridTemplateColumns,
|
||||||
|
activeTabName,
|
||||||
|
} = useAppSelector(imageGallerySelector);
|
||||||
|
|
||||||
|
const [gallerySize, setGallerySize] = useState<Size>({
|
||||||
|
width: '300',
|
||||||
|
height: '100%',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [galleryMaxSize, setGalleryMaxSize] = useState<Size>({
|
||||||
|
width: '590', // keep max at 590 for any tab
|
||||||
|
height: '100%',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [galleryMinSize, setGalleryMinSize] = useState<Size>({
|
||||||
|
width: '300', // keep max at 590 for any tab
|
||||||
|
height: '100%',
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeTabName === 'inpainting' && shouldPinGallery) {
|
||||||
|
setGalleryMinSize((prevSize) => {
|
||||||
|
return { ...prevSize, width: '200' };
|
||||||
|
});
|
||||||
|
setGalleryMaxSize((prevSize) => {
|
||||||
|
return { ...prevSize, width: '200' };
|
||||||
|
});
|
||||||
|
setGallerySize((prevSize) => {
|
||||||
|
return { ...prevSize, width: Math.min(Number(prevSize.width), 200) };
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setGalleryMaxSize((prevSize) => {
|
||||||
|
return { ...prevSize, width: '590', height: '100%' };
|
||||||
|
});
|
||||||
|
setGallerySize((prevSize) => {
|
||||||
|
return { ...prevSize, width: Math.min(Number(prevSize.width), 590) };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [activeTabName, shouldPinGallery, setGalleryMaxSize]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!shouldPinGallery) {
|
||||||
|
setGalleryMaxSize((prevSize) => {
|
||||||
|
// calculate vh in px
|
||||||
|
return {
|
||||||
|
...prevSize,
|
||||||
|
width: window.innerWidth,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [shouldPinGallery]);
|
||||||
|
|
||||||
|
const galleryRef = useRef<HTMLDivElement>(null);
|
||||||
|
const galleryContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const timeoutIdRef = useRef<number | null>(null);
|
||||||
|
|
||||||
|
const handleSetShouldPinGallery = () => {
|
||||||
|
dispatch(setShouldPinGallery(!shouldPinGallery));
|
||||||
|
setGallerySize({
|
||||||
|
...gallerySize,
|
||||||
|
height: shouldPinGallery ? '100vh' : '100%',
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGalleryClose = () => {
|
const handleToggleGallery = () => {
|
||||||
|
shouldShowGallery ? handleCloseGallery() : handleOpenGallery();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenGallery = () => {
|
||||||
|
dispatch(setShouldShowGallery(true));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseGallery = () => {
|
||||||
|
dispatch(
|
||||||
|
setGalleryScrollPosition(
|
||||||
|
galleryContainerRef.current ? galleryContainerRef.current.scrollTop : 0
|
||||||
|
)
|
||||||
|
);
|
||||||
dispatch(setShouldShowGallery(false));
|
dispatch(setShouldShowGallery(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -38,92 +123,281 @@ export default function ImageGallery() {
|
|||||||
dispatch(requestImages());
|
dispatch(requestImages());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleChangeGalleryImageMinimumWidth = (v: number) => {
|
||||||
|
dispatch(setGalleryImageMinimumWidth(v));
|
||||||
|
};
|
||||||
|
|
||||||
|
const setCloseGalleryTimer = () => {
|
||||||
|
timeoutIdRef.current = window.setTimeout(() => handleCloseGallery(), 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelCloseGalleryTimer = () => {
|
||||||
|
timeoutIdRef.current && window.clearTimeout(timeoutIdRef.current);
|
||||||
|
};
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'g',
|
'g',
|
||||||
() => {
|
() => {
|
||||||
handleShowGalleryToggle();
|
handleToggleGallery();
|
||||||
},
|
},
|
||||||
[shouldShowGallery]
|
[shouldShowGallery]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useHotkeys('left', () => {
|
||||||
|
dispatch(selectPrevImage());
|
||||||
|
});
|
||||||
|
|
||||||
|
useHotkeys('right', () => {
|
||||||
|
dispatch(selectNextImage());
|
||||||
|
});
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'left',
|
'shift+p',
|
||||||
() => {
|
() => {
|
||||||
dispatch(selectPrevImage());
|
handleSetShouldPinGallery();
|
||||||
},
|
},
|
||||||
[]
|
[shouldPinGallery]
|
||||||
|
);
|
||||||
|
|
||||||
|
const IMAGE_SIZE_STEP = 32;
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'shift+up',
|
||||||
|
() => {
|
||||||
|
if (galleryImageMinimumWidth >= 256) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (galleryImageMinimumWidth < 256) {
|
||||||
|
const newMinWidth = galleryImageMinimumWidth + IMAGE_SIZE_STEP;
|
||||||
|
if (newMinWidth <= 256) {
|
||||||
|
dispatch(setGalleryImageMinimumWidth(newMinWidth));
|
||||||
|
toast({
|
||||||
|
title: `Gallery Thumbnail Size set to ${newMinWidth}`,
|
||||||
|
status: 'success',
|
||||||
|
duration: 1000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dispatch(setGalleryImageMinimumWidth(256));
|
||||||
|
toast({
|
||||||
|
title: `Gallery Thumbnail Size set to 256`,
|
||||||
|
status: 'success',
|
||||||
|
duration: 1000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[galleryImageMinimumWidth]
|
||||||
);
|
);
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'right',
|
'shift+down',
|
||||||
() => {
|
() => {
|
||||||
dispatch(selectNextImage());
|
if (galleryImageMinimumWidth <= 32) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (galleryImageMinimumWidth > 32) {
|
||||||
|
const newMinWidth = galleryImageMinimumWidth - IMAGE_SIZE_STEP;
|
||||||
|
if (newMinWidth > 32) {
|
||||||
|
dispatch(setGalleryImageMinimumWidth(newMinWidth));
|
||||||
|
toast({
|
||||||
|
title: `Gallery Thumbnail Size set to ${newMinWidth}`,
|
||||||
|
status: 'success',
|
||||||
|
duration: 1000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dispatch(setGalleryImageMinimumWidth(32));
|
||||||
|
toast({
|
||||||
|
title: `Gallery Thumbnail Size set to 32`,
|
||||||
|
status: 'success',
|
||||||
|
duration: 1000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[]
|
[galleryImageMinimumWidth]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'shift+r',
|
||||||
|
() => {
|
||||||
|
dispatch(setGalleryImageMinimumWidth(64));
|
||||||
|
toast({
|
||||||
|
title: `Reset Gallery Image Size`,
|
||||||
|
status: 'success',
|
||||||
|
duration: 2500,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[galleryImageMinimumWidth]
|
||||||
|
);
|
||||||
|
|
||||||
|
// set gallery scroll position
|
||||||
|
useEffect(() => {
|
||||||
|
if (!galleryContainerRef.current) return;
|
||||||
|
galleryContainerRef.current.scrollTop = galleryScrollPosition;
|
||||||
|
}, [galleryScrollPosition, shouldShowGallery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="image-gallery-area">
|
<CSSTransition
|
||||||
{!shouldShowGallery && (
|
nodeRef={galleryRef}
|
||||||
<IAIIconButton
|
in={shouldShowGallery}
|
||||||
tooltip="Show Gallery"
|
unmountOnExit
|
||||||
tooltipPlacement="top"
|
timeout={200}
|
||||||
aria-label="Show Gallery"
|
classNames="image-gallery-area"
|
||||||
onClick={handleShowGalleryToggle}
|
>
|
||||||
className="image-gallery-popup-btn"
|
<div
|
||||||
>
|
className="image-gallery-area"
|
||||||
<MdPhotoLibrary />
|
data-pinned={shouldPinGallery}
|
||||||
</IAIIconButton>
|
ref={galleryRef}
|
||||||
)}
|
onMouseLeave={!shouldPinGallery ? setCloseGalleryTimer : undefined}
|
||||||
|
onMouseEnter={!shouldPinGallery ? cancelCloseGalleryTimer : undefined}
|
||||||
{shouldShowGallery && (
|
>
|
||||||
<Resizable
|
<Resizable
|
||||||
defaultSize={{ width: '300', height: '100%' }}
|
minWidth={galleryMinSize.width}
|
||||||
minWidth={'300'}
|
maxWidth={galleryMaxSize.width}
|
||||||
maxWidth={activeTab == 1 ? '300' : '600'}
|
maxHeight={'100%'}
|
||||||
className="image-gallery-popup"
|
className={'image-gallery-popup'}
|
||||||
|
handleStyles={{ left: { width: '20px' } }}
|
||||||
|
enable={{
|
||||||
|
top: false,
|
||||||
|
right: false,
|
||||||
|
bottom: false,
|
||||||
|
left: true,
|
||||||
|
topRight: false,
|
||||||
|
bottomRight: false,
|
||||||
|
bottomLeft: false,
|
||||||
|
topLeft: false,
|
||||||
|
}}
|
||||||
|
size={gallerySize}
|
||||||
|
onResizeStop={(
|
||||||
|
_event: MouseEvent | TouchEvent,
|
||||||
|
_direction: Direction,
|
||||||
|
elementRef: HTMLElement,
|
||||||
|
delta: NumberSize
|
||||||
|
) => {
|
||||||
|
setGallerySize({
|
||||||
|
width: Number(gallerySize.width) + delta.width,
|
||||||
|
height: '100%',
|
||||||
|
});
|
||||||
|
elementRef.removeAttribute('data-resize-alert');
|
||||||
|
}}
|
||||||
|
onResize={(
|
||||||
|
_event: MouseEvent | TouchEvent,
|
||||||
|
_direction: Direction,
|
||||||
|
elementRef: HTMLElement,
|
||||||
|
delta: NumberSize
|
||||||
|
) => {
|
||||||
|
const newWidth = Number(gallerySize.width) + delta.width;
|
||||||
|
if (newWidth >= galleryMaxSize.width) {
|
||||||
|
elementRef.setAttribute('data-resize-alert', 'true');
|
||||||
|
} else {
|
||||||
|
elementRef.removeAttribute('data-resize-alert');
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="image-gallery-header">
|
<div className="image-gallery-header">
|
||||||
<h1>Your Invocations</h1>
|
{activeTabName !== 'inpainting' ? (
|
||||||
<IconButton
|
<>
|
||||||
|
<h1>Your Invocations</h1>
|
||||||
|
<Spacer />
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<IAIPopover
|
||||||
|
trigger="click"
|
||||||
|
hasArrow={activeTabName === 'inpainting' ? false : true}
|
||||||
|
triggerComponent={
|
||||||
|
<IAIIconButton
|
||||||
|
size={'sm'}
|
||||||
|
aria-label={'Gallery Settings'}
|
||||||
|
icon={<FaWrench />}
|
||||||
|
className="image-gallery-icon-btn"
|
||||||
|
cursor={'pointer'}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
styleClass="image-gallery-size-popover"
|
||||||
|
>
|
||||||
|
<IAISlider
|
||||||
|
value={galleryImageMinimumWidth}
|
||||||
|
onChange={handleChangeGalleryImageMinimumWidth}
|
||||||
|
min={32}
|
||||||
|
max={256}
|
||||||
|
width={100}
|
||||||
|
label={'Image Size'}
|
||||||
|
formLabelProps={{ style: { fontSize: '0.9rem' } }}
|
||||||
|
sliderThumbTooltipProps={{
|
||||||
|
label: `${galleryImageMinimumWidth}px`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IAIIconButton
|
||||||
|
size={'sm'}
|
||||||
|
aria-label={'Reset'}
|
||||||
|
tooltip={'Reset Size'}
|
||||||
|
onClick={() => dispatch(setGalleryImageMinimumWidth(64))}
|
||||||
|
icon={<BiReset />}
|
||||||
|
data-selected={shouldPinGallery}
|
||||||
|
styleClass="image-gallery-icon-btn"
|
||||||
|
/>
|
||||||
|
</IAIPopover>
|
||||||
|
|
||||||
|
<IAIIconButton
|
||||||
|
size={'sm'}
|
||||||
|
aria-label={'Pin Gallery'}
|
||||||
|
tooltip={'Pin Gallery (Shift+P)'}
|
||||||
|
onClick={handleSetShouldPinGallery}
|
||||||
|
icon={<BsPinAngleFill />}
|
||||||
|
data-selected={shouldPinGallery}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IAIIconButton
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
aria-label={'Close Gallery'}
|
aria-label={'Close Gallery'}
|
||||||
onClick={handleGalleryClose}
|
tooltip={'Close Gallery (G)'}
|
||||||
className="image-gallery-close-btn"
|
onClick={handleCloseGallery}
|
||||||
|
className="image-gallery-icon-btn"
|
||||||
icon={<MdClear />}
|
icon={<MdClear />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="image-gallery-container">
|
<div className="image-gallery-container" ref={galleryContainerRef}>
|
||||||
{images.length ? (
|
{images.length || areMoreImagesAvailable ? (
|
||||||
<div className="image-gallery">
|
<>
|
||||||
{images.map((image) => {
|
<div
|
||||||
const { uuid } = image;
|
className="image-gallery"
|
||||||
const isSelected = currentImageUuid === uuid;
|
style={{ gridTemplateColumns: galleryGridTemplateColumns }}
|
||||||
return (
|
>
|
||||||
<HoverableImage
|
{images.map((image) => {
|
||||||
key={uuid}
|
const { uuid } = image;
|
||||||
image={image}
|
const isSelected = currentImageUuid === uuid;
|
||||||
isSelected={isSelected}
|
return (
|
||||||
/>
|
<HoverableImage
|
||||||
);
|
key={uuid}
|
||||||
})}
|
image={image}
|
||||||
</div>
|
isSelected={isSelected}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleClickLoadMore}
|
||||||
|
isDisabled={!areMoreImagesAvailable}
|
||||||
|
className="image-gallery-load-more-btn"
|
||||||
|
>
|
||||||
|
{areMoreImagesAvailable ? 'Load More' : 'All Images Loaded'}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="image-gallery-container-placeholder">
|
<div className="image-gallery-container-placeholder">
|
||||||
<MdPhotoLibrary />
|
<MdPhotoLibrary />
|
||||||
<p>No Images In Gallery</p>
|
<p>No Images In Gallery</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Button
|
|
||||||
onClick={handleClickLoadMore}
|
|
||||||
isDisabled={!areMoreImagesAvailable}
|
|
||||||
className="image-gallery-load-more-btn"
|
|
||||||
>
|
|
||||||
{areMoreImagesAvailable ? 'Load More' : 'All Images Loaded'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</Resizable>
|
</Resizable>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</CSSTransition>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
@use '../../../styles/Mixins/' as *;
|
@use '../../../styles/Mixins/' as *;
|
||||||
|
|
||||||
.image-metadata-viewer {
|
.image-metadata-viewer {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
background-color: var(--metadata-bg-color);
|
background-color: var(--metadata-bg-color);
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
max-height: $app-metadata-height;
|
max-height: $app-metadata-height;
|
||||||
|
height: 100%;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
20
frontend/src/features/gallery/ShowHideGalleryButton.scss
Normal file
20
frontend/src/features/gallery/ShowHideGalleryButton.scss
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
@use '../../styles/Mixins/' as *;
|
||||||
|
|
||||||
|
.show-hide-gallery-button {
|
||||||
|
position: absolute !important;
|
||||||
|
top: 50%;
|
||||||
|
right: -1rem;
|
||||||
|
transform: translate(0, -50%);
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
border-radius: 0.5rem 0 0 0.5rem !important;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
|
||||||
|
@include Button(
|
||||||
|
$btn-width: 1rem,
|
||||||
|
$btn-height: 12rem,
|
||||||
|
$icon-size: 20px,
|
||||||
|
$btn-color: var(--btn-grey),
|
||||||
|
$btn-color-hover: var(--btn-grey-hover)
|
||||||
|
);
|
||||||
|
}
|
57
frontend/src/features/gallery/ShowHideGalleryButton.tsx
Normal file
57
frontend/src/features/gallery/ShowHideGalleryButton.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { MdPhotoLibrary } from 'react-icons/md';
|
||||||
|
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
|
||||||
|
import IAIIconButton from '../../common/components/IAIIconButton';
|
||||||
|
import { setShouldShowGallery } from '../gallery/gallerySlice';
|
||||||
|
import { selectNextImage, selectPrevImage } from './gallerySlice';
|
||||||
|
|
||||||
|
const ShowHideGalleryButton = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const { shouldPinGallery, shouldShowGallery } = useAppSelector(
|
||||||
|
(state: RootState) => state.gallery
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleShowGalleryToggle = () => {
|
||||||
|
dispatch(setShouldShowGallery(!shouldShowGallery));
|
||||||
|
};
|
||||||
|
|
||||||
|
// useHotkeys(
|
||||||
|
// 'g',
|
||||||
|
// () => {
|
||||||
|
// handleShowGalleryToggle();
|
||||||
|
// },
|
||||||
|
// [shouldShowGallery]
|
||||||
|
// );
|
||||||
|
|
||||||
|
// useHotkeys(
|
||||||
|
// 'left',
|
||||||
|
// () => {
|
||||||
|
// dispatch(selectPrevImage());
|
||||||
|
// },
|
||||||
|
// []
|
||||||
|
// );
|
||||||
|
|
||||||
|
// useHotkeys(
|
||||||
|
// 'right',
|
||||||
|
// () => {
|
||||||
|
// dispatch(selectNextImage());
|
||||||
|
// },
|
||||||
|
// []
|
||||||
|
// );
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IAIIconButton
|
||||||
|
tooltip="Show Gallery (G)"
|
||||||
|
tooltipPlacement="top"
|
||||||
|
aria-label="Show Gallery"
|
||||||
|
onClick={handleShowGalleryToggle}
|
||||||
|
styleClass="show-hide-gallery-button"
|
||||||
|
onMouseOver={!shouldPinGallery ? handleShowGalleryToggle : undefined}
|
||||||
|
>
|
||||||
|
<MdPhotoLibrary />
|
||||||
|
</IAIIconButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShowHideGalleryButton;
|
@ -11,12 +11,20 @@ export interface GalleryState {
|
|||||||
areMoreImagesAvailable: boolean;
|
areMoreImagesAvailable: boolean;
|
||||||
latest_mtime?: number;
|
latest_mtime?: number;
|
||||||
earliest_mtime?: number;
|
earliest_mtime?: number;
|
||||||
|
shouldPinGallery: boolean;
|
||||||
|
shouldShowGallery: boolean;
|
||||||
|
galleryScrollPosition: number;
|
||||||
|
galleryImageMinimumWidth: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: GalleryState = {
|
const initialState: GalleryState = {
|
||||||
currentImageUuid: '',
|
currentImageUuid: '',
|
||||||
images: [],
|
images: [],
|
||||||
areMoreImagesAvailable: true,
|
areMoreImagesAvailable: true,
|
||||||
|
shouldPinGallery: true,
|
||||||
|
shouldShowGallery: true,
|
||||||
|
galleryScrollPosition: 0,
|
||||||
|
galleryImageMinimumWidth: 64,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const gallerySlice = createSlice({
|
export const gallerySlice = createSlice({
|
||||||
@ -151,6 +159,18 @@ export const gallerySlice = createSlice({
|
|||||||
state.areMoreImagesAvailable = areMoreImagesAvailable;
|
state.areMoreImagesAvailable = areMoreImagesAvailable;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setShouldPinGallery: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.shouldPinGallery = action.payload;
|
||||||
|
},
|
||||||
|
setShouldShowGallery: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.shouldShowGallery = action.payload;
|
||||||
|
},
|
||||||
|
setGalleryScrollPosition: (state, action: PayloadAction<number>) => {
|
||||||
|
state.galleryScrollPosition = action.payload;
|
||||||
|
},
|
||||||
|
setGalleryImageMinimumWidth: (state, action: PayloadAction<number>) => {
|
||||||
|
state.galleryImageMinimumWidth = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -163,6 +183,10 @@ export const {
|
|||||||
setIntermediateImage,
|
setIntermediateImage,
|
||||||
selectNextImage,
|
selectNextImage,
|
||||||
selectPrevImage,
|
selectPrevImage,
|
||||||
|
setShouldPinGallery,
|
||||||
|
setShouldShowGallery,
|
||||||
|
setGalleryScrollPosition,
|
||||||
|
setGalleryImageMinimumWidth,
|
||||||
} = gallerySlice.actions;
|
} = gallerySlice.actions;
|
||||||
|
|
||||||
export default gallerySlice.reducer;
|
export default gallerySlice.reducer;
|
||||||
|
43
frontend/src/features/gallery/gallerySliceSelectors.ts
Normal file
43
frontend/src/features/gallery/gallerySliceSelectors.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { RootState } from '../../app/store';
|
||||||
|
import { OptionsState } from '../options/optionsSlice';
|
||||||
|
import { tabMap } from '../tabs/InvokeTabs';
|
||||||
|
import { GalleryState } from './gallerySlice';
|
||||||
|
|
||||||
|
export const imageGallerySelector = createSelector(
|
||||||
|
[(state: RootState) => state.gallery, (state: RootState) => state.options],
|
||||||
|
(gallery: GalleryState, options: OptionsState) => {
|
||||||
|
const {
|
||||||
|
images,
|
||||||
|
currentImageUuid,
|
||||||
|
areMoreImagesAvailable,
|
||||||
|
shouldPinGallery,
|
||||||
|
shouldShowGallery,
|
||||||
|
galleryScrollPosition,
|
||||||
|
galleryImageMinimumWidth,
|
||||||
|
} = gallery;
|
||||||
|
|
||||||
|
const { activeTab } = options;
|
||||||
|
|
||||||
|
return {
|
||||||
|
images,
|
||||||
|
currentImageUuid,
|
||||||
|
areMoreImagesAvailable,
|
||||||
|
shouldPinGallery,
|
||||||
|
shouldShowGallery,
|
||||||
|
galleryScrollPosition,
|
||||||
|
galleryImageMinimumWidth,
|
||||||
|
galleryGridTemplateColumns: `repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, auto))`,
|
||||||
|
activeTabName: tabMap[activeTab],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const hoverableImageSelector = createSelector(
|
||||||
|
(state: RootState) => state.options,
|
||||||
|
(options: OptionsState) => {
|
||||||
|
return {
|
||||||
|
activeTabName: tabMap[options.activeTab],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
@ -8,7 +8,7 @@ import {
|
|||||||
import IAISwitch from '../../../../common/components/IAISwitch';
|
import IAISwitch from '../../../../common/components/IAISwitch';
|
||||||
import { setShouldRunFacetool } from '../../optionsSlice';
|
import { setShouldRunFacetool } from '../../optionsSlice';
|
||||||
|
|
||||||
export default function FaceRestore() {
|
export default function FaceRestoreHeader() {
|
||||||
const isGFPGANAvailable = useAppSelector(
|
const isGFPGANAvailable = useAppSelector(
|
||||||
(state: RootState) => state.system.isGFPGANAvailable
|
(state: RootState) => state.system.isGFPGANAvailable
|
||||||
);
|
);
|
@ -1,39 +0,0 @@
|
|||||||
import { Flex } from '@chakra-ui/layout';
|
|
||||||
import React, { ChangeEvent } from 'react';
|
|
||||||
import {
|
|
||||||
RootState,
|
|
||||||
useAppDispatch,
|
|
||||||
useAppSelector,
|
|
||||||
} from '../../../../app/store';
|
|
||||||
import IAISwitch from '../../../../common/components/IAISwitch';
|
|
||||||
import { setShouldUseInitImage } from '../../optionsSlice';
|
|
||||||
|
|
||||||
export default function ImageToImageAccordion() {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
const initialImagePath = useAppSelector(
|
|
||||||
(state: RootState) => state.options.initialImagePath
|
|
||||||
);
|
|
||||||
|
|
||||||
const shouldUseInitImage = useAppSelector(
|
|
||||||
(state: RootState) => state.options.shouldUseInitImage
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleChangeShouldUseInitImage = (e: ChangeEvent<HTMLInputElement>) =>
|
|
||||||
dispatch(setShouldUseInitImage(e.target.checked));
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
justifyContent={'space-between'}
|
|
||||||
alignItems={'center'}
|
|
||||||
width={'100%'}
|
|
||||||
mr={2}
|
|
||||||
>
|
|
||||||
<p>Image to Image</p>
|
|
||||||
<IAISwitch
|
|
||||||
isDisabled={!initialImagePath}
|
|
||||||
isChecked={shouldUseInitImage}
|
|
||||||
onChange={handleChangeShouldUseInitImage}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
import { Flex } from '@chakra-ui/react';
|
|
||||||
import InitAndMaskImage from '../../InitAndMaskImage';
|
|
||||||
import ImageFit from './ImageFit';
|
|
||||||
import ImageToImageStrength from './ImageToImageStrength';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Options for img2img generation (strength, fit, init/mask upload).
|
|
||||||
*/
|
|
||||||
const ImageToImageOptions = () => {
|
|
||||||
return (
|
|
||||||
<Flex direction={'column'} gap={2}>
|
|
||||||
<ImageToImageStrength />
|
|
||||||
<ImageFit />
|
|
||||||
<InitAndMaskImage />
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ImageToImageOptions;
|
|
@ -30,7 +30,7 @@ export default function ImageToImageStrength(props: ImageToImageStrengthProps) {
|
|||||||
max={0.99}
|
max={0.99}
|
||||||
onChange={handleChangeStrength}
|
onChange={handleChangeStrength}
|
||||||
value={img2imgStrength}
|
value={img2imgStrength}
|
||||||
width="90px"
|
width="100%"
|
||||||
isInteger={false}
|
isInteger={false}
|
||||||
styleClass={styleClass}
|
styleClass={styleClass}
|
||||||
/>
|
/>
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
.inpainting-bounding-box-dimensions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: 1rem;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inpainting-bounding-box-dimensions-slider-numberinput {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
column-gap: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
import { FormLabel } from '@chakra-ui/react';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import {
|
||||||
|
RootState,
|
||||||
|
useAppDispatch,
|
||||||
|
useAppSelector,
|
||||||
|
} from '../../../../app/store';
|
||||||
|
import IAINumberInput from '../../../../common/components/IAINumberInput';
|
||||||
|
import IAISlider from '../../../../common/components/IAISlider';
|
||||||
|
import { roundDownToMultiple } from '../../../../common/util/roundDownToMultiple';
|
||||||
|
import {
|
||||||
|
InpaintingState,
|
||||||
|
setBoundingBoxDimensions,
|
||||||
|
} from '../../../tabs/Inpainting/inpaintingSlice';
|
||||||
|
|
||||||
|
const boundingBoxDimensionsSelector = createSelector(
|
||||||
|
(state: RootState) => state.inpainting,
|
||||||
|
(inpainting: InpaintingState) => {
|
||||||
|
const { canvasDimensions, boundingBoxDimensions } = inpainting;
|
||||||
|
return { canvasDimensions, boundingBoxDimensions };
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const BoundingBoxDimensions = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { canvasDimensions, boundingBoxDimensions } = useAppSelector(
|
||||||
|
boundingBoxDimensionsSelector
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeBoundingBoxWidth = (v: number) => {
|
||||||
|
dispatch(setBoundingBoxDimensions({ ...boundingBoxDimensions, width: v }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeBoundingBoxHeight = (v: number) => {
|
||||||
|
dispatch(setBoundingBoxDimensions({ ...boundingBoxDimensions, height: v }));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="inpainting-bounding-box-dimensions">
|
||||||
|
Inpainting Bounding Box
|
||||||
|
<div className="inpainting-bounding-box-dimensions-slider-numberinput">
|
||||||
|
<IAISlider
|
||||||
|
label="Width"
|
||||||
|
width={'8rem'}
|
||||||
|
min={64}
|
||||||
|
max={roundDownToMultiple(canvasDimensions.width, 64)}
|
||||||
|
step={64}
|
||||||
|
value={boundingBoxDimensions.width}
|
||||||
|
onChange={handleChangeBoundingBoxWidth}
|
||||||
|
/>
|
||||||
|
<IAINumberInput
|
||||||
|
value={boundingBoxDimensions.width}
|
||||||
|
onChange={handleChangeBoundingBoxWidth}
|
||||||
|
min={64}
|
||||||
|
max={roundDownToMultiple(canvasDimensions.width, 64)}
|
||||||
|
step={64}
|
||||||
|
width={'5.5rem'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="inpainting-bounding-box-dimensions-slider-numberinput">
|
||||||
|
<IAISlider
|
||||||
|
label="Height"
|
||||||
|
width={'8rem'}
|
||||||
|
min={64}
|
||||||
|
max={roundDownToMultiple(canvasDimensions.height, 64)}
|
||||||
|
step={64}
|
||||||
|
value={boundingBoxDimensions.height}
|
||||||
|
onChange={handleChangeBoundingBoxHeight}
|
||||||
|
/>
|
||||||
|
<IAINumberInput
|
||||||
|
value={boundingBoxDimensions.height}
|
||||||
|
onChange={handleChangeBoundingBoxHeight}
|
||||||
|
min={64}
|
||||||
|
max={roundDownToMultiple(canvasDimensions.height, 64)}
|
||||||
|
step={64}
|
||||||
|
width={'5.5rem'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BoundingBoxDimensions;
|
@ -1,12 +1,15 @@
|
|||||||
import { Flex } from '@chakra-ui/react';
|
import { Flex } from '@chakra-ui/react';
|
||||||
import { RootState } from '../../app/store';
|
|
||||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
|
||||||
import { setHiresFix } from './optionsSlice';
|
|
||||||
import { ChangeEvent } from 'react';
|
import { ChangeEvent } from 'react';
|
||||||
import IAISwitch from '../../common/components/IAISwitch';
|
import {
|
||||||
|
RootState,
|
||||||
|
useAppDispatch,
|
||||||
|
useAppSelector,
|
||||||
|
} from '../../../../app/store';
|
||||||
|
import IAISwitch from '../../../../common/components/IAISwitch';
|
||||||
|
import { setHiresFix } from '../../optionsSlice';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Image output options. Includes width, height, seamless tiling.
|
* Hires Fix Toggle
|
||||||
*/
|
*/
|
||||||
const HiresOptions = () => {
|
const HiresOptions = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@ -16,7 +19,6 @@ const HiresOptions = () => {
|
|||||||
const handleChangeHiresFix = (e: ChangeEvent<HTMLInputElement>) =>
|
const handleChangeHiresFix = (e: ChangeEvent<HTMLInputElement>) =>
|
||||||
dispatch(setHiresFix(e.target.checked));
|
dispatch(setHiresFix(e.target.checked));
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex gap={2} direction={'column'}>
|
<Flex gap={2} direction={'column'}>
|
||||||
<IAISwitch
|
<IAISwitch
|
@ -0,0 +1,11 @@
|
|||||||
|
import { Box } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
const OutputHeader = () => {
|
||||||
|
return (
|
||||||
|
<Box flex="1" textAlign="left">
|
||||||
|
Seed
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OutputHeader;
|
@ -1,10 +1,8 @@
|
|||||||
import { Flex } from '@chakra-ui/react';
|
import { Flex } from '@chakra-ui/react';
|
||||||
|
|
||||||
import HiresOptions from './HiresOptions';
|
import HiresOptions from './HiresOptions';
|
||||||
import SeamlessOptions from './SeamlessOptions';
|
import SeamlessOptions from './SeamlessOptions';
|
||||||
|
|
||||||
const OutputOptions = () => {
|
const OutputOptions = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex gap={2} direction={'column'}>
|
<Flex gap={2} direction={'column'}>
|
||||||
<SeamlessOptions />
|
<SeamlessOptions />
|
@ -1,10 +1,16 @@
|
|||||||
import { Flex } from '@chakra-ui/react';
|
import { Flex } from '@chakra-ui/react';
|
||||||
import { RootState } from '../../app/store';
|
|
||||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
|
||||||
import { setSeamless } from './optionsSlice';
|
|
||||||
import { ChangeEvent } from 'react';
|
import { ChangeEvent } from 'react';
|
||||||
import IAISwitch from '../../common/components/IAISwitch';
|
import {
|
||||||
|
RootState,
|
||||||
|
useAppDispatch,
|
||||||
|
useAppSelector,
|
||||||
|
} from '../../../../app/store';
|
||||||
|
import IAISwitch from '../../../../common/components/IAISwitch';
|
||||||
|
import { setSeamless } from '../../optionsSlice';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seamless tiling toggle
|
||||||
|
*/
|
||||||
const SeamlessOptions = () => {
|
const SeamlessOptions = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
@ -25,4 +31,4 @@ const SeamlessOptions = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SeamlessOptions;
|
export default SeamlessOptions;
|
@ -0,0 +1,11 @@
|
|||||||
|
import { Box } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
const SeedHeader = () => {
|
||||||
|
return (
|
||||||
|
<Box flex="1" textAlign="left">
|
||||||
|
Seed
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SeedHeader;
|
@ -8,7 +8,7 @@ import {
|
|||||||
import IAISwitch from '../../../../common/components/IAISwitch';
|
import IAISwitch from '../../../../common/components/IAISwitch';
|
||||||
import { setShouldRunESRGAN } from '../../optionsSlice';
|
import { setShouldRunESRGAN } from '../../optionsSlice';
|
||||||
|
|
||||||
export default function Upscale() {
|
export default function UpscaleHeader() {
|
||||||
const isESRGANAvailable = useAppSelector(
|
const isESRGANAvailable = useAppSelector(
|
||||||
(state: RootState) => state.system.isESRGANAvailable
|
(state: RootState) => state.system.isESRGANAvailable
|
||||||
);
|
);
|
@ -2,7 +2,7 @@ import { Flex } from '@chakra-ui/react';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import GenerateVariations from './GenerateVariations';
|
import GenerateVariations from './GenerateVariations';
|
||||||
|
|
||||||
export default function Variations() {
|
export default function VariationsHeader() {
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
justifyContent={'space-between'}
|
justifyContent={'space-between'}
|
@ -1,20 +0,0 @@
|
|||||||
.checkerboard {
|
|
||||||
background-position: 0px 0px, 10px 10px;
|
|
||||||
background-size: 20px 20px;
|
|
||||||
background-image: linear-gradient(
|
|
||||||
45deg,
|
|
||||||
#eee 25%,
|
|
||||||
transparent 25%,
|
|
||||||
transparent 75%,
|
|
||||||
#eee 75%,
|
|
||||||
#eee 100%
|
|
||||||
),
|
|
||||||
linear-gradient(
|
|
||||||
45deg,
|
|
||||||
#eee 25%,
|
|
||||||
white 25%,
|
|
||||||
white 75%,
|
|
||||||
#eee 75%,
|
|
||||||
#eee 100%
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,70 +0,0 @@
|
|||||||
import { Flex, Image } from '@chakra-ui/react';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
|
||||||
import { RootState } from '../../app/store';
|
|
||||||
import { OptionsState, setInitialImagePath, setMaskPath } from './optionsSlice';
|
|
||||||
import './InitAndMaskImage.css';
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { isEqual } from 'lodash';
|
|
||||||
import InitAndMaskUploadButtons from './InitAndMaskUploadButtons';
|
|
||||||
|
|
||||||
const optionsSelector = createSelector(
|
|
||||||
(state: RootState) => state.options,
|
|
||||||
(options: OptionsState) => {
|
|
||||||
return {
|
|
||||||
initialImagePath: options.initialImagePath,
|
|
||||||
maskPath: options.maskPath,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
{ memoizeOptions: { resultEqualityCheck: isEqual } }
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays init and mask images and buttons to upload/delete them.
|
|
||||||
*/
|
|
||||||
const InitAndMaskImage = () => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const { initialImagePath, maskPath } = useAppSelector(optionsSelector);
|
|
||||||
const [shouldShowMask, setShouldShowMask] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const handleInitImageOnError = () => {
|
|
||||||
dispatch(setInitialImagePath(''));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMaskImageOnError = () => {
|
|
||||||
dispatch(setMaskPath(''));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex direction={'column'} alignItems={'center'} gap={2}>
|
|
||||||
<InitAndMaskUploadButtons setShouldShowMask={setShouldShowMask} />
|
|
||||||
{initialImagePath && (
|
|
||||||
<Flex position={'relative'} width={'100%'}>
|
|
||||||
<Image
|
|
||||||
fit={'contain'}
|
|
||||||
src={initialImagePath}
|
|
||||||
rounded={'md'}
|
|
||||||
className={'checkerboard'}
|
|
||||||
maxWidth={320}
|
|
||||||
onError={handleInitImageOnError}
|
|
||||||
/>
|
|
||||||
{shouldShowMask && maskPath && (
|
|
||||||
<Image
|
|
||||||
position={'absolute'}
|
|
||||||
top={0}
|
|
||||||
left={0}
|
|
||||||
maxWidth={320}
|
|
||||||
fit={'contain'}
|
|
||||||
src={maskPath}
|
|
||||||
rounded={'md'}
|
|
||||||
zIndex={1}
|
|
||||||
onError={handleMaskImageOnError}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default InitAndMaskImage;
|
|
@ -1,147 +0,0 @@
|
|||||||
import { Button, Flex, IconButton, useToast } from '@chakra-ui/react';
|
|
||||||
import { SyntheticEvent, useCallback } from 'react';
|
|
||||||
import { FaTrash, FaUpload } from 'react-icons/fa';
|
|
||||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
|
||||||
import { RootState } from '../../app/store';
|
|
||||||
import { OptionsState, setInitialImagePath, setMaskPath } from './optionsSlice';
|
|
||||||
import {
|
|
||||||
uploadInitialImage,
|
|
||||||
uploadMaskImage,
|
|
||||||
} from '../../app/socketio/actions';
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { isEqual } from 'lodash';
|
|
||||||
import ImageUploader from './ImageUploader';
|
|
||||||
import { FileRejection } from 'react-dropzone';
|
|
||||||
|
|
||||||
const optionsSelector = createSelector(
|
|
||||||
(state: RootState) => state.options,
|
|
||||||
(options: OptionsState) => {
|
|
||||||
return {
|
|
||||||
initialImagePath: options.initialImagePath,
|
|
||||||
maskPath: options.maskPath,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
{ memoizeOptions: { resultEqualityCheck: isEqual } }
|
|
||||||
);
|
|
||||||
|
|
||||||
type InitAndMaskUploadButtonsProps = {
|
|
||||||
setShouldShowMask: (b: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Init and mask image upload buttons.
|
|
||||||
*/
|
|
||||||
const InitAndMaskUploadButtons = ({
|
|
||||||
setShouldShowMask,
|
|
||||||
}: InitAndMaskUploadButtonsProps) => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const { initialImagePath, maskPath } = useAppSelector(optionsSelector);
|
|
||||||
|
|
||||||
// Use a toast to alert user when a file upload is rejected
|
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
// Clear the init and mask images
|
|
||||||
const handleClickResetInitialImage = (e: SyntheticEvent) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
dispatch(setInitialImagePath(''));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Clear the init and mask images
|
|
||||||
const handleClickResetMask = (e: SyntheticEvent) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
dispatch(setMaskPath(''));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle hover to view initial image and mask image
|
|
||||||
const handleMouseOverInitialImageUploadButton = () =>
|
|
||||||
setShouldShowMask(false);
|
|
||||||
const handleMouseOutInitialImageUploadButton = () => setShouldShowMask(true);
|
|
||||||
|
|
||||||
const handleMouseOverMaskUploadButton = () => setShouldShowMask(true);
|
|
||||||
const handleMouseOutMaskUploadButton = () => setShouldShowMask(true);
|
|
||||||
|
|
||||||
// Callbacks to for handling file upload attempts
|
|
||||||
const initImageFileAcceptedCallback = useCallback(
|
|
||||||
(file: File) => dispatch(uploadInitialImage(file)),
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
const maskImageFileAcceptedCallback = useCallback(
|
|
||||||
(file: File) => dispatch(uploadMaskImage(file)),
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
const fileRejectionCallback = useCallback(
|
|
||||||
(rejection: FileRejection) => {
|
|
||||||
const msg = rejection.errors.reduce(
|
|
||||||
(acc: string, cur: { message: string }) => acc + '\n' + cur.message,
|
|
||||||
''
|
|
||||||
);
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: 'Upload failed',
|
|
||||||
description: msg,
|
|
||||||
status: 'error',
|
|
||||||
isClosable: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[toast]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex gap={2} justifyContent={'space-between'} width={'100%'}>
|
|
||||||
<ImageUploader
|
|
||||||
fileAcceptedCallback={initImageFileAcceptedCallback}
|
|
||||||
fileRejectionCallback={fileRejectionCallback}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
size={'sm'}
|
|
||||||
fontSize={'md'}
|
|
||||||
fontWeight={'normal'}
|
|
||||||
onMouseOver={handleMouseOverInitialImageUploadButton}
|
|
||||||
onMouseOut={handleMouseOutInitialImageUploadButton}
|
|
||||||
leftIcon={<FaUpload />}
|
|
||||||
width={'100%'}
|
|
||||||
>
|
|
||||||
Image
|
|
||||||
</Button>
|
|
||||||
</ImageUploader>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
isDisabled={!initialImagePath}
|
|
||||||
size={'sm'}
|
|
||||||
aria-label={'Reset mask'}
|
|
||||||
onClick={handleClickResetInitialImage}
|
|
||||||
icon={<FaTrash />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ImageUploader
|
|
||||||
fileAcceptedCallback={maskImageFileAcceptedCallback}
|
|
||||||
fileRejectionCallback={fileRejectionCallback}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
isDisabled={!initialImagePath}
|
|
||||||
size={'sm'}
|
|
||||||
fontSize={'md'}
|
|
||||||
fontWeight={'normal'}
|
|
||||||
onMouseOver={handleMouseOverMaskUploadButton}
|
|
||||||
onMouseOut={handleMouseOutMaskUploadButton}
|
|
||||||
leftIcon={<FaUpload />}
|
|
||||||
width={'100%'}
|
|
||||||
>
|
|
||||||
Mask
|
|
||||||
</Button>
|
|
||||||
</ImageUploader>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
isDisabled={!maskPath}
|
|
||||||
size={'sm'}
|
|
||||||
aria-label={'Reset mask'}
|
|
||||||
onClick={handleClickResetMask}
|
|
||||||
icon={<FaTrash />}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default InitAndMaskUploadButtons;
|
|
@ -1,8 +1,10 @@
|
|||||||
|
import { Checkbox } from '@chakra-ui/react';
|
||||||
import React, { ChangeEvent } from 'react';
|
import React, { ChangeEvent } from 'react';
|
||||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||||
|
import IAICheckbox from '../../../common/components/IAICheckbox';
|
||||||
import { setShowAdvancedOptions } from '../optionsSlice';
|
import { setShowAdvancedOptions } from '../optionsSlice';
|
||||||
|
|
||||||
export default function MainAdvancedOptions() {
|
export default function MainAdvancedOptionsCheckbox() {
|
||||||
const showAdvancedOptions = useAppSelector(
|
const showAdvancedOptions = useAppSelector(
|
||||||
(state: RootState) => state.options.showAdvancedOptions
|
(state: RootState) => state.options.showAdvancedOptions
|
||||||
);
|
);
|
||||||
@ -12,15 +14,11 @@ export default function MainAdvancedOptions() {
|
|||||||
dispatch(setShowAdvancedOptions(e.target.checked));
|
dispatch(setShowAdvancedOptions(e.target.checked));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="advanced_options_checker">
|
<IAICheckbox
|
||||||
<input
|
label="Advanced Options"
|
||||||
type="checkbox"
|
styleClass="advanced-options-checkbox"
|
||||||
name="advanced_options"
|
onChange={handleShowAdvancedOptions}
|
||||||
id=""
|
isChecked={showAdvancedOptions}
|
||||||
onChange={handleShowAdvancedOptions}
|
/>
|
||||||
checked={showAdvancedOptions}
|
|
||||||
/>
|
|
||||||
<label htmlFor="advanced_options">Advanced Options</label>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -14,7 +14,7 @@ export default function MainCFGScale() {
|
|||||||
<IAINumberInput
|
<IAINumberInput
|
||||||
label="CFG Scale"
|
label="CFG Scale"
|
||||||
step={0.5}
|
step={0.5}
|
||||||
min={1}
|
min={1.01}
|
||||||
max={30}
|
max={30}
|
||||||
onChange={handleChangeCfgScale}
|
onChange={handleChangeCfgScale}
|
||||||
value={cfgScale}
|
value={cfgScale}
|
||||||
|
@ -2,11 +2,14 @@ import React, { ChangeEvent } from 'react';
|
|||||||
import { HEIGHTS } from '../../../app/constants';
|
import { HEIGHTS } from '../../../app/constants';
|
||||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||||
import IAISelect from '../../../common/components/IAISelect';
|
import IAISelect from '../../../common/components/IAISelect';
|
||||||
|
import { tabMap } from '../../tabs/InvokeTabs';
|
||||||
import { setHeight } from '../optionsSlice';
|
import { setHeight } from '../optionsSlice';
|
||||||
import { fontSize } from './MainOptions';
|
import { fontSize } from './MainOptions';
|
||||||
|
|
||||||
export default function MainHeight() {
|
export default function MainHeight() {
|
||||||
const height = useAppSelector((state: RootState) => state.options.height);
|
const { activeTab, height } = useAppSelector(
|
||||||
|
(state: RootState) => state.options
|
||||||
|
);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const handleChangeHeight = (e: ChangeEvent<HTMLSelectElement>) =>
|
const handleChangeHeight = (e: ChangeEvent<HTMLSelectElement>) =>
|
||||||
@ -14,6 +17,7 @@ export default function MainHeight() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<IAISelect
|
<IAISelect
|
||||||
|
isDisabled={tabMap[activeTab] === 'inpainting'}
|
||||||
label="Height"
|
label="Height"
|
||||||
value={height}
|
value={height}
|
||||||
flexGrow={1}
|
flexGrow={1}
|
||||||
|
@ -22,10 +22,10 @@
|
|||||||
grid-template-columns: auto !important;
|
grid-template-columns: auto !important;
|
||||||
row-gap: 0.4rem;
|
row-gap: 0.4rem;
|
||||||
|
|
||||||
.number-input-label,
|
.invokeai__number-input-form-label,
|
||||||
.iai-select-label {
|
.invokeai__select-label {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem !important;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,43 +40,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.advanced_options_checker {
|
.advanced-options-checkbox {
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, max-content);
|
|
||||||
column-gap: 0.5rem;
|
|
||||||
align-items: center;
|
|
||||||
background-color: var(--background-color-secondary);
|
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
border-radius: 0.5rem;
|
|
||||||
|
|
||||||
input[type='checkbox'] {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
background-color: var(--input-checkbox-bg);
|
|
||||||
width: 1rem;
|
|
||||||
height: 1rem;
|
|
||||||
border-radius: 0.2rem;
|
|
||||||
|
|
||||||
display: grid;
|
|
||||||
place-content: center;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
width: 1rem;
|
|
||||||
height: 1rem;
|
|
||||||
transform: scale(0);
|
|
||||||
transition: 120ms transform ease-in-out;
|
|
||||||
border-radius: 0.2rem;
|
|
||||||
box-shadow: inset 1rem 1rem var(--input-checkbox-checked-tick);
|
|
||||||
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:checked {
|
|
||||||
background-color: var(--input-checkbox-checked-bg);
|
|
||||||
&::before {
|
|
||||||
transform: scale(0.7);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,14 @@ import React, { ChangeEvent } from 'react';
|
|||||||
import { WIDTHS } from '../../../app/constants';
|
import { WIDTHS } from '../../../app/constants';
|
||||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||||
import IAISelect from '../../../common/components/IAISelect';
|
import IAISelect from '../../../common/components/IAISelect';
|
||||||
|
import { tabMap } from '../../tabs/InvokeTabs';
|
||||||
import { setWidth } from '../optionsSlice';
|
import { setWidth } from '../optionsSlice';
|
||||||
import { fontSize } from './MainOptions';
|
import { fontSize } from './MainOptions';
|
||||||
|
|
||||||
export default function MainWidth() {
|
export default function MainWidth() {
|
||||||
const width = useAppSelector((state: RootState) => state.options.width);
|
const { width, activeTab } = useAppSelector(
|
||||||
|
(state: RootState) => state.options
|
||||||
|
);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const handleChangeWidth = (e: ChangeEvent<HTMLSelectElement>) =>
|
const handleChangeWidth = (e: ChangeEvent<HTMLSelectElement>) =>
|
||||||
@ -14,6 +17,7 @@ export default function MainWidth() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<IAISelect
|
<IAISelect
|
||||||
|
isDisabled={tabMap[activeTab] === 'inpainting'}
|
||||||
label="Width"
|
label="Width"
|
||||||
value={width}
|
value={width}
|
||||||
flexGrow={1}
|
flexGrow={1}
|
||||||
|
@ -28,7 +28,7 @@ export default function CancelButton() {
|
|||||||
aria-label="Cancel"
|
aria-label="Cancel"
|
||||||
isDisabled={!isConnected || !isProcessing}
|
isDisabled={!isConnected || !isProcessing}
|
||||||
onClick={handleClickCancel}
|
onClick={handleClickCancel}
|
||||||
className="cancel-btn"
|
styleClass="cancel-btn"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,19 @@
|
|||||||
import React from 'react';
|
|
||||||
import { generateImage } from '../../../app/socketio/actions';
|
import { generateImage } from '../../../app/socketio/actions';
|
||||||
import { useAppDispatch } from '../../../app/store';
|
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||||
import IAIButton from '../../../common/components/IAIButton';
|
import IAIButton from '../../../common/components/IAIButton';
|
||||||
import useCheckParameters from '../../../common/hooks/useCheckParameters';
|
import useCheckParameters from '../../../common/hooks/useCheckParameters';
|
||||||
|
import { tabMap } from '../../tabs/InvokeTabs';
|
||||||
|
|
||||||
export default function InvokeButton() {
|
export default function InvokeButton() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const isReady = useCheckParameters();
|
const isReady = useCheckParameters();
|
||||||
|
|
||||||
|
const activeTab = useAppSelector(
|
||||||
|
(state: RootState) => state.options.activeTab
|
||||||
|
);
|
||||||
|
|
||||||
const handleClickGenerate = () => {
|
const handleClickGenerate = () => {
|
||||||
dispatch(generateImage());
|
dispatch(generateImage(tabMap[activeTab]));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -7,16 +7,16 @@
|
|||||||
|
|
||||||
.invoke-btn {
|
.invoke-btn {
|
||||||
@include Button(
|
@include Button(
|
||||||
$btn-color: var(--btn-purple),
|
$btn-color: var(--accent-color),
|
||||||
$btn-color-hover: var(--btn-purple-hover),
|
$btn-color-hover: var(--accent-color-hover),
|
||||||
$btn-width: 5rem
|
$btn-width: 5rem
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cancel-btn {
|
.cancel-btn {
|
||||||
@include Button(
|
@include Button(
|
||||||
$btn-color: var(--btn-red),
|
$btn-color: var(--destructive-color),
|
||||||
$btn-color-hover: var(--btn-red-hover),
|
$btn-color-hover: var(--destructive-color-hover),
|
||||||
$btn-width: 3rem
|
$btn-width: 3rem
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
border: 2px solid var(--prompt-border-color);
|
border: 2px solid var(--input-border-color);
|
||||||
box-shadow: 0 0 10px 0 var(--prompt-box-shadow-color);
|
box-shadow: 0 0 10px 0 var(--input-box-shadow-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
&[aria-invalid='true'] {
|
&[aria-invalid='true'] {
|
||||||
|
@ -10,12 +10,15 @@ import useCheckParameters, {
|
|||||||
systemSelector,
|
systemSelector,
|
||||||
} from '../../../common/hooks/useCheckParameters';
|
} from '../../../common/hooks/useCheckParameters';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { tabMap } from '../../tabs/InvokeTabs';
|
||||||
|
|
||||||
export const optionsSelector = createSelector(
|
export const optionsSelector = createSelector(
|
||||||
(state: RootState) => state.options,
|
(state: RootState) => state.options,
|
||||||
(options: OptionsState) => {
|
(options: OptionsState) => {
|
||||||
|
const { prompt, activeTab } = options;
|
||||||
return {
|
return {
|
||||||
prompt: options.prompt,
|
prompt,
|
||||||
|
activeTabName: tabMap[activeTab],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -30,7 +33,7 @@ export const optionsSelector = createSelector(
|
|||||||
*/
|
*/
|
||||||
const PromptInput = () => {
|
const PromptInput = () => {
|
||||||
const promptRef = useRef<HTMLTextAreaElement>(null);
|
const promptRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const { prompt } = useAppSelector(optionsSelector);
|
const { prompt, activeTabName } = useAppSelector(optionsSelector);
|
||||||
const { isProcessing } = useAppSelector(systemSelector);
|
const { isProcessing } = useAppSelector(systemSelector);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const isReady = useCheckParameters();
|
const isReady = useCheckParameters();
|
||||||
@ -40,13 +43,13 @@ const PromptInput = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'ctrl+enter',
|
'ctrl+enter, cmd+enter',
|
||||||
() => {
|
() => {
|
||||||
if (isReady) {
|
if (isReady) {
|
||||||
dispatch(generateImage());
|
dispatch(generateImage(activeTabName));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isReady]
|
[isReady, activeTabName]
|
||||||
);
|
);
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
@ -60,7 +63,7 @@ const PromptInput = () => {
|
|||||||
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
if (e.key === 'Enter' && e.shiftKey === false && isReady) {
|
if (e.key === 'Enter' && e.shiftKey === false && isReady) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dispatch(generateImage());
|
dispatch(generateImage(activeTabName));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,104 +0,0 @@
|
|||||||
import { Flex } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
import { RootState } from '../../app/store';
|
|
||||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
|
||||||
|
|
||||||
import {
|
|
||||||
setCfgScale,
|
|
||||||
setSampler,
|
|
||||||
setThreshold,
|
|
||||||
setPerlin,
|
|
||||||
setSteps,
|
|
||||||
OptionsState,
|
|
||||||
} from './optionsSlice';
|
|
||||||
|
|
||||||
import { SAMPLERS } from '../../app/constants';
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { isEqual } from 'lodash';
|
|
||||||
import { ChangeEvent } from 'react';
|
|
||||||
import IAINumberInput from '../../common/components/IAINumberInput';
|
|
||||||
import IAISelect from '../../common/components/IAISelect';
|
|
||||||
|
|
||||||
const optionsSelector = createSelector(
|
|
||||||
(state: RootState) => state.options,
|
|
||||||
(options: OptionsState) => {
|
|
||||||
return {
|
|
||||||
steps: options.steps,
|
|
||||||
cfgScale: options.cfgScale,
|
|
||||||
sampler: options.sampler,
|
|
||||||
threshold: options.threshold,
|
|
||||||
perlin: options.perlin,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memoizeOptions: {
|
|
||||||
resultEqualityCheck: isEqual,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sampler options. Includes steps, CFG scale, sampler.
|
|
||||||
*/
|
|
||||||
const SamplerOptions = () => {
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const { steps, cfgScale, sampler, threshold, perlin } = useAppSelector(optionsSelector);
|
|
||||||
|
|
||||||
const handleChangeSteps = (v: string | number) =>
|
|
||||||
dispatch(setSteps(Number(v)));
|
|
||||||
|
|
||||||
const handleChangeCfgScale = (v: string | number) =>
|
|
||||||
dispatch(setCfgScale(Number(v)));
|
|
||||||
|
|
||||||
const handleChangeSampler = (e: ChangeEvent<HTMLSelectElement>) =>
|
|
||||||
dispatch(setSampler(e.target.value));
|
|
||||||
|
|
||||||
const handleChangeThreshold = (v: string | number) =>
|
|
||||||
dispatch(setThreshold(Number(v)));
|
|
||||||
|
|
||||||
const handleChangePerlin = (v: string | number) =>
|
|
||||||
dispatch(setPerlin(Number(v)));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex gap={2} direction={'column'}>
|
|
||||||
{/* <IAINumberInput
|
|
||||||
label="Steps"
|
|
||||||
min={1}
|
|
||||||
step={1}
|
|
||||||
precision={0}
|
|
||||||
onChange={handleChangeSteps}
|
|
||||||
value={steps}
|
|
||||||
/> */}
|
|
||||||
{/* <IAINumberInput
|
|
||||||
label="CFG scale"
|
|
||||||
step={0.5}
|
|
||||||
onChange={handleChangeCfgScale}
|
|
||||||
value={cfgScale}
|
|
||||||
/> */}
|
|
||||||
<IAISelect
|
|
||||||
label="Sampler"
|
|
||||||
value={sampler}
|
|
||||||
onChange={handleChangeSampler}
|
|
||||||
validValues={SAMPLERS}
|
|
||||||
/>
|
|
||||||
{/* <IAINumberInput
|
|
||||||
label='Threshold'
|
|
||||||
min={0}
|
|
||||||
step={0.1}
|
|
||||||
onChange={handleChangeThreshold}
|
|
||||||
value={threshold}
|
|
||||||
/> */}
|
|
||||||
{/* <IAINumberInput
|
|
||||||
label='Perlin'
|
|
||||||
min={0}
|
|
||||||
max={1}
|
|
||||||
step={0.05}
|
|
||||||
onChange={handleChangePerlin}
|
|
||||||
value={perlin}
|
|
||||||
/> */}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SamplerOptions;
|
|
@ -4,6 +4,7 @@ import * as InvokeAI from '../../app/invokeai';
|
|||||||
import promptToString from '../../common/util/promptToString';
|
import promptToString from '../../common/util/promptToString';
|
||||||
import { seedWeightsToString } from '../../common/util/seedWeightPairs';
|
import { seedWeightsToString } from '../../common/util/seedWeightPairs';
|
||||||
import { FACETOOL_TYPES } from '../../app/constants';
|
import { FACETOOL_TYPES } from '../../app/constants';
|
||||||
|
import { InvokeTabName, tabMap } from '../tabs/InvokeTabs';
|
||||||
|
|
||||||
export type UpscalingLevel = 2 | 4;
|
export type UpscalingLevel = 2 | 4;
|
||||||
|
|
||||||
@ -41,7 +42,7 @@ export interface OptionsState {
|
|||||||
showAdvancedOptions: boolean;
|
showAdvancedOptions: boolean;
|
||||||
activeTab: number;
|
activeTab: number;
|
||||||
shouldShowImageDetails: boolean;
|
shouldShowImageDetails: boolean;
|
||||||
shouldShowGallery: boolean;
|
showDualDisplay: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialOptionsState: OptionsState = {
|
const initialOptionsState: OptionsState = {
|
||||||
@ -76,7 +77,7 @@ const initialOptionsState: OptionsState = {
|
|||||||
showAdvancedOptions: true,
|
showAdvancedOptions: true,
|
||||||
activeTab: 0,
|
activeTab: 0,
|
||||||
shouldShowImageDetails: false,
|
shouldShowImageDetails: false,
|
||||||
shouldShowGallery: false,
|
showDualDisplay: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialState: OptionsState = initialOptionsState;
|
const initialState: OptionsState = initialOptionsState;
|
||||||
@ -321,14 +322,18 @@ export const optionsSlice = createSlice({
|
|||||||
setShowAdvancedOptions: (state, action: PayloadAction<boolean>) => {
|
setShowAdvancedOptions: (state, action: PayloadAction<boolean>) => {
|
||||||
state.showAdvancedOptions = action.payload;
|
state.showAdvancedOptions = action.payload;
|
||||||
},
|
},
|
||||||
setActiveTab: (state, action: PayloadAction<number>) => {
|
setActiveTab: (state, action: PayloadAction<number | InvokeTabName>) => {
|
||||||
state.activeTab = action.payload;
|
if (typeof action.payload === 'number') {
|
||||||
|
state.activeTab = action.payload;
|
||||||
|
} else {
|
||||||
|
state.activeTab = tabMap.indexOf(action.payload);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
setShouldShowImageDetails: (state, action: PayloadAction<boolean>) => {
|
setShouldShowImageDetails: (state, action: PayloadAction<boolean>) => {
|
||||||
state.shouldShowImageDetails = action.payload;
|
state.shouldShowImageDetails = action.payload;
|
||||||
},
|
},
|
||||||
setShouldShowGallery: (state, action: PayloadAction<boolean>) => {
|
setShowDualDisplay: (state, action: PayloadAction<boolean>) => {
|
||||||
state.shouldShowGallery = action.payload;
|
state.showDualDisplay = action.payload;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -369,9 +374,9 @@ export const {
|
|||||||
setShowAdvancedOptions,
|
setShowAdvancedOptions,
|
||||||
setActiveTab,
|
setActiveTab,
|
||||||
setShouldShowImageDetails,
|
setShouldShowImageDetails,
|
||||||
setShouldShowGallery,
|
|
||||||
setAllTextToImageParameters,
|
setAllTextToImageParameters,
|
||||||
setAllImageToImageParameters,
|
setAllImageToImageParameters,
|
||||||
|
setShowDualDisplay,
|
||||||
} = optionsSlice.actions;
|
} = optionsSlice.actions;
|
||||||
|
|
||||||
export default optionsSlice.reducer;
|
export default optionsSlice.reducer;
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
padding: 0 1rem 1rem 3rem;
|
padding: 0 1rem 1rem 3rem;
|
||||||
border-top-width: 0.3rem;
|
border-top-width: 0.3rem;
|
||||||
border-color: var(--console-border-color);
|
border-color: var(--resizeable-handle-border-color);
|
||||||
|
|
||||||
.console-info-color {
|
.console-info-color {
|
||||||
color: var(--error-level-info);
|
color: var(--error-level-info);
|
||||||
@ -64,9 +64,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.autoscroll-enabled {
|
&.autoscroll-enabled {
|
||||||
background: var(--btn-purple) !important;
|
background: var(--accent-color) !important;
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--btn-purple-hover) !important;
|
background: var(--accent-color-hover) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,8 +34,8 @@
|
|||||||
|
|
||||||
button {
|
button {
|
||||||
@include Button(
|
@include Button(
|
||||||
$btn-color: var(--btn-red),
|
$btn-color: var(--destructive-color),
|
||||||
$btn-color-hover: var(--btn-red-hover)
|
$btn-color-hover: var(--destructive-color-hover)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -90,7 +90,7 @@ export const systemSlice = createSlice({
|
|||||||
state.currentIteration = 0;
|
state.currentIteration = 0;
|
||||||
state.totalIterations = 0;
|
state.totalIterations = 0;
|
||||||
state.currentStatusHasSteps = false;
|
state.currentStatusHasSteps = false;
|
||||||
state.currentStatus = 'Server error';
|
state.currentStatus = 'Error';
|
||||||
state.wasErrorSeen = false;
|
state.wasErrorSeen = false;
|
||||||
},
|
},
|
||||||
errorSeen: (state) => {
|
errorSeen: (state) => {
|
||||||
|
@ -10,7 +10,6 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
row-gap: 1rem;
|
row-gap: 1rem;
|
||||||
grid-auto-rows: max-content;
|
grid-auto-rows: max-content;
|
||||||
width: $options-bar-max-width;
|
|
||||||
height: $app-content-height;
|
height: $app-content-height;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
@include HideScrollbar;
|
@include HideScrollbar;
|
||||||
@ -141,3 +140,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.image-to-image-current-image-display {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import ImageToImagePanel from './ImageToImagePanel';
|
|
||||||
import ImageToImageDisplay from './ImageToImageDisplay';
|
|
||||||
import ImageGallery from '../../gallery/ImageGallery';
|
|
||||||
import { RootState, useAppSelector } from '../../../app/store';
|
|
||||||
|
|
||||||
export default function ImageToImage() {
|
|
||||||
const shouldShowGallery = useAppSelector(
|
|
||||||
(state: RootState) => state.options.shouldShowGallery
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="image-to-image-workarea">
|
|
||||||
<ImageToImagePanel />
|
|
||||||
<div
|
|
||||||
className="image-to-image-display-area"
|
|
||||||
style={
|
|
||||||
shouldShowGallery
|
|
||||||
? { gridTemplateColumns: 'auto max-content' }
|
|
||||||
: { gridTemplateColumns: 'auto' }
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ImageToImageDisplay />
|
|
||||||
<ImageGallery />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -43,14 +43,14 @@ export default function ImageToImageDisplay() {
|
|||||||
<InitImagePreview />
|
<InitImagePreview />
|
||||||
<div className="image-to-image-current-image-display">
|
<div className="image-to-image-current-image-display">
|
||||||
<CurrentImagePreview imageToDisplay={imageToDisplay} />
|
<CurrentImagePreview imageToDisplay={imageToDisplay} />
|
||||||
|
{shouldShowImageDetails && (
|
||||||
|
<ImageMetadataViewer
|
||||||
|
image={imageToDisplay}
|
||||||
|
styleClass="img2img-metadata"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{shouldShowImageDetails && (
|
|
||||||
<ImageMetadataViewer
|
|
||||||
image={imageToDisplay}
|
|
||||||
styleClass="img2img-metadata"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
import { Box } from '@chakra-ui/react';
|
|
||||||
import React from 'react';
|
|
||||||
import { Feature } from '../../../app/features';
|
import { Feature } from '../../../app/features';
|
||||||
import { RootState, useAppSelector } from '../../../app/store';
|
import { RootState, useAppSelector } from '../../../app/store';
|
||||||
import FaceRestore from '../../options/AdvancedOptions/FaceRestore/FaceRestore';
|
import FaceRestoreHeader from '../../options/AdvancedOptions/FaceRestore/FaceRestoreHeader';
|
||||||
import FaceRestoreOptions from '../../options/AdvancedOptions/FaceRestore/FaceRestoreOptions';
|
import FaceRestoreOptions from '../../options/AdvancedOptions/FaceRestore/FaceRestoreOptions';
|
||||||
import ImageFit from '../../options/AdvancedOptions/ImageToImage/ImageFit';
|
import ImageFit from '../../options/AdvancedOptions/ImageToImage/ImageFit';
|
||||||
import ImageToImageStrength from '../../options/AdvancedOptions/ImageToImage/ImageToImageStrength';
|
import ImageToImageStrength from '../../options/AdvancedOptions/ImageToImage/ImageToImageStrength';
|
||||||
|
import OutputHeader from '../../options/AdvancedOptions/Output/OutputHeader';
|
||||||
|
import OutputOptions from '../../options/AdvancedOptions/Output/OutputOptions';
|
||||||
|
import SeedHeader from '../../options/AdvancedOptions/Seed/SeedHeader';
|
||||||
import SeedOptions from '../../options/AdvancedOptions/Seed/SeedOptions';
|
import SeedOptions from '../../options/AdvancedOptions/Seed/SeedOptions';
|
||||||
import Upscale from '../../options/AdvancedOptions/Upscale/Upscale';
|
import UpscaleHeader from '../../options/AdvancedOptions/Upscale/UpscaleHeader';
|
||||||
import UpscaleOptions from '../../options/AdvancedOptions/Upscale/UpscaleOptions';
|
import UpscaleOptions from '../../options/AdvancedOptions/Upscale/UpscaleOptions';
|
||||||
import Variations from '../../options/AdvancedOptions/Variations/Variations';
|
import VariationsHeader from '../../options/AdvancedOptions/Variations/VariationsHeader';
|
||||||
import VariationsOptions from '../../options/AdvancedOptions/Variations/VariationsOptions';
|
import VariationsOptions from '../../options/AdvancedOptions/Variations/VariationsOptions';
|
||||||
import MainAdvancedOptions from '../../options/MainOptions/MainAdvancedOptions';
|
import MainAdvancedOptionsCheckbox from '../../options/MainOptions/MainAdvancedOptionsCheckbox';
|
||||||
import MainOptions from '../../options/MainOptions/MainOptions';
|
import MainOptions from '../../options/MainOptions/MainOptions';
|
||||||
import OptionsAccordion from '../../options/OptionsAccordion';
|
import OptionsAccordion from '../../options/OptionsAccordion';
|
||||||
import OutputOptions from '../../options/OutputOptions';
|
|
||||||
import ProcessButtons from '../../options/ProcessButtons/ProcessButtons';
|
import ProcessButtons from '../../options/ProcessButtons/ProcessButtons';
|
||||||
import PromptInput from '../../options/PromptInput/PromptInput';
|
import PromptInput from '../../options/PromptInput/PromptInput';
|
||||||
|
|
||||||
@ -25,35 +25,27 @@ export default function ImageToImagePanel() {
|
|||||||
|
|
||||||
const imageToImageAccordions = {
|
const imageToImageAccordions = {
|
||||||
seed: {
|
seed: {
|
||||||
header: (
|
header: <SeedHeader />,
|
||||||
<Box flex="1" textAlign="left">
|
|
||||||
Seed
|
|
||||||
</Box>
|
|
||||||
),
|
|
||||||
feature: Feature.SEED,
|
feature: Feature.SEED,
|
||||||
options: <SeedOptions />,
|
options: <SeedOptions />,
|
||||||
},
|
},
|
||||||
variations: {
|
variations: {
|
||||||
header: <Variations />,
|
header: <VariationsHeader />,
|
||||||
feature: Feature.VARIATIONS,
|
feature: Feature.VARIATIONS,
|
||||||
options: <VariationsOptions />,
|
options: <VariationsOptions />,
|
||||||
},
|
},
|
||||||
face_restore: {
|
face_restore: {
|
||||||
header: <FaceRestore />,
|
header: <FaceRestoreHeader />,
|
||||||
feature: Feature.FACE_CORRECTION,
|
feature: Feature.FACE_CORRECTION,
|
||||||
options: <FaceRestoreOptions />,
|
options: <FaceRestoreOptions />,
|
||||||
},
|
},
|
||||||
upscale: {
|
upscale: {
|
||||||
header: <Upscale />,
|
header: <UpscaleHeader />,
|
||||||
feature: Feature.UPSCALE,
|
feature: Feature.UPSCALE,
|
||||||
options: <UpscaleOptions />,
|
options: <UpscaleOptions />,
|
||||||
},
|
},
|
||||||
other: {
|
other: {
|
||||||
header: (
|
header: <OutputHeader /> ,
|
||||||
<Box flex="1" textAlign="left">
|
|
||||||
Other
|
|
||||||
</Box>
|
|
||||||
),
|
|
||||||
feature: Feature.OTHER,
|
feature: Feature.OTHER,
|
||||||
options: <OutputOptions />,
|
options: <OutputOptions />,
|
||||||
},
|
},
|
||||||
@ -69,7 +61,7 @@ export default function ImageToImagePanel() {
|
|||||||
styleClass="main-option-block image-to-image-strength-main-option"
|
styleClass="main-option-block image-to-image-strength-main-option"
|
||||||
/>
|
/>
|
||||||
<ImageFit />
|
<ImageFit />
|
||||||
<MainAdvancedOptions />
|
<MainAdvancedOptionsCheckbox />
|
||||||
{showAdvancedOptions ? (
|
{showAdvancedOptions ? (
|
||||||
<OptionsAccordion accordionInfo={imageToImageAccordions} />
|
<OptionsAccordion accordionInfo={imageToImageAccordions} />
|
||||||
) : null}
|
) : null}
|
||||||
|
12
frontend/src/features/tabs/ImageToImage/index.tsx
Normal file
12
frontend/src/features/tabs/ImageToImage/index.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ImageToImagePanel from './ImageToImagePanel';
|
||||||
|
import ImageToImageDisplay from './ImageToImageDisplay';
|
||||||
|
import InvokeWorkarea from '../InvokeWorkarea';
|
||||||
|
|
||||||
|
export default function ImageToImageWorkarea() {
|
||||||
|
return (
|
||||||
|
<InvokeWorkarea optionsPanel={<ImageToImagePanel />}>
|
||||||
|
<ImageToImageDisplay />
|
||||||
|
</InvokeWorkarea>
|
||||||
|
);
|
||||||
|
}
|
169
frontend/src/features/tabs/Inpainting/Inpainting.scss
Normal file
169
frontend/src/features/tabs/Inpainting/Inpainting.scss
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
@use '../../../styles/Mixins/' as *;
|
||||||
|
|
||||||
|
.brush-preview-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brush-preview {
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px black solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inpainting-workarea {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: max-content auto;
|
||||||
|
column-gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inpainting-display-area {
|
||||||
|
display: grid;
|
||||||
|
column-gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inpainting-display {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background-color: var(--background-color-secondary);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
// column-gap: 1rem;
|
||||||
|
height: $app-content-height;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inpainting-toolkit {
|
||||||
|
// display: grid;
|
||||||
|
// grid-template-rows: auto auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: var(--inpaint-bg-color);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inpainting-canvas-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.inpainting-canvas-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: min-content;
|
||||||
|
height: min-content;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
|
||||||
|
.inpainting-alerts {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
column-gap: 0.5rem;
|
||||||
|
z-index: 2;
|
||||||
|
padding: 0.5rem;
|
||||||
|
pointer-events: none;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
div {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
padding: 0.2rem 0.6rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inpainting-canvas-stage {
|
||||||
|
canvas {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// .canvas-scale-calculator {
|
||||||
|
// width: calc(100% - 1rem);
|
||||||
|
// height: calc(100% - 1rem);
|
||||||
|
// display: flex;
|
||||||
|
// align-items: center;
|
||||||
|
// justify-content: center;
|
||||||
|
// }
|
||||||
|
|
||||||
|
.inpainting-canvas-scale-wrapper {
|
||||||
|
width: calc(100% - 1rem);
|
||||||
|
height: calc(100% - 1rem);
|
||||||
|
// width: max-content;
|
||||||
|
// height: max-content;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inpainting-settings {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
row-gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
.inpainting-buttons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 0.8rem;
|
||||||
|
|
||||||
|
button {
|
||||||
|
height: 2.4rem;
|
||||||
|
svg {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inpainting-buttons-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inpainting-slider-numberinput {
|
||||||
|
display: flex;
|
||||||
|
column-gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overrides
|
||||||
|
.inpainting-workarea-container {
|
||||||
|
.image-gallery-area {
|
||||||
|
.chakra-popover__popper {
|
||||||
|
inset: 0 auto auto -75px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-image-options {
|
||||||
|
button {
|
||||||
|
@include Button(
|
||||||
|
$btn-width: 2.5rem,
|
||||||
|
$icon-size: 18px,
|
||||||
|
$btn-color: var(--btn-grey),
|
||||||
|
$btn-color-hover: var(--btn-grey-hover)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chakra-popover__popper {
|
||||||
|
z-index: 11;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-image-preview {
|
||||||
|
padding: 0 1rem 1rem 1rem;
|
||||||
|
}
|
||||||
|
}
|
289
frontend/src/features/tabs/Inpainting/InpaintingCanvas.tsx
Normal file
289
frontend/src/features/tabs/Inpainting/InpaintingCanvas.tsx
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
// lib
|
||||||
|
import {
|
||||||
|
KeyboardEvent,
|
||||||
|
MutableRefObject,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import Konva from 'konva';
|
||||||
|
import { Layer, Stage } from 'react-konva';
|
||||||
|
import { Image as KonvaImage } from 'react-konva';
|
||||||
|
import { Stage as StageType } from 'konva/lib/Stage';
|
||||||
|
|
||||||
|
// app
|
||||||
|
import { useAppDispatch, useAppSelector } from '../../../app/store';
|
||||||
|
import {
|
||||||
|
addLine,
|
||||||
|
addPointToCurrentLine,
|
||||||
|
setBoundingBoxCoordinate,
|
||||||
|
setCursorPosition,
|
||||||
|
setIsMovingBoundingBox,
|
||||||
|
setTool,
|
||||||
|
} from './inpaintingSlice';
|
||||||
|
import { inpaintingCanvasSelector } from './inpaintingSliceSelectors';
|
||||||
|
|
||||||
|
// component
|
||||||
|
import InpaintingCanvasLines from './components/InpaintingCanvasLines';
|
||||||
|
import InpaintingCanvasBrushPreview from './components/InpaintingCanvasBrushPreview';
|
||||||
|
import InpaintingCanvasBrushPreviewOutline from './components/InpaintingCanvasBrushPreviewOutline';
|
||||||
|
import Cacher from './components/Cacher';
|
||||||
|
import { Vector2d } from 'konva/lib/types';
|
||||||
|
import getScaledCursorPosition from './util/getScaledCursorPosition';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import InpaintingBoundingBoxPreview from './components/InpaintingBoundingBoxPreview';
|
||||||
|
import { KonvaEventObject } from 'konva/lib/Node';
|
||||||
|
import KeyboardEventManager from './components/KeyboardEventManager';
|
||||||
|
|
||||||
|
// Use a closure allow other components to use these things... not ideal...
|
||||||
|
export let stageRef: MutableRefObject<StageType | null>;
|
||||||
|
export let maskLayerRef: MutableRefObject<Konva.Layer | null>;
|
||||||
|
export let inpaintingImageElementRef: MutableRefObject<HTMLImageElement | null>;
|
||||||
|
|
||||||
|
const InpaintingCanvas = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const {
|
||||||
|
tool,
|
||||||
|
brushSize,
|
||||||
|
shouldInvertMask,
|
||||||
|
shouldShowMask,
|
||||||
|
shouldShowCheckboardTransparency,
|
||||||
|
maskOpacity,
|
||||||
|
imageToInpaint,
|
||||||
|
isMovingBoundingBox,
|
||||||
|
boundingBoxDimensions,
|
||||||
|
canvasDimensions,
|
||||||
|
boundingBoxCoordinate,
|
||||||
|
stageScale,
|
||||||
|
} = useAppSelector(inpaintingCanvasSelector);
|
||||||
|
|
||||||
|
// set the closure'd refs
|
||||||
|
stageRef = useRef<StageType>(null);
|
||||||
|
maskLayerRef = useRef<Konva.Layer>(null);
|
||||||
|
inpaintingImageElementRef = useRef<HTMLImageElement>(null);
|
||||||
|
|
||||||
|
const lastCursorPosition = useRef<Vector2d>({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
// Use refs for values that do not affect rendering, other values in redux
|
||||||
|
const didMouseMoveRef = useRef<boolean>(false);
|
||||||
|
const isDrawing = useRef<boolean>(false);
|
||||||
|
|
||||||
|
// Load the image into this
|
||||||
|
const [canvasBgImage, setCanvasBgImage] = useState<HTMLImageElement | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load the image and set the options panel width & height
|
||||||
|
useEffect(() => {
|
||||||
|
if (imageToInpaint) {
|
||||||
|
const image = new Image();
|
||||||
|
image.onload = () => {
|
||||||
|
inpaintingImageElementRef.current = image;
|
||||||
|
setCanvasBgImage(image);
|
||||||
|
};
|
||||||
|
image.src = imageToInpaint.url;
|
||||||
|
}
|
||||||
|
}, [imageToInpaint, dispatch, stageScale]);
|
||||||
|
|
||||||
|
const handleMouseDown = useCallback(() => {
|
||||||
|
if (!stageRef.current) return;
|
||||||
|
|
||||||
|
const scaledCursorPosition = getScaledCursorPosition(stageRef.current);
|
||||||
|
|
||||||
|
if (!scaledCursorPosition || !maskLayerRef.current) return;
|
||||||
|
|
||||||
|
isDrawing.current = true;
|
||||||
|
|
||||||
|
// Add a new line starting from the current cursor position.
|
||||||
|
dispatch(
|
||||||
|
addLine({
|
||||||
|
tool,
|
||||||
|
strokeWidth: brushSize / 2,
|
||||||
|
points: [scaledCursorPosition.x, scaledCursorPosition.y],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [dispatch, brushSize, tool]);
|
||||||
|
|
||||||
|
const handleMouseMove = useCallback(() => {
|
||||||
|
if (!stageRef.current) return;
|
||||||
|
|
||||||
|
const scaledCursorPosition = getScaledCursorPosition(stageRef.current);
|
||||||
|
|
||||||
|
if (!scaledCursorPosition) return;
|
||||||
|
|
||||||
|
dispatch(setCursorPosition(scaledCursorPosition));
|
||||||
|
|
||||||
|
if (!maskLayerRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deltaX = lastCursorPosition.current.x - scaledCursorPosition.x;
|
||||||
|
const deltaY = lastCursorPosition.current.y - scaledCursorPosition.y;
|
||||||
|
|
||||||
|
lastCursorPosition.current = scaledCursorPosition;
|
||||||
|
|
||||||
|
if (isMovingBoundingBox) {
|
||||||
|
const x = _.clamp(
|
||||||
|
Math.floor(boundingBoxCoordinate.x - deltaX),
|
||||||
|
0,
|
||||||
|
canvasDimensions.width - boundingBoxDimensions.width
|
||||||
|
);
|
||||||
|
|
||||||
|
const y = _.clamp(
|
||||||
|
Math.floor(boundingBoxCoordinate.y - deltaY),
|
||||||
|
0,
|
||||||
|
canvasDimensions.height - boundingBoxDimensions.height
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
setBoundingBoxCoordinate({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDrawing.current) return;
|
||||||
|
|
||||||
|
didMouseMoveRef.current = true;
|
||||||
|
// Extend the current line
|
||||||
|
dispatch(
|
||||||
|
addPointToCurrentLine([scaledCursorPosition.x, scaledCursorPosition.y])
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
dispatch,
|
||||||
|
isMovingBoundingBox,
|
||||||
|
boundingBoxDimensions,
|
||||||
|
canvasDimensions,
|
||||||
|
boundingBoxCoordinate,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleMouseUp = useCallback(() => {
|
||||||
|
if (!didMouseMoveRef.current && isDrawing.current && stageRef.current) {
|
||||||
|
const scaledCursorPosition = getScaledCursorPosition(stageRef.current);
|
||||||
|
|
||||||
|
if (!scaledCursorPosition || !maskLayerRef.current) return;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extend the current line.
|
||||||
|
* In this case, the mouse didn't move, so we append the same point to
|
||||||
|
* the line's existing points. This allows the line to render as a circle
|
||||||
|
* centered on that point.
|
||||||
|
*/
|
||||||
|
dispatch(
|
||||||
|
addPointToCurrentLine([scaledCursorPosition.x, scaledCursorPosition.y])
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
didMouseMoveRef.current = false;
|
||||||
|
}
|
||||||
|
isDrawing.current = false;
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handleMouseOutCanvas = useCallback(() => {
|
||||||
|
dispatch(setCursorPosition(null));
|
||||||
|
dispatch(setIsMovingBoundingBox(false));
|
||||||
|
isDrawing.current = false;
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handleMouseEnter = useCallback(
|
||||||
|
(e: KonvaEventObject<MouseEvent>) => {
|
||||||
|
if (e.evt.buttons === 1) {
|
||||||
|
if (!stageRef.current) return;
|
||||||
|
|
||||||
|
const scaledCursorPosition = getScaledCursorPosition(stageRef.current);
|
||||||
|
|
||||||
|
if (!scaledCursorPosition || !maskLayerRef.current) return;
|
||||||
|
|
||||||
|
isDrawing.current = true;
|
||||||
|
|
||||||
|
// Add a new line starting from the current cursor position.
|
||||||
|
dispatch(
|
||||||
|
addLine({
|
||||||
|
tool,
|
||||||
|
strokeWidth: brushSize / 2,
|
||||||
|
points: [scaledCursorPosition.x, scaledCursorPosition.y],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch, brushSize, tool]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="inpainting-canvas-wrapper checkerboard" tabIndex={1}>
|
||||||
|
<div className="inpainting-alerts">
|
||||||
|
{!shouldShowMask && <div>Mask Hidden (H)</div>}
|
||||||
|
{shouldInvertMask && <div>Mask Inverted (Shift+M)</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{canvasBgImage && (
|
||||||
|
<Stage
|
||||||
|
width={Math.floor(canvasBgImage.width * stageScale)}
|
||||||
|
height={Math.floor(canvasBgImage.height * stageScale)}
|
||||||
|
scale={{ x: stageScale, y: stageScale }}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseUp={handleMouseUp}
|
||||||
|
onMouseOut={handleMouseOutCanvas}
|
||||||
|
onMouseLeave={handleMouseOutCanvas}
|
||||||
|
style={{ cursor: shouldShowMask ? 'none' : 'default' }}
|
||||||
|
className="inpainting-canvas-stage"
|
||||||
|
ref={stageRef}
|
||||||
|
>
|
||||||
|
{!shouldInvertMask && !shouldShowCheckboardTransparency && (
|
||||||
|
<Layer name={'image-layer'} listening={false}>
|
||||||
|
<KonvaImage listening={false} image={canvasBgImage} />
|
||||||
|
</Layer>
|
||||||
|
)}
|
||||||
|
{shouldShowMask && (
|
||||||
|
<>
|
||||||
|
<Layer
|
||||||
|
name={'mask-layer'}
|
||||||
|
listening={false}
|
||||||
|
opacity={
|
||||||
|
shouldShowCheckboardTransparency || shouldInvertMask
|
||||||
|
? 1
|
||||||
|
: maskOpacity
|
||||||
|
}
|
||||||
|
ref={maskLayerRef}
|
||||||
|
>
|
||||||
|
<InpaintingCanvasLines />
|
||||||
|
<InpaintingCanvasBrushPreview />
|
||||||
|
|
||||||
|
{shouldInvertMask && (
|
||||||
|
<KonvaImage
|
||||||
|
image={canvasBgImage}
|
||||||
|
listening={false}
|
||||||
|
globalCompositeOperation="source-in"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!shouldInvertMask && shouldShowCheckboardTransparency && (
|
||||||
|
<KonvaImage
|
||||||
|
image={canvasBgImage}
|
||||||
|
listening={false}
|
||||||
|
globalCompositeOperation="source-out"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Layer>
|
||||||
|
<Layer name={'preview-layer'} listening={false}>
|
||||||
|
<InpaintingCanvasBrushPreviewOutline />
|
||||||
|
<InpaintingBoundingBoxPreview />
|
||||||
|
</Layer>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stage>
|
||||||
|
)}
|
||||||
|
<Cacher />
|
||||||
|
<KeyboardEventManager />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
export default InpaintingCanvas;
|
@ -0,0 +1,34 @@
|
|||||||
|
import { Spinner } from '@chakra-ui/react';
|
||||||
|
import { useLayoutEffect, useRef } from 'react';
|
||||||
|
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||||
|
import { setStageScale } from './inpaintingSlice';
|
||||||
|
|
||||||
|
const InpaintingCanvasPlaceholder = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { needsRepaint, imageToInpaint } = useAppSelector(
|
||||||
|
(state: RootState) => state.inpainting
|
||||||
|
);
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!ref.current || !imageToInpaint) return;
|
||||||
|
|
||||||
|
const width = ref.current.clientWidth;
|
||||||
|
const height = ref.current.clientHeight;
|
||||||
|
|
||||||
|
const scale = Math.min(
|
||||||
|
1,
|
||||||
|
Math.min(width / imageToInpaint.width, height / imageToInpaint.height)
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch(setStageScale(scale));
|
||||||
|
}, [dispatch, imageToInpaint, needsRepaint]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} className="inpainting-canvas-container">
|
||||||
|
<Spinner thickness="2px" speed="1s" size="xl" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InpaintingCanvasPlaceholder;
|
384
frontend/src/features/tabs/Inpainting/InpaintingControls.tsx
Normal file
384
frontend/src/features/tabs/Inpainting/InpaintingControls.tsx
Normal file
@ -0,0 +1,384 @@
|
|||||||
|
import { useToast } from '@chakra-ui/react';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import {
|
||||||
|
FaEraser,
|
||||||
|
FaPaintBrush,
|
||||||
|
FaPalette,
|
||||||
|
FaPlus,
|
||||||
|
FaRedo,
|
||||||
|
FaUndo,
|
||||||
|
} from 'react-icons/fa';
|
||||||
|
import { BiHide, BiShow } from 'react-icons/bi';
|
||||||
|
import { VscSplitHorizontal } from 'react-icons/vsc';
|
||||||
|
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||||
|
import IAIIconButton from '../../../common/components/IAIIconButton';
|
||||||
|
import {
|
||||||
|
clearMask,
|
||||||
|
redo,
|
||||||
|
setMaskColor,
|
||||||
|
setBrushSize,
|
||||||
|
setMaskOpacity,
|
||||||
|
setShouldShowBrushPreview,
|
||||||
|
setTool,
|
||||||
|
undo,
|
||||||
|
setShouldShowMask,
|
||||||
|
setShouldInvertMask,
|
||||||
|
setNeedsRepaint,
|
||||||
|
} from './inpaintingSlice';
|
||||||
|
|
||||||
|
import { tabMap } from '../InvokeTabs';
|
||||||
|
import {
|
||||||
|
MdInvertColors,
|
||||||
|
MdInvertColorsOff,
|
||||||
|
MdOutlineCloseFullscreen,
|
||||||
|
} from 'react-icons/md';
|
||||||
|
import IAISlider from '../../../common/components/IAISlider';
|
||||||
|
import IAINumberInput from '../../../common/components/IAINumberInput';
|
||||||
|
import { inpaintingControlsSelector } from './inpaintingSliceSelectors';
|
||||||
|
import IAIPopover from '../../../common/components/IAIPopover';
|
||||||
|
import IAIColorPicker from '../../../common/components/IAIColorPicker';
|
||||||
|
import { RgbaColor } from 'react-colorful';
|
||||||
|
import { setShowDualDisplay } from '../../options/optionsSlice';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
const InpaintingControls = () => {
|
||||||
|
const {
|
||||||
|
tool,
|
||||||
|
brushSize,
|
||||||
|
maskColor,
|
||||||
|
maskOpacity,
|
||||||
|
shouldInvertMask,
|
||||||
|
shouldShowMask,
|
||||||
|
canUndo,
|
||||||
|
canRedo,
|
||||||
|
isMaskEmpty,
|
||||||
|
activeTabName,
|
||||||
|
showDualDisplay,
|
||||||
|
} = useAppSelector(inpaintingControlsSelector);
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
// Hotkeys
|
||||||
|
useHotkeys(
|
||||||
|
'[',
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (brushSize - 5 > 0) {
|
||||||
|
handleChangeBrushSize(brushSize - 5);
|
||||||
|
} else {
|
||||||
|
handleChangeBrushSize(1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||||
|
},
|
||||||
|
[activeTabName, shouldShowMask, brushSize]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
']',
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleChangeBrushSize(brushSize + 5);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||||
|
},
|
||||||
|
[activeTabName, shouldShowMask, brushSize]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'shift+[',
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleChangeMaskOpacity(Math.max(maskOpacity - 0.05, 0));
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||||
|
},
|
||||||
|
[activeTabName, shouldShowMask, maskOpacity]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'shift+]',
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleChangeMaskOpacity(Math.min(maskOpacity + 0.05, 100));
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||||
|
},
|
||||||
|
[activeTabName, shouldShowMask, maskOpacity]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'e',
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (activeTabName !== 'inpainting' || !shouldShowMask) return;
|
||||||
|
handleSelectEraserTool();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||||
|
},
|
||||||
|
[activeTabName, shouldShowMask]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'b',
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSelectBrushTool();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||||
|
},
|
||||||
|
[activeTabName, shouldShowMask]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'cmd+z, control+z',
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleUndo();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: activeTabName === 'inpainting' && shouldShowMask && canUndo,
|
||||||
|
},
|
||||||
|
[activeTabName, shouldShowMask, canUndo]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'cmd+shift+z, control+shift+z, control+y, cmd+y',
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleRedo();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: activeTabName === 'inpainting' && shouldShowMask && canRedo,
|
||||||
|
},
|
||||||
|
[activeTabName, shouldShowMask, canRedo]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'h',
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleToggleShouldShowMask();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: activeTabName === 'inpainting',
|
||||||
|
},
|
||||||
|
[activeTabName, shouldShowMask]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'shift+m',
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleToggleShouldInvertMask();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||||
|
},
|
||||||
|
[activeTabName, shouldInvertMask, shouldShowMask]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'shift+c',
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleClearMask();
|
||||||
|
toast({
|
||||||
|
title: 'Mask Cleared',
|
||||||
|
status: 'success',
|
||||||
|
duration: 2500,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: activeTabName === 'inpainting' && shouldShowMask && !isMaskEmpty,
|
||||||
|
},
|
||||||
|
[activeTabName, isMaskEmpty, shouldShowMask]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'shift+j',
|
||||||
|
() => {
|
||||||
|
handleDualDisplay();
|
||||||
|
},
|
||||||
|
[showDualDisplay]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClearMask = () => {
|
||||||
|
dispatch(clearMask());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectEraserTool = () => dispatch(setTool('eraser'));
|
||||||
|
|
||||||
|
const handleSelectBrushTool = () => dispatch(setTool('brush'));
|
||||||
|
|
||||||
|
const handleChangeBrushSize = (v: number) => {
|
||||||
|
dispatch(setShouldShowBrushPreview(true));
|
||||||
|
dispatch(setBrushSize(v));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeMaskOpacity = (v: number) => {
|
||||||
|
dispatch(setMaskOpacity(v));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleShouldShowMask = () =>
|
||||||
|
dispatch(setShouldShowMask(!shouldShowMask));
|
||||||
|
|
||||||
|
const handleToggleShouldInvertMask = () =>
|
||||||
|
dispatch(setShouldInvertMask(!shouldInvertMask));
|
||||||
|
|
||||||
|
const handleShowBrushPreview = () => {
|
||||||
|
dispatch(setShouldShowBrushPreview(true));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHideBrushPreview = () => {
|
||||||
|
dispatch(setShouldShowBrushPreview(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeBrushColor = (newColor: RgbaColor) => {
|
||||||
|
const { r, g, b, a: maskOpacity } = newColor;
|
||||||
|
dispatch(setMaskColor({ r, g, b }));
|
||||||
|
dispatch(setMaskOpacity(maskOpacity));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUndo = () => dispatch(undo());
|
||||||
|
|
||||||
|
const handleRedo = () => dispatch(redo());
|
||||||
|
|
||||||
|
const handleDualDisplay = () => {
|
||||||
|
dispatch(setShowDualDisplay(!showDualDisplay));
|
||||||
|
dispatch(setNeedsRepaint(true));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="inpainting-settings">
|
||||||
|
<div className="inpainting-buttons">
|
||||||
|
<div className="inpainting-buttons-group">
|
||||||
|
<IAIPopover
|
||||||
|
trigger="hover"
|
||||||
|
onOpen={handleShowBrushPreview}
|
||||||
|
onClose={handleHideBrushPreview}
|
||||||
|
triggerComponent={
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Brush (B)"
|
||||||
|
tooltip="Brush (B)"
|
||||||
|
icon={<FaPaintBrush />}
|
||||||
|
onClick={handleSelectBrushTool}
|
||||||
|
data-selected={tool === 'brush'}
|
||||||
|
isDisabled={!shouldShowMask}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="inpainting-slider-numberinput">
|
||||||
|
<IAISlider
|
||||||
|
label="Brush Size"
|
||||||
|
value={brushSize}
|
||||||
|
onChange={handleChangeBrushSize}
|
||||||
|
min={1}
|
||||||
|
max={200}
|
||||||
|
width="100px"
|
||||||
|
focusThumbOnChange={false}
|
||||||
|
isDisabled={!shouldShowMask}
|
||||||
|
/>
|
||||||
|
<IAINumberInput
|
||||||
|
value={brushSize}
|
||||||
|
onChange={handleChangeBrushSize}
|
||||||
|
width={'80px'}
|
||||||
|
min={1}
|
||||||
|
max={999}
|
||||||
|
isDisabled={!shouldShowMask}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</IAIPopover>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Eraser (E)"
|
||||||
|
tooltip="Eraser (E)"
|
||||||
|
icon={<FaEraser />}
|
||||||
|
onClick={handleSelectEraserTool}
|
||||||
|
data-selected={tool === 'eraser'}
|
||||||
|
isDisabled={!shouldShowMask}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="inpainting-buttons-group">
|
||||||
|
<IAIPopover
|
||||||
|
trigger="hover"
|
||||||
|
triggerComponent={
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Mask Color"
|
||||||
|
tooltip="Mask Color"
|
||||||
|
icon={<FaPalette />}
|
||||||
|
isDisabled={!shouldShowMask}
|
||||||
|
cursor={'pointer'}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IAIColorPicker
|
||||||
|
color={{ ...maskColor, a: maskOpacity }}
|
||||||
|
onChange={handleChangeBrushColor}
|
||||||
|
/>
|
||||||
|
</IAIPopover>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Hide/Show Mask (H)"
|
||||||
|
tooltip="Hide/Show Mask (H)"
|
||||||
|
data-selected={!shouldShowMask}
|
||||||
|
icon={shouldShowMask ? <BiShow size={22} /> : <BiHide size={22} />}
|
||||||
|
onClick={handleToggleShouldShowMask}
|
||||||
|
/>
|
||||||
|
<IAIIconButton
|
||||||
|
tooltip="Invert Mask Display (Shift+M)"
|
||||||
|
aria-label="Invert Mask Display (Shift+M)"
|
||||||
|
data-selected={shouldInvertMask}
|
||||||
|
icon={
|
||||||
|
shouldInvertMask ? (
|
||||||
|
<MdInvertColors size={22} />
|
||||||
|
) : (
|
||||||
|
<MdInvertColorsOff size={22} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onClick={handleToggleShouldInvertMask}
|
||||||
|
isDisabled={!shouldShowMask}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="inpainting-buttons-group">
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Undo"
|
||||||
|
tooltip="Undo"
|
||||||
|
icon={<FaUndo />}
|
||||||
|
onClick={handleUndo}
|
||||||
|
isDisabled={!canUndo || !shouldShowMask}
|
||||||
|
/>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Redo"
|
||||||
|
tooltip="Redo"
|
||||||
|
icon={<FaRedo />}
|
||||||
|
onClick={handleRedo}
|
||||||
|
isDisabled={!canRedo || !shouldShowMask}
|
||||||
|
/>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Clear Mask (Shift + C)"
|
||||||
|
tooltip="Clear Mask (Shift + C)"
|
||||||
|
icon={<FaPlus size={18} style={{ transform: 'rotate(45deg)' }} />}
|
||||||
|
onClick={handleClearMask}
|
||||||
|
isDisabled={isMaskEmpty || !shouldShowMask}
|
||||||
|
/>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Split Layout (Shift+J)"
|
||||||
|
tooltip="Split Layout (Shift+J)"
|
||||||
|
icon={<VscSplitHorizontal />}
|
||||||
|
data-selected={showDualDisplay}
|
||||||
|
onClick={handleDualDisplay}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InpaintingControls;
|
69
frontend/src/features/tabs/Inpainting/InpaintingDisplay.tsx
Normal file
69
frontend/src/features/tabs/Inpainting/InpaintingDisplay.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { useLayoutEffect } from 'react';
|
||||||
|
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||||
|
import CurrentImageDisplay from '../../gallery/CurrentImageDisplay';
|
||||||
|
import { OptionsState } from '../../options/optionsSlice';
|
||||||
|
import InpaintingCanvas from './InpaintingCanvas';
|
||||||
|
import InpaintingCanvasPlaceholder from './InpaintingCanvasPlaceholder';
|
||||||
|
import InpaintingControls from './InpaintingControls';
|
||||||
|
import { InpaintingState, setNeedsRepaint } from './inpaintingSlice';
|
||||||
|
|
||||||
|
const inpaintingDisplaySelector = createSelector(
|
||||||
|
[(state: RootState) => state.inpainting, (state: RootState) => state.options],
|
||||||
|
(inpainting: InpaintingState, options: OptionsState) => {
|
||||||
|
const { needsRepaint } = inpainting;
|
||||||
|
const { showDualDisplay } = options;
|
||||||
|
return {
|
||||||
|
needsRepaint,
|
||||||
|
showDualDisplay,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const InpaintingDisplay = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { showDualDisplay, needsRepaint } = useAppSelector(
|
||||||
|
inpaintingDisplaySelector
|
||||||
|
);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const resizeCallback = _.debounce(
|
||||||
|
() => dispatch(setNeedsRepaint(true)),
|
||||||
|
250
|
||||||
|
);
|
||||||
|
window.addEventListener('resize', resizeCallback);
|
||||||
|
return () => window.removeEventListener('resize', resizeCallback);
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="inpainting-display"
|
||||||
|
style={
|
||||||
|
showDualDisplay
|
||||||
|
? { gridTemplateColumns: '1fr 1fr' }
|
||||||
|
: { gridTemplateColumns: 'auto' }
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="inpainting-toolkit">
|
||||||
|
<InpaintingControls />
|
||||||
|
|
||||||
|
<div className="inpainting-canvas-container">
|
||||||
|
{needsRepaint ? (
|
||||||
|
<InpaintingCanvasPlaceholder />
|
||||||
|
) : (
|
||||||
|
<InpaintingCanvas />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{showDualDisplay && <CurrentImageDisplay />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InpaintingDisplay;
|
63
frontend/src/features/tabs/Inpainting/InpaintingPanel.tsx
Normal file
63
frontend/src/features/tabs/Inpainting/InpaintingPanel.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { Feature } from '../../../app/features';
|
||||||
|
import { RootState, useAppSelector } from '../../../app/store';
|
||||||
|
import FaceRestoreHeader from '../../options/AdvancedOptions/FaceRestore/FaceRestoreHeader';
|
||||||
|
import FaceRestoreOptions from '../../options/AdvancedOptions/FaceRestore/FaceRestoreOptions';
|
||||||
|
import ImageToImageStrength from '../../options/AdvancedOptions/ImageToImage/ImageToImageStrength';
|
||||||
|
import BoundingBoxDimensions from '../../options/AdvancedOptions/Inpainting/BoundingBoxDimensions';
|
||||||
|
import SeedHeader from '../../options/AdvancedOptions/Seed/SeedHeader';
|
||||||
|
import SeedOptions from '../../options/AdvancedOptions/Seed/SeedOptions';
|
||||||
|
import UpscaleHeader from '../../options/AdvancedOptions/Upscale/UpscaleHeader';
|
||||||
|
import UpscaleOptions from '../../options/AdvancedOptions/Upscale/UpscaleOptions';
|
||||||
|
import VariationsHeader from '../../options/AdvancedOptions/Variations/VariationsHeader';
|
||||||
|
import VariationsOptions from '../../options/AdvancedOptions/Variations/VariationsOptions';
|
||||||
|
import MainAdvancedOptionsCheckbox from '../../options/MainOptions/MainAdvancedOptionsCheckbox';
|
||||||
|
import MainOptions from '../../options/MainOptions/MainOptions';
|
||||||
|
import OptionsAccordion from '../../options/OptionsAccordion';
|
||||||
|
import ProcessButtons from '../../options/ProcessButtons/ProcessButtons';
|
||||||
|
import PromptInput from '../../options/PromptInput/PromptInput';
|
||||||
|
|
||||||
|
export default function InpaintingPanel() {
|
||||||
|
const showAdvancedOptions = useAppSelector(
|
||||||
|
(state: RootState) => state.options.showAdvancedOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
const imageToImageAccordions = {
|
||||||
|
seed: {
|
||||||
|
header: <SeedHeader />,
|
||||||
|
feature: Feature.SEED,
|
||||||
|
options: <SeedOptions />,
|
||||||
|
},
|
||||||
|
variations: {
|
||||||
|
header: <VariationsHeader />,
|
||||||
|
feature: Feature.VARIATIONS,
|
||||||
|
options: <VariationsOptions />,
|
||||||
|
},
|
||||||
|
face_restore: {
|
||||||
|
header: <FaceRestoreHeader />,
|
||||||
|
feature: Feature.FACE_CORRECTION,
|
||||||
|
options: <FaceRestoreOptions />,
|
||||||
|
},
|
||||||
|
upscale: {
|
||||||
|
header: <UpscaleHeader />,
|
||||||
|
feature: Feature.UPSCALE,
|
||||||
|
options: <UpscaleOptions />,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="image-to-image-panel">
|
||||||
|
<PromptInput />
|
||||||
|
<ProcessButtons />
|
||||||
|
<MainOptions />
|
||||||
|
<BoundingBoxDimensions />
|
||||||
|
<ImageToImageStrength
|
||||||
|
label="Image To Image Strength"
|
||||||
|
styleClass="main-option-block image-to-image-strength-main-option"
|
||||||
|
/>
|
||||||
|
<MainAdvancedOptionsCheckbox />
|
||||||
|
{showAdvancedOptions ? (
|
||||||
|
<OptionsAccordion accordionInfo={imageToImageAccordions} />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
56
frontend/src/features/tabs/Inpainting/components/Cacher.tsx
Normal file
56
frontend/src/features/tabs/Inpainting/components/Cacher.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { useLayoutEffect } from 'react';
|
||||||
|
import { RootState, useAppSelector } from '../../../../app/store';
|
||||||
|
import { maskLayerRef } from '../InpaintingCanvas';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konva's cache() method basically rasterizes an object/canvas.
|
||||||
|
* This is needed to rasterize the mask, before setting the opacity.
|
||||||
|
* If we do not cache the maskLayer, the brush strokes will have opacity
|
||||||
|
* set individually.
|
||||||
|
*
|
||||||
|
* This logical component simply uses useLayoutEffect() to synchronously
|
||||||
|
* cache the mask layer every time something that changes how it should draw
|
||||||
|
* is changed.
|
||||||
|
*/
|
||||||
|
const Cacher = () => {
|
||||||
|
const {
|
||||||
|
tool,
|
||||||
|
lines,
|
||||||
|
cursorPosition,
|
||||||
|
brushSize,
|
||||||
|
canvasDimensions: { width, height },
|
||||||
|
maskColor,
|
||||||
|
shouldInvertMask,
|
||||||
|
shouldShowMask,
|
||||||
|
shouldShowBrushPreview,
|
||||||
|
shouldShowCheckboardTransparency,
|
||||||
|
imageToInpaint,
|
||||||
|
} = useAppSelector((state: RootState) => state.inpainting);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!maskLayerRef.current) return;
|
||||||
|
maskLayerRef.current.cache({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
lines,
|
||||||
|
cursorPosition,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
tool,
|
||||||
|
brushSize,
|
||||||
|
maskColor,
|
||||||
|
shouldInvertMask,
|
||||||
|
shouldShowMask,
|
||||||
|
shouldShowBrushPreview,
|
||||||
|
shouldShowCheckboardTransparency,
|
||||||
|
imageToInpaint,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Cacher;
|
@ -0,0 +1,189 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import Konva from 'konva';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { Group, Rect } from 'react-konva';
|
||||||
|
import { RootState, useAppSelector } from '../../../../app/store';
|
||||||
|
import { InpaintingState } from '../inpaintingSlice';
|
||||||
|
import { rgbaColorToString } from '../util/colorToString';
|
||||||
|
import { DASH_WIDTH, MARCHING_ANTS_SPEED } from '../util/constants';
|
||||||
|
|
||||||
|
|
||||||
|
const boundingBoxPreviewSelector = createSelector(
|
||||||
|
(state: RootState) => state.inpainting,
|
||||||
|
(inpainting: InpaintingState) => {
|
||||||
|
const {
|
||||||
|
boundingBoxCoordinate,
|
||||||
|
boundingBoxDimensions,
|
||||||
|
boundingBoxPreviewFill,
|
||||||
|
canvasDimensions,
|
||||||
|
stageScale,
|
||||||
|
} = inpainting;
|
||||||
|
return {
|
||||||
|
boundingBoxCoordinate,
|
||||||
|
boundingBoxDimensions,
|
||||||
|
boundingBoxPreviewFillString: rgbaColorToString(boundingBoxPreviewFill),
|
||||||
|
canvasDimensions,
|
||||||
|
dash: DASH_WIDTH / stageScale, // scale dash lengths
|
||||||
|
strokeWidth: 1 / stageScale, // scale stroke thickness
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shades the area around the mask.
|
||||||
|
*/
|
||||||
|
const InpaintingBoundingBoxPreviewOverlay = () => {
|
||||||
|
const {
|
||||||
|
boundingBoxCoordinate,
|
||||||
|
boundingBoxDimensions,
|
||||||
|
boundingBoxPreviewFillString,
|
||||||
|
canvasDimensions,
|
||||||
|
} = useAppSelector(boundingBoxPreviewSelector);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group>
|
||||||
|
<Rect
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
height={canvasDimensions.height}
|
||||||
|
width={canvasDimensions.width}
|
||||||
|
fill={boundingBoxPreviewFillString}
|
||||||
|
/>
|
||||||
|
<Rect
|
||||||
|
x={boundingBoxCoordinate.x}
|
||||||
|
y={boundingBoxCoordinate.y}
|
||||||
|
width={boundingBoxDimensions.width}
|
||||||
|
height={boundingBoxDimensions.height}
|
||||||
|
fill={'rgb(255,255,255)'}
|
||||||
|
listening={false}
|
||||||
|
globalCompositeOperation={'destination-out'}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws marching ants around the mask.
|
||||||
|
*/
|
||||||
|
const InpaintingBoundingBoxPreviewMarchingAnts = () => {
|
||||||
|
const { boundingBoxCoordinate, boundingBoxDimensions } = useAppSelector(
|
||||||
|
boundingBoxPreviewSelector
|
||||||
|
);
|
||||||
|
|
||||||
|
const blackStrokeRectRef = useRef<Konva.Rect>(null);
|
||||||
|
const whiteStrokeRectRef = useRef<Konva.Rect>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const blackStrokeRect = blackStrokeRectRef.current;
|
||||||
|
const whiteStrokeRect = whiteStrokeRectRef.current;
|
||||||
|
|
||||||
|
const anim = new Konva.Animation((frame) => {
|
||||||
|
if (!frame) return;
|
||||||
|
blackStrokeRect?.dashOffset(
|
||||||
|
-1 * (Math.floor(frame.time / MARCHING_ANTS_SPEED) % 16)
|
||||||
|
);
|
||||||
|
whiteStrokeRect?.dashOffset(
|
||||||
|
-1 * ((Math.floor(frame.time / MARCHING_ANTS_SPEED) % 16) + 4)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
anim.start();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
anim.stop();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group>
|
||||||
|
<Rect
|
||||||
|
x={boundingBoxCoordinate.x}
|
||||||
|
y={boundingBoxCoordinate.y}
|
||||||
|
width={boundingBoxDimensions.width}
|
||||||
|
height={boundingBoxDimensions.height}
|
||||||
|
stroke={'black'}
|
||||||
|
strokeWidth={1}
|
||||||
|
dash={[4, 4]}
|
||||||
|
ref={blackStrokeRectRef}
|
||||||
|
listening={false}
|
||||||
|
/>
|
||||||
|
<Rect
|
||||||
|
x={boundingBoxCoordinate.x}
|
||||||
|
y={boundingBoxCoordinate.y}
|
||||||
|
width={boundingBoxDimensions.width}
|
||||||
|
height={boundingBoxDimensions.height}
|
||||||
|
stroke={'white'}
|
||||||
|
dash={[4, 4]}
|
||||||
|
strokeWidth={1}
|
||||||
|
ref={whiteStrokeRectRef}
|
||||||
|
listening={false}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws non-marching ants around the mask.
|
||||||
|
*/
|
||||||
|
const InpaintingBoundingBoxPreviewAnts = () => {
|
||||||
|
const { boundingBoxCoordinate, boundingBoxDimensions, dash, strokeWidth } =
|
||||||
|
useAppSelector(boundingBoxPreviewSelector);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group>
|
||||||
|
<Rect
|
||||||
|
x={boundingBoxCoordinate.x}
|
||||||
|
y={boundingBoxCoordinate.y}
|
||||||
|
width={boundingBoxDimensions.width}
|
||||||
|
height={boundingBoxDimensions.height}
|
||||||
|
stroke={'black'}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
dash={[dash, dash]}
|
||||||
|
dashOffset={0}
|
||||||
|
listening={false}
|
||||||
|
/>
|
||||||
|
<Rect
|
||||||
|
x={boundingBoxCoordinate.x}
|
||||||
|
y={boundingBoxCoordinate.y}
|
||||||
|
width={boundingBoxDimensions.width}
|
||||||
|
height={boundingBoxDimensions.height}
|
||||||
|
stroke={'white'}
|
||||||
|
dash={[dash, dash]}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
dashOffset={dash}
|
||||||
|
listening={false}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const boundingBoxPreviewTypeSelector = createSelector(
|
||||||
|
(state: RootState) => state.inpainting,
|
||||||
|
(inpainting: InpaintingState) => inpainting.boundingBoxPreviewType
|
||||||
|
);
|
||||||
|
|
||||||
|
const InpaintingBoundingBoxPreview = () => {
|
||||||
|
const boundingBoxPreviewType = useAppSelector(boundingBoxPreviewTypeSelector);
|
||||||
|
|
||||||
|
switch (boundingBoxPreviewType) {
|
||||||
|
case 'overlay': {
|
||||||
|
return <InpaintingBoundingBoxPreviewOverlay />;
|
||||||
|
}
|
||||||
|
case 'ants': {
|
||||||
|
return <InpaintingBoundingBoxPreviewAnts />;
|
||||||
|
}
|
||||||
|
case 'marchingAnts': {
|
||||||
|
return <InpaintingBoundingBoxPreviewMarchingAnts />;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InpaintingBoundingBoxPreview;
|
@ -0,0 +1,67 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { Circle } from 'react-konva';
|
||||||
|
import { RootState, useAppSelector } from '../../../../app/store';
|
||||||
|
import { InpaintingState } from '../inpaintingSlice';
|
||||||
|
import { rgbColorToString } from '../util/colorToString';
|
||||||
|
|
||||||
|
const inpaintingCanvasBrushPreviewSelector = createSelector(
|
||||||
|
(state: RootState) => state.inpainting,
|
||||||
|
(inpainting: InpaintingState) => {
|
||||||
|
const {
|
||||||
|
cursorPosition,
|
||||||
|
canvasDimensions: { width, height },
|
||||||
|
shouldShowBrushPreview,
|
||||||
|
brushSize,
|
||||||
|
maskColor,
|
||||||
|
tool,
|
||||||
|
} = inpainting;
|
||||||
|
|
||||||
|
return {
|
||||||
|
cursorPosition,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
shouldShowBrushPreview,
|
||||||
|
brushSize,
|
||||||
|
maskColorString: rgbColorToString(maskColor),
|
||||||
|
tool,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a black circle around the canvas brush preview.
|
||||||
|
*/
|
||||||
|
const InpaintingCanvasBrushPreview = () => {
|
||||||
|
const {
|
||||||
|
cursorPosition,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
shouldShowBrushPreview,
|
||||||
|
brushSize,
|
||||||
|
maskColorString,
|
||||||
|
tool,
|
||||||
|
} = useAppSelector(inpaintingCanvasBrushPreviewSelector);
|
||||||
|
|
||||||
|
if (!(cursorPosition || shouldShowBrushPreview)) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Circle
|
||||||
|
x={cursorPosition ? cursorPosition.x : width / 2}
|
||||||
|
y={cursorPosition ? cursorPosition.y : height / 2}
|
||||||
|
radius={brushSize / 2}
|
||||||
|
fill={maskColorString}
|
||||||
|
listening={false}
|
||||||
|
globalCompositeOperation={
|
||||||
|
tool === 'eraser' ? 'destination-out' : 'source-over'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InpaintingCanvasBrushPreview;
|
@ -0,0 +1,62 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { Circle } from 'react-konva';
|
||||||
|
import { RootState, useAppSelector } from '../../../../app/store';
|
||||||
|
import { InpaintingState } from '../inpaintingSlice';
|
||||||
|
|
||||||
|
const inpaintingCanvasBrushPreviewSelector = createSelector(
|
||||||
|
(state: RootState) => state.inpainting,
|
||||||
|
(inpainting: InpaintingState) => {
|
||||||
|
const {
|
||||||
|
cursorPosition,
|
||||||
|
canvasDimensions: { width, height },
|
||||||
|
shouldShowBrushPreview,
|
||||||
|
brushSize,
|
||||||
|
stageScale,
|
||||||
|
} = inpainting;
|
||||||
|
|
||||||
|
return {
|
||||||
|
cursorPosition,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
shouldShowBrushPreview,
|
||||||
|
brushSize,
|
||||||
|
strokeWidth: 1 / stageScale, // scale stroke thickness
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the canvas brush preview outline.
|
||||||
|
*/
|
||||||
|
const InpaintingCanvasBrushPreviewOutline = () => {
|
||||||
|
const {
|
||||||
|
cursorPosition,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
shouldShowBrushPreview,
|
||||||
|
brushSize,
|
||||||
|
strokeWidth,
|
||||||
|
} = useAppSelector(inpaintingCanvasBrushPreviewSelector);
|
||||||
|
|
||||||
|
if (!((cursorPosition || shouldShowBrushPreview) && width && height))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Circle
|
||||||
|
x={cursorPosition ? cursorPosition.x : width / 2}
|
||||||
|
y={cursorPosition ? cursorPosition.y : height / 2}
|
||||||
|
radius={brushSize / 2}
|
||||||
|
stroke={'rgba(0,0,0,1)'}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
strokeEnabled={true}
|
||||||
|
listening={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default InpaintingCanvasBrushPreviewOutline;
|
@ -0,0 +1,38 @@
|
|||||||
|
import { Line } from 'react-konva';
|
||||||
|
import { RootState, useAppSelector } from '../../../../app/store';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the lines which comprise the mask.
|
||||||
|
*
|
||||||
|
* Uses globalCompositeOperation to handle the brush and eraser tools.
|
||||||
|
*/
|
||||||
|
const InpaintingCanvasLines = () => {
|
||||||
|
const { lines, maskColor } = useAppSelector(
|
||||||
|
(state: RootState) => state.inpainting
|
||||||
|
);
|
||||||
|
const { r, g, b } = maskColor;
|
||||||
|
const maskColorString = `rgb(${r},${g},${b})`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{lines.map((line, i) => (
|
||||||
|
<Line
|
||||||
|
key={i}
|
||||||
|
points={line.points}
|
||||||
|
stroke={maskColorString}
|
||||||
|
strokeWidth={line.strokeWidth * 2}
|
||||||
|
tension={0}
|
||||||
|
lineCap="round"
|
||||||
|
lineJoin="round"
|
||||||
|
shadowForStrokeEnabled={false}
|
||||||
|
listening={false}
|
||||||
|
globalCompositeOperation={
|
||||||
|
line.tool === 'brush' ? 'source-over' : 'destination-out'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InpaintingCanvasLines;
|
@ -0,0 +1,101 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import {
|
||||||
|
RootState,
|
||||||
|
useAppDispatch,
|
||||||
|
useAppSelector,
|
||||||
|
} from '../../../../app/store';
|
||||||
|
import { OptionsState } from '../../../options/optionsSlice';
|
||||||
|
import { tabMap } from '../../InvokeTabs';
|
||||||
|
import {
|
||||||
|
InpaintingState,
|
||||||
|
toggleIsMovingBoundingBox,
|
||||||
|
toggleTool,
|
||||||
|
} from '../inpaintingSlice';
|
||||||
|
|
||||||
|
const keyboardEventManagerSelector = createSelector(
|
||||||
|
[(state: RootState) => state.options, (state: RootState) => state.inpainting],
|
||||||
|
(options: OptionsState, inpainting: InpaintingState) => {
|
||||||
|
const { shouldShowMask, cursorPosition } = inpainting;
|
||||||
|
return {
|
||||||
|
activeTabName: tabMap[options.activeTab],
|
||||||
|
shouldShowMask,
|
||||||
|
isCursorOnCanvas: Boolean(cursorPosition),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const KeyboardEventManager = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { shouldShowMask, activeTabName, isCursorOnCanvas } = useAppSelector(
|
||||||
|
keyboardEventManagerSelector
|
||||||
|
);
|
||||||
|
|
||||||
|
const isFirstEvent = useRef<boolean>(true);
|
||||||
|
const wasLastEventOverCanvas = useRef<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const listener = (e: KeyboardEvent) => {
|
||||||
|
if (!isCursorOnCanvas) {
|
||||||
|
wasLastEventOverCanvas.current = false;
|
||||||
|
|
||||||
|
if (isFirstEvent.current) {
|
||||||
|
isFirstEvent.current = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFirstEvent.current) {
|
||||||
|
wasLastEventOverCanvas.current = true;
|
||||||
|
isFirstEvent.current = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!['Alt', ' '].includes(e.key) ||
|
||||||
|
activeTabName !== 'inpainting' ||
|
||||||
|
!shouldShowMask ||
|
||||||
|
e.repeat
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wasLastEventOverCanvas.current) {
|
||||||
|
wasLastEventOverCanvas.current = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
switch (e.key) {
|
||||||
|
case 'Alt': {
|
||||||
|
dispatch(toggleTool());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ' ': {
|
||||||
|
dispatch(toggleIsMovingBoundingBox());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('adding listeners');
|
||||||
|
document.addEventListener('keydown', listener);
|
||||||
|
document.addEventListener('keyup', listener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', listener);
|
||||||
|
document.removeEventListener('keyup', listener);
|
||||||
|
};
|
||||||
|
}, [dispatch, activeTabName, shouldShowMask, isCursorOnCanvas]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KeyboardEventManager;
|
@ -0,0 +1,146 @@
|
|||||||
|
import { useToast } from '@chakra-ui/react';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
|
||||||
|
type UseInpaintingHotkeysConfig = {
|
||||||
|
activeTab: string;
|
||||||
|
brushSize: number;
|
||||||
|
handleChangeBrushSize: (newBrushSize: number) => void;
|
||||||
|
handleSelectEraserTool: () => void;
|
||||||
|
handleSelectBrushTool: () => void;
|
||||||
|
canUndo: boolean;
|
||||||
|
handleUndo: () => void;
|
||||||
|
canRedo: boolean;
|
||||||
|
handleRedo: () => void;
|
||||||
|
canClearMask: boolean;
|
||||||
|
handleClearMask: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useInpaintingHotkeys = (config: UseInpaintingHotkeysConfig) => {
|
||||||
|
const {
|
||||||
|
activeTab,
|
||||||
|
brushSize,
|
||||||
|
handleChangeBrushSize,
|
||||||
|
handleSelectEraserTool,
|
||||||
|
handleSelectBrushTool,
|
||||||
|
canUndo,
|
||||||
|
handleUndo,
|
||||||
|
canRedo,
|
||||||
|
handleRedo,
|
||||||
|
canClearMask,
|
||||||
|
handleClearMask,
|
||||||
|
} = config;
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
// Hotkeys
|
||||||
|
useHotkeys(
|
||||||
|
'[',
|
||||||
|
() => {
|
||||||
|
if (activeTab === 'inpainting' && brushSize - 5 > 0) {
|
||||||
|
handleChangeBrushSize(brushSize - 5);
|
||||||
|
} else {
|
||||||
|
handleChangeBrushSize(1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[brushSize]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
']',
|
||||||
|
() => {
|
||||||
|
if (activeTab === 'inpainting') {
|
||||||
|
handleChangeBrushSize(brushSize + 5);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[brushSize]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys('e', () => {
|
||||||
|
if (activeTab === 'inpainting') {
|
||||||
|
handleSelectEraserTool();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useHotkeys('b', () => {
|
||||||
|
if (activeTab === 'inpainting') {
|
||||||
|
handleSelectBrushTool();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'cmd+z',
|
||||||
|
() => {
|
||||||
|
if (activeTab === 'inpainting' && canUndo) {
|
||||||
|
handleUndo();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[canUndo]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'control+z',
|
||||||
|
() => {
|
||||||
|
if (activeTab === 'inpainting' && canUndo) {
|
||||||
|
handleUndo();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[canUndo]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'cmd+shift+z',
|
||||||
|
() => {
|
||||||
|
if (activeTab === 'inpainting' && canRedo) {
|
||||||
|
handleRedo();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[canRedo]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'control+shift+z',
|
||||||
|
() => {
|
||||||
|
if (activeTab === 'inpainting' && canRedo) {
|
||||||
|
handleRedo();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[canRedo]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'control+y',
|
||||||
|
() => {
|
||||||
|
if (activeTab === 'inpainting' && canRedo) {
|
||||||
|
handleRedo();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[canRedo]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'cmd+y',
|
||||||
|
() => {
|
||||||
|
if (activeTab === 'inpainting' && canRedo) {
|
||||||
|
handleRedo();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[canRedo]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'c',
|
||||||
|
() => {
|
||||||
|
if (activeTab === 'inpainting' && canClearMask) {
|
||||||
|
handleClearMask();
|
||||||
|
toast({
|
||||||
|
title: 'Mask Cleared',
|
||||||
|
status: 'success',
|
||||||
|
duration: 2500,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[canClearMask]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useInpaintingHotkeys;
|
14
frontend/src/features/tabs/Inpainting/index.tsx
Normal file
14
frontend/src/features/tabs/Inpainting/index.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import InpaintingPanel from './InpaintingPanel';
|
||||||
|
import InpaintingDisplay from './InpaintingDisplay';
|
||||||
|
import InvokeWorkarea from '../InvokeWorkarea';
|
||||||
|
|
||||||
|
export default function InpaintingWorkarea() {
|
||||||
|
return (
|
||||||
|
<InvokeWorkarea
|
||||||
|
optionsPanel={<InpaintingPanel />}
|
||||||
|
className="inpainting-workarea-container"
|
||||||
|
>
|
||||||
|
<InpaintingDisplay />
|
||||||
|
</InvokeWorkarea>
|
||||||
|
);
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user