mirror of
https://github.com/easydiffusion/easydiffusion.git
synced 2025-04-26 12:28:21 +02:00
Image Editor Updates (#612)
* fixed tools for image editor to be more modular and made cursor an actual cursor change * fixed eraser cursor positioning * updated opacity to not have a 100 option * separated clear into an actions section * added history support for image editor. ctrl-z and ctrl-y both work now * removed extra console log debugging stuff * updated buttons style * updated the button ui on the main page as requested * updated with a bunch of bugfixes
This commit is contained in:
parent
e7ca8090fd
commit
cb618efb98
@ -72,19 +72,23 @@
|
|||||||
<span id="init_image_size_box"></span>
|
<span id="init_image_size_box"></span>
|
||||||
<button class="init_image_clear image_clear_btn"><i class="fa-solid fa-xmark"></i></button>
|
<button class="init_image_clear image_clear_btn"><i class="fa-solid fa-xmark"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="button">
|
<div id="init_image_buttons">
|
||||||
<i class="fa-regular fa-folder-open"></i>
|
<div class="button">
|
||||||
Browse
|
<i class="fa-regular fa-folder-open"></i>
|
||||||
<input id="init_image" name="init_image" type="file" />
|
Browse
|
||||||
</div>
|
<input id="init_image" name="init_image" type="file" />
|
||||||
<div id="init_image_button_draw" class="button">
|
</div>
|
||||||
<i class="fa-solid fa-pencil"></i>
|
<div id="init_image_button_draw" class="button">
|
||||||
Draw
|
<i class="fa-solid fa-pencil"></i>
|
||||||
</div>
|
Draw
|
||||||
<div id="init_image_button_inpaint" class="button">
|
</div>
|
||||||
<i class="fa-solid fa-paintbrush"></i>
|
<div id="inpaint_button_container">
|
||||||
Inpaint
|
<div id="init_image_button_inpaint" class="button">
|
||||||
<input id="enable_mask" name="enable_mask" type="checkbox">
|
<i class="fa-solid fa-paintbrush"></i>
|
||||||
|
Inpaint
|
||||||
|
</div>
|
||||||
|
<input id="enable_mask" name="enable_mask" type="checkbox">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -85,8 +85,13 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.image_editor_tool .editor-options-container {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.image_editor_tool .editor-options-container > * {
|
.image_editor_tool .editor-options-container > * {
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
|
flex: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-controls-center {
|
.editor-controls-center {
|
||||||
@ -108,16 +113,6 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-controls-center .cursor-icon {
|
|
||||||
position: absolute;
|
|
||||||
left: 0px;
|
|
||||||
right: 0px;
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0;
|
|
||||||
width: 0px;
|
|
||||||
text-shadow: 0 0 5px black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-controls-right {
|
.editor-controls-right {
|
||||||
padding: 32px;
|
padding: 32px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -200,6 +195,10 @@
|
|||||||
#init_image_preview_container .button {
|
#init_image_preview_container .button {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
#image-editor .button {
|
|
||||||
|
.image-editor-popup .button {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
}
|
||||||
|
.image-editor-popup h4 {
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
@ -44,9 +44,6 @@ code {
|
|||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
.image_preview_container {
|
|
||||||
margin-top: 6px;
|
|
||||||
}
|
|
||||||
.image_clear_btn {
|
.image_clear_btn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transform: translate(30%, -30%);
|
transform: translate(30%, -30%);
|
||||||
@ -459,31 +456,35 @@ img {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#init_image_preview_container:not(.has-image) #inpaintingEditor,
|
#init_image_preview_container {
|
||||||
#init_image_preview_container:not(.has-image) #samplerSelection,
|
display: flex;
|
||||||
|
margin-top: 6px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
#init_image_preview_container:not(.has-image) #init_image_wrapper,
|
#init_image_preview_container:not(.has-image) #init_image_wrapper,
|
||||||
#init_image_preview_container:not(.has-image) #init_image_button_inpaint {
|
#init_image_preview_container:not(.has-image) #inpaint_button_container {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#init_image_preview_container:not(.has-image) {
|
|
||||||
grid-template-columns: 50% 50%;
|
#init_image_buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#init_image_preview_container {
|
#init_image_preview_container.has-image #init_image_buttons {
|
||||||
padding: 4px;
|
flex-direction: column;
|
||||||
display: grid;
|
padding-left: 8px;
|
||||||
grid-template-columns: max-content auto;
|
|
||||||
grid-template-rows: 33% 33% 33%;
|
|
||||||
gap: 8px
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#init_image_preview_container > div:not(:first-child) {
|
#init_image_buttons .button {
|
||||||
position: relative;
|
position: relative;
|
||||||
min-height: 32px;
|
height: 32px;
|
||||||
|
width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#init_image_preview_container > div:not(:first-child) > input {
|
#init_image_buttons .button > input {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -492,6 +493,12 @@ img {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#inpaint_button_container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
#init_image_wrapper {
|
#init_image_wrapper {
|
||||||
grid-row: span 3;
|
grid-row: span 3;
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -614,20 +621,27 @@ input[type="file"] {
|
|||||||
button,
|
button,
|
||||||
input::file-selector-button,
|
input::file-selector-button,
|
||||||
.button {
|
.button {
|
||||||
padding: 2px 4px;
|
padding: 6px;
|
||||||
border-radius: 4px;
|
border-radius: var(--input-border-radius);
|
||||||
background: var(--button-color);
|
background: var(--button-color);
|
||||||
color: var(--button-text-color);
|
color: var(--button-text-color);
|
||||||
border: var(--button-border);
|
border: var(--button-border);
|
||||||
|
height: 24px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
box-shadow: 2px 2px 1px 1px #00000088;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button i {
|
.button i {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button:hover,
|
||||||
|
.button:hover {
|
||||||
|
background: var(--background-color4)
|
||||||
|
}
|
||||||
|
|
||||||
input::file-selector-button {
|
input::file-selector-button {
|
||||||
padding: 0px 4px;
|
padding: 0px 4px;
|
||||||
height: 19px;
|
height: 19px;
|
||||||
@ -1029,11 +1043,6 @@ i.active {
|
|||||||
float: right;
|
float: right;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
button:hover,
|
|
||||||
.button:hover {
|
|
||||||
transition-duration: 0.1s;
|
|
||||||
background: hsl(var(--accent-hue), 100%, calc(var(--accent-lightness) + 6%));
|
|
||||||
}
|
|
||||||
|
|
||||||
button:active {
|
button:active {
|
||||||
transition-duration: 0.1s;
|
transition-duration: 0.1s;
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
--input-border-color: var(--background-color4);
|
--input-border-color: var(--background-color4);
|
||||||
|
|
||||||
--button-text-color: var(--input-text-color);
|
--button-text-color: var(--input-text-color);
|
||||||
--button-color: var(--accent-color);
|
--button-color: var(--input-background-color);
|
||||||
--button-border: none;
|
--button-border: none;
|
||||||
|
|
||||||
/* other */
|
/* other */
|
||||||
@ -42,7 +42,6 @@
|
|||||||
--background-color4: #cccccc;
|
--background-color4: #cccccc;
|
||||||
|
|
||||||
--text-color: black;
|
--text-color: black;
|
||||||
--button-text-color: white;
|
|
||||||
|
|
||||||
--input-text-color: black;
|
--input-text-color: black;
|
||||||
--input-background-color: #f8f9fa;
|
--input-background-color: #f8f9fa;
|
||||||
|
BIN
ui/media/images/fa-eraser.png
Normal file
BIN
ui/media/images/fa-eraser.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 11 KiB |
BIN
ui/media/images/fa-eye-dropper.png
Normal file
BIN
ui/media/images/fa-eye-dropper.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 12 KiB |
BIN
ui/media/images/fa-pencil.png
Normal file
BIN
ui/media/images/fa-pencil.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 10 KiB |
@ -128,10 +128,12 @@ const TASK_MAPPING = {
|
|||||||
},
|
},
|
||||||
mask: { name: 'Mask',
|
mask: { name: 'Mask',
|
||||||
setUI: (mask) => {
|
setUI: (mask) => {
|
||||||
inpaintingEditor.setImg(mask)
|
setTimeout(() => { // add a delay to insure this happens AFTER the main image loads (which reloads the inpainter)
|
||||||
|
imageInpainter.setImg(mask)
|
||||||
|
}, 250)
|
||||||
maskSetting.checked = Boolean(mask)
|
maskSetting.checked = Boolean(mask)
|
||||||
},
|
},
|
||||||
readUI: () => (maskSetting.checked ? inpaintingEditor.getImg() : undefined),
|
readUI: () => (maskSetting.checked ? imageInpainter.getImg() : undefined),
|
||||||
parse: (val) => val
|
parse: (val) => val
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -290,18 +292,11 @@ function restoreTaskToUI(task, fieldsToSkip) {
|
|||||||
// Show the source picture if present
|
// Show the source picture if present
|
||||||
initImagePreview.src = (task.reqBody.init_image == undefined ? '' : task.reqBody.init_image)
|
initImagePreview.src = (task.reqBody.init_image == undefined ? '' : task.reqBody.init_image)
|
||||||
if (IMAGE_REGEX.test(initImagePreview.src)) {
|
if (IMAGE_REGEX.test(initImagePreview.src)) {
|
||||||
Boolean(task.reqBody.mask) ? inpaintingEditor.setImg(task.reqBody.mask) : inpaintingEditor.resetBackground()
|
if (Boolean(task.reqBody.mask)) {
|
||||||
initImagePreviewContainer.style.display = 'block'
|
setTimeout(() => { // add a delay to insure this happens AFTER the main image loads (which reloads the inpainter)
|
||||||
inpaintingEditorContainer.style.display = 'none'
|
imageInpainter.setImg(task.reqBody.mask)
|
||||||
promptStrengthContainer.style.display = 'table-row'
|
}, 250)
|
||||||
//samplerSelectionContainer.style.display = 'none'
|
}
|
||||||
// maskSetting.checked = false
|
|
||||||
inpaintingEditorContainer.style.display = maskSetting.checked ? 'block' : 'none'
|
|
||||||
} else {
|
|
||||||
initImagePreviewContainer.style.display = 'none'
|
|
||||||
// inpaintingEditorContainer.style.display = 'none'
|
|
||||||
promptStrengthContainer.style.display = 'none'
|
|
||||||
// maskSetting.style.display = 'none'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function readUI() {
|
function readUI() {
|
||||||
|
@ -3,18 +3,11 @@ var editorControlsLeft = document.getElementById("image-editor-controls-left")
|
|||||||
const IMAGE_EDITOR_MAX_SIZE = 800
|
const IMAGE_EDITOR_MAX_SIZE = 800
|
||||||
|
|
||||||
const IMAGE_EDITOR_BUTTONS = [
|
const IMAGE_EDITOR_BUTTONS = [
|
||||||
{
|
|
||||||
name: "Clear",
|
|
||||||
icon: "fa-solid fa-xmark",
|
|
||||||
handler: editor => {
|
|
||||||
editor.clear()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "Cancel",
|
name: "Cancel",
|
||||||
icon: "fa-regular fa-circle-xmark",
|
icon: "fa-regular fa-circle-xmark",
|
||||||
handler: editor => {
|
handler: editor => {
|
||||||
editor.close()
|
editor.hide()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -26,16 +19,93 @@ const IMAGE_EDITOR_BUTTONS = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const defaultToolBegin = (editor, ctx, x, y, is_overlay = false) => {
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(x, y)
|
||||||
|
}
|
||||||
|
const defaultToolMove = (editor, ctx, x, y, is_overlay = false) => {
|
||||||
|
ctx.lineTo(x, y)
|
||||||
|
if (is_overlay) {
|
||||||
|
ctx.clearRect(0, 0, editor.width, editor.height)
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const defaultToolEnd = (editor, ctx, x, y, is_overlay = false) => {
|
||||||
|
ctx.stroke()
|
||||||
|
if (is_overlay) {
|
||||||
|
ctx.clearRect(0, 0, editor.width, editor.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const IMAGE_EDITOR_TOOLS = [
|
const IMAGE_EDITOR_TOOLS = [
|
||||||
{
|
{
|
||||||
id: "draw",
|
id: "draw",
|
||||||
name: "Draw",
|
name: "Draw",
|
||||||
icon: "fa-solid fa-pencil"
|
icon: "fa-solid fa-pencil",
|
||||||
|
cursor: "url(/media/images/fa-pencil.png) 0 24, pointer",
|
||||||
|
begin: defaultToolBegin,
|
||||||
|
move: defaultToolMove,
|
||||||
|
end: defaultToolEnd
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "erase",
|
id: "erase",
|
||||||
name: "Erase",
|
name: "Erase",
|
||||||
icon: "fa-solid fa-eraser"
|
icon: "fa-solid fa-eraser",
|
||||||
|
cursor: "url(/media/images/fa-eraser.png) 0 18, pointer",
|
||||||
|
begin: defaultToolBegin,
|
||||||
|
move: (editor, ctx, x, y, is_overlay = false) => {
|
||||||
|
ctx.lineTo(x, y)
|
||||||
|
if (is_overlay) {
|
||||||
|
ctx.clearRect(0, 0, editor.width, editor.height)
|
||||||
|
ctx.globalCompositeOperation = "source-over"
|
||||||
|
ctx.globalAlpha = 1
|
||||||
|
ctx.filter = "none"
|
||||||
|
ctx.drawImage(editor.canvas_current, 0, 0)
|
||||||
|
editor.setBrush(editor.layers.overlay)
|
||||||
|
ctx.stroke()
|
||||||
|
editor.canvas_current.style.opacity = 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
end: (editor, ctx, x, y, is_overlay = false) => {
|
||||||
|
ctx.stroke()
|
||||||
|
if (is_overlay) {
|
||||||
|
ctx.clearRect(0, 0, editor.width, editor.height)
|
||||||
|
editor.canvas_current.style.opacity = ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setBrush: (editor, layer) => {
|
||||||
|
layer.ctx.globalCompositeOperation = "destination-out"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "colorpicker",
|
||||||
|
name: "Color Picker",
|
||||||
|
icon: "fa-solid fa-eye-dropper",
|
||||||
|
cursor: "url(/media/images/fa-eye-dropper.png) 0 24, pointer",
|
||||||
|
begin: (editor, ctx, x, y, is_overlay = false) => {
|
||||||
|
var img_rgb = editor.layers.background.ctx.getImageData(x, y, 1, 1).data
|
||||||
|
var drawn_rgb = editor.ctx_current.getImageData(x, y, 1, 1).data
|
||||||
|
var drawn_opacity = drawn_rgb[3] / 255
|
||||||
|
editor.custom_color_input.value = rgbToHex({
|
||||||
|
r: (drawn_rgb[0] * drawn_opacity) + (img_rgb[0] * (1 - drawn_opacity)),
|
||||||
|
g: (drawn_rgb[1] * drawn_opacity) + (img_rgb[1] * (1 - drawn_opacity)),
|
||||||
|
b: (drawn_rgb[2] * drawn_opacity) + (img_rgb[2] * (1 - drawn_opacity)),
|
||||||
|
})
|
||||||
|
editor.custom_color_input.dispatchEvent(new Event("change"))
|
||||||
|
},
|
||||||
|
move: (editor, ctx, x, y, is_overlay = false) => {},
|
||||||
|
end: (editor, ctx, x, y, is_overlay = false) => {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const IMAGE_EDITOR_ACTIONS = [
|
||||||
|
{
|
||||||
|
id: "clear",
|
||||||
|
name: "Clear",
|
||||||
|
icon: "fa-solid fa-xmark",
|
||||||
|
handler: (editor) => {
|
||||||
|
editor.ctx_current.clearRect(0, 0, editor.width, editor.height)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -105,7 +175,7 @@ var IMAGE_EDITOR_SECTIONS = [
|
|||||||
name: "opacity",
|
name: "opacity",
|
||||||
title: "Opacity",
|
title: "Opacity",
|
||||||
default: 0,
|
default: 0,
|
||||||
options: [ 0, 0.25, 0.5, 0.75, 1 ],
|
options: [ 0, 0.2, 0.4, 0.6, 0.8 ],
|
||||||
initElement: (element, option) => {
|
initElement: (element, option) => {
|
||||||
element.style.background = `repeating-conic-gradient(rgba(0, 0, 0, ${option}) 0% 25%, rgba(255, 255, 255, ${option}) 0% 50%) 50% / 10px 10px`
|
element.style.background = `repeating-conic-gradient(rgba(0, 0, 0, ${option}) 0% 25%, rgba(255, 255, 255, ${option}) 0% 50%) 50% / 10px 10px`
|
||||||
}
|
}
|
||||||
@ -130,17 +200,118 @@ var IMAGE_EDITOR_SECTIONS = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
class EditorHistory {
|
||||||
|
constructor(editor) {
|
||||||
|
this.editor = editor
|
||||||
|
this.events = [] // stack of all events (actions/edits)
|
||||||
|
this.current_edit = null
|
||||||
|
this.rewind_index = 0 // how many events back into the history we've rewound to. (current state is just after event at index 'length - this.rewind_index - 1')
|
||||||
|
}
|
||||||
|
push(event) {
|
||||||
|
// probably add something here eventually to save state every x events
|
||||||
|
if (this.rewind_index != 0) {
|
||||||
|
this.events = this.events.slice(0, 0 - this.rewind_index)
|
||||||
|
this.rewind_index = 0
|
||||||
|
}
|
||||||
|
var snapshot_frequency = 20 // (every x edits, take a snapshot of the current drawing state, for faster rewinding)
|
||||||
|
if (this.events.length > 0 && this.events.length % snapshot_frequency == 0) {
|
||||||
|
event.snapshot = this.editor.layers.drawing.ctx.getImageData(0, 0, this.editor.width, this.editor.height)
|
||||||
|
}
|
||||||
|
this.events.push(event)
|
||||||
|
}
|
||||||
|
pushAction(action) {
|
||||||
|
this.push({
|
||||||
|
type: "action",
|
||||||
|
id: action
|
||||||
|
});
|
||||||
|
}
|
||||||
|
editBegin(x, y) {
|
||||||
|
this.current_edit = {
|
||||||
|
type: "edit",
|
||||||
|
id: this.editor.getOptionValue("tool"),
|
||||||
|
options: Object.assign({}, this.editor.options),
|
||||||
|
points: [ { x: x, y: y } ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editMove(x, y) {
|
||||||
|
if (this.current_edit) {
|
||||||
|
this.current_edit.points.push({ x: x, y: y })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editEnd(x, y) {
|
||||||
|
if (this.current_edit) {
|
||||||
|
this.push(this.current_edit)
|
||||||
|
this.current_edit = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clear() {
|
||||||
|
this.events = []
|
||||||
|
}
|
||||||
|
undo() {
|
||||||
|
this.rewindTo(this.rewind_index + 1)
|
||||||
|
}
|
||||||
|
redo() {
|
||||||
|
this.rewindTo(this.rewind_index - 1)
|
||||||
|
}
|
||||||
|
rewindTo(new_rewind_index) {
|
||||||
|
if (new_rewind_index < 0 || new_rewind_index > this.events.length) {
|
||||||
|
return; // do nothing if target index is out of bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
var ctx = this.editor.layers.drawing.ctx
|
||||||
|
ctx.clearRect(0, 0, this.editor.width, this.editor.height)
|
||||||
|
|
||||||
|
var target_index = this.events.length - 1 - new_rewind_index
|
||||||
|
var snapshot_index = target_index
|
||||||
|
while (snapshot_index > -1) {
|
||||||
|
if (this.events[snapshot_index].snapshot) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
snapshot_index--
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot_index != -1) {
|
||||||
|
ctx.putImageData(this.events[snapshot_index].snapshot, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = (snapshot_index + 1); i <= target_index; i++) {
|
||||||
|
var event = this.events[i]
|
||||||
|
if (event.type == "action") {
|
||||||
|
var action = IMAGE_EDITOR_ACTIONS.find(a => a.id == event.id)
|
||||||
|
action.handler(this.editor)
|
||||||
|
}
|
||||||
|
else if (event.type == "edit") {
|
||||||
|
var tool = IMAGE_EDITOR_TOOLS.find(t => t.id == event.id)
|
||||||
|
this.editor.setBrush(this.editor.layers.drawing, event.options)
|
||||||
|
|
||||||
|
var first_point = event.points[0]
|
||||||
|
tool.begin(this.editor, ctx, first_point.x, first_point.y)
|
||||||
|
for (var point_i = 1; point_i < event.points.length; point_i++) {
|
||||||
|
tool.move(this.editor, ctx, event.points[point_i].x, event.points[point_i].y)
|
||||||
|
}
|
||||||
|
var last_point = event.points[event.points.length - 1]
|
||||||
|
tool.end(this.editor, ctx, last_point.x, last_point.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// re-set brush to current settings
|
||||||
|
this.editor.setBrush(this.editor.layers.drawing)
|
||||||
|
|
||||||
|
this.rewind_index = new_rewind_index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ImageEditor {
|
class ImageEditor {
|
||||||
constructor(popup, inpainter = false) {
|
constructor(popup, inpainter = false) {
|
||||||
this.inpainter = inpainter
|
this.inpainter = inpainter
|
||||||
this.popup = popup
|
this.popup = popup
|
||||||
|
this.history = new EditorHistory(this)
|
||||||
if (inpainter) {
|
if (inpainter) {
|
||||||
this.popup.classList.add("inpainter")
|
this.popup.classList.add("inpainter")
|
||||||
}
|
}
|
||||||
this.drawing = false
|
this.drawing = false
|
||||||
this.dropper_active = false
|
this.temp_previous_tool = null // used for the ctrl-colorpicker functionality
|
||||||
this.container = popup.querySelector(".editor-controls-center > div")
|
this.container = popup.querySelector(".editor-controls-center > div")
|
||||||
this.cursor_icon = document.createElement("i")
|
|
||||||
this.layers = {}
|
this.layers = {}
|
||||||
var layer_names = [
|
var layer_names = [
|
||||||
"background",
|
"background",
|
||||||
@ -158,11 +329,6 @@ class ImageEditor {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.setSize(512, 512)
|
|
||||||
|
|
||||||
this.cursor_icon.classList.add("cursor-icon")
|
|
||||||
this.container.appendChild(this.cursor_icon)
|
|
||||||
|
|
||||||
// add mouse handlers
|
// add mouse handlers
|
||||||
this.container.addEventListener("mousedown", this.mouseHandler.bind(this))
|
this.container.addEventListener("mousedown", this.mouseHandler.bind(this))
|
||||||
this.container.addEventListener("mouseup", this.mouseHandler.bind(this))
|
this.container.addEventListener("mouseup", this.mouseHandler.bind(this))
|
||||||
@ -174,16 +340,6 @@ class ImageEditor {
|
|||||||
this.container.addEventListener("touchmove", this.mouseHandler.bind(this))
|
this.container.addEventListener("touchmove", this.mouseHandler.bind(this))
|
||||||
this.container.addEventListener("touchcancel", this.mouseHandler.bind(this))
|
this.container.addEventListener("touchcancel", this.mouseHandler.bind(this))
|
||||||
this.container.addEventListener("touchend", this.mouseHandler.bind(this))
|
this.container.addEventListener("touchend", this.mouseHandler.bind(this))
|
||||||
// setup forwarding for keypresses so the eyedropper works accordingly
|
|
||||||
var mouseHandlerHelper = this.mouseHandler.bind(this)
|
|
||||||
this.container.addEventListener("mouseenter",function() {
|
|
||||||
document.addEventListener("keyup", mouseHandlerHelper)
|
|
||||||
document.addEventListener("keydown", mouseHandlerHelper)
|
|
||||||
})
|
|
||||||
this.container.addEventListener("mouseout",function() {
|
|
||||||
document.removeEventListener("keyup", mouseHandlerHelper)
|
|
||||||
document.removeEventListener("keydown", mouseHandlerHelper)
|
|
||||||
})
|
|
||||||
|
|
||||||
// initialize editor controls
|
// initialize editor controls
|
||||||
this.options = {}
|
this.options = {}
|
||||||
@ -239,7 +395,36 @@ class ImageEditor {
|
|||||||
buttonContainer.appendChild(element)
|
buttonContainer.appendChild(element)
|
||||||
element.addEventListener("click", event => button.handler(this))
|
element.addEventListener("click", event => button.handler(this))
|
||||||
})
|
})
|
||||||
|
var actionsContainer = document.createElement("div")
|
||||||
|
var actionsTitle = document.createElement("h4")
|
||||||
|
actionsTitle.textContent = "Actions"
|
||||||
|
actionsContainer.appendChild(actionsTitle);
|
||||||
|
IMAGE_EDITOR_ACTIONS.forEach(action => {
|
||||||
|
var element = document.createElement("div")
|
||||||
|
var icon = document.createElement("i")
|
||||||
|
element.className = "image-editor-button button"
|
||||||
|
icon.className = action.icon
|
||||||
|
element.appendChild(icon)
|
||||||
|
element.append(action.name)
|
||||||
|
actionsContainer.appendChild(element)
|
||||||
|
element.addEventListener("click", event => this.runAction(action.id))
|
||||||
|
})
|
||||||
|
this.popup.querySelector(".editor-controls-right").appendChild(actionsContainer)
|
||||||
this.popup.querySelector(".editor-controls-right").appendChild(buttonContainer)
|
this.popup.querySelector(".editor-controls-right").appendChild(buttonContainer)
|
||||||
|
|
||||||
|
this.keyHandlerBound = this.keyHandler.bind(this)
|
||||||
|
|
||||||
|
this.setSize(512, 512)
|
||||||
|
}
|
||||||
|
show() {
|
||||||
|
this.popup.classList.add("active")
|
||||||
|
document.addEventListener("keydown", this.keyHandlerBound)
|
||||||
|
document.addEventListener("keyup", this.keyHandlerBound)
|
||||||
|
}
|
||||||
|
hide() {
|
||||||
|
this.popup.classList.remove("active")
|
||||||
|
document.removeEventListener("keydown", this.keyHandlerBound)
|
||||||
|
document.removeEventListener("keyup", this.keyHandlerBound)
|
||||||
}
|
}
|
||||||
setSize(width, height) {
|
setSize(width, height) {
|
||||||
if (width == this.width && height == this.height) {
|
if (width == this.width && height == this.height) {
|
||||||
@ -272,13 +457,15 @@ class ImageEditor {
|
|||||||
this.saveImage() // We've reset the size of the image so inpainting is different
|
this.saveImage() // We've reset the size of the image so inpainting is different
|
||||||
}
|
}
|
||||||
this.setBrush()
|
this.setBrush()
|
||||||
|
this.history.clear()
|
||||||
}
|
}
|
||||||
setCursorIcon(icon_class = null) {
|
get tool() {
|
||||||
if (icon_class == null) {
|
var tool_id = this.getOptionValue("tool")
|
||||||
var tool = this.getOptionValue("tool")
|
return IMAGE_EDITOR_TOOLS.find(t => t.id == tool_id);
|
||||||
icon_class = IMAGE_EDITOR_TOOLS.find(t => t.id == tool).icon
|
}
|
||||||
}
|
loadTool() {
|
||||||
this.cursor_icon.className = `cursor-icon ${icon_class}`
|
this.drawing = false
|
||||||
|
this.container.style.cursor = this.tool.cursor;
|
||||||
}
|
}
|
||||||
setImage(url, width, height) {
|
setImage(url, width, height) {
|
||||||
this.setSize(width, height)
|
this.setSize(width, height)
|
||||||
@ -297,6 +484,7 @@ class ImageEditor {
|
|||||||
this.layers.background.ctx.rect(0, 0, this.width, this.height)
|
this.layers.background.ctx.rect(0, 0, this.width, this.height)
|
||||||
this.layers.background.ctx.fill()
|
this.layers.background.ctx.fill()
|
||||||
}
|
}
|
||||||
|
this.history.clear()
|
||||||
}
|
}
|
||||||
saveImage() {
|
saveImage() {
|
||||||
if (!this.inpainter) {
|
if (!this.inpainter) {
|
||||||
@ -312,31 +500,47 @@ class ImageEditor {
|
|||||||
.some(channel => channel !== 0)
|
.some(channel => channel !== 0)
|
||||||
maskSetting.checked = !is_blank
|
maskSetting.checked = !is_blank
|
||||||
}
|
}
|
||||||
this.close()
|
this.hide()
|
||||||
}
|
}
|
||||||
getImg() { // a drop-in replacement of the drawingboard version
|
getImg() { // a drop-in replacement of the drawingboard version
|
||||||
return this.layers.drawing.canvas.toDataURL()
|
return this.layers.drawing.canvas.toDataURL()
|
||||||
}
|
}
|
||||||
close() {
|
setImg(dataUrl) { // a drop-in replacement of the drawingboard version
|
||||||
this.popup.classList.remove("active")
|
var image = new Image()
|
||||||
|
image.onload = () => {
|
||||||
|
var ctx = this.layers.drawing.ctx;
|
||||||
|
ctx.clearRect(0, 0, this.width, this.height)
|
||||||
|
ctx.globalCompositeOperation = "source-over"
|
||||||
|
ctx.globalAlpha = 1
|
||||||
|
ctx.filter = "none"
|
||||||
|
ctx.drawImage(image, 0, 0, this.width, this.height)
|
||||||
|
this.setBrush(this.layers.drawing)
|
||||||
|
}
|
||||||
|
image.src = dataUrl
|
||||||
}
|
}
|
||||||
clear() {
|
runAction(action_id) {
|
||||||
this.ctx_current.clearRect(0, 0, this.width, this.height)
|
var action = IMAGE_EDITOR_ACTIONS.find(a => a.id == action_id)
|
||||||
|
this.history.pushAction(action_id)
|
||||||
|
action.handler(this)
|
||||||
}
|
}
|
||||||
get eraser_active() {
|
setBrush(layer = null, options = null) {
|
||||||
return this.getOptionValue("tool") == "erase"
|
if (options == null) {
|
||||||
}
|
options = this.options
|
||||||
setBrush(layer = null) {
|
}
|
||||||
if (layer) {
|
if (layer) {
|
||||||
layer.ctx.lineCap = "round"
|
layer.ctx.lineCap = "round"
|
||||||
layer.ctx.lineJoin = "round"
|
layer.ctx.lineJoin = "round"
|
||||||
layer.ctx.lineWidth = this.getOptionValue("brush_size")
|
layer.ctx.lineWidth = options.brush_size
|
||||||
layer.ctx.fillStyle = this.getOptionValue("color")
|
layer.ctx.fillStyle = options.color
|
||||||
layer.ctx.strokeStyle = this.getOptionValue("color")
|
layer.ctx.strokeStyle = options.color
|
||||||
var sharpness = parseInt(this.getOptionValue("sharpness") * this.getOptionValue("brush_size"))
|
var sharpness = parseInt(options.sharpness * options.brush_size)
|
||||||
layer.ctx.filter = sharpness == 0 ? `none` : `blur(${sharpness}px)`
|
layer.ctx.filter = sharpness == 0 ? `none` : `blur(${sharpness}px)`
|
||||||
layer.ctx.globalAlpha = (1 - this.getOptionValue("opacity"))
|
layer.ctx.globalAlpha = (1 - options.opacity)
|
||||||
layer.ctx.globalCompositeOperation = this.eraser_active ? "destination-out" : "source-over"
|
layer.ctx.globalCompositeOperation = "source-over"
|
||||||
|
var tool = IMAGE_EDITOR_TOOLS.find(t => t.id == options.tool)
|
||||||
|
if (tool && tool.setBrush) {
|
||||||
|
tool.setBrush(editor, layer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Object.values([ "drawing", "overlay" ]).map(name => this.layers[name]).forEach(l => {
|
Object.values([ "drawing", "overlay" ]).map(name => this.layers[name]).forEach(l => {
|
||||||
@ -353,6 +557,39 @@ class ImageEditor {
|
|||||||
get canvas_current() {
|
get canvas_current() {
|
||||||
return this.layers.drawing.canvas
|
return this.layers.drawing.canvas
|
||||||
}
|
}
|
||||||
|
keyHandler(event) { // handles keybinds like ctrl+z, ctrl+y
|
||||||
|
if (!this.popup.classList.contains("active")) {
|
||||||
|
document.removeEventListener("keydown", this.keyHandlerBound)
|
||||||
|
document.removeEventListener("keyup", this.keyHandlerBound)
|
||||||
|
return // this catches if something else closes the window but doesnt properly unbind the key handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// keybindings
|
||||||
|
if (event.type == "keydown") {
|
||||||
|
if ((event.key == "z" || event.key == "Z") && event.ctrlKey) {
|
||||||
|
if (!event.shiftKey) {
|
||||||
|
this.history.undo()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.history.redo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.key == "y" && event.ctrlKey) {
|
||||||
|
this.history.redo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dropper ctrl holding handler stuff
|
||||||
|
var dropper_active = this.temp_previous_tool != null;
|
||||||
|
if (dropper_active && !event.ctrlKey) {
|
||||||
|
this.selectOption("tool", IMAGE_EDITOR_TOOLS.findIndex(t => t.id == this.temp_previous_tool))
|
||||||
|
this.temp_previous_tool = null
|
||||||
|
}
|
||||||
|
else if (!dropper_active && event.ctrlKey) {
|
||||||
|
this.temp_previous_tool = this.getOptionValue("tool")
|
||||||
|
this.selectOption("tool", IMAGE_EDITOR_TOOLS.findIndex(t => t.id == "colorpicker"))
|
||||||
|
}
|
||||||
|
}
|
||||||
mouseHandler(event) {
|
mouseHandler(event) {
|
||||||
var bbox = this.layers.overlay.canvas.getBoundingClientRect()
|
var bbox = this.layers.overlay.canvas.getBoundingClientRect()
|
||||||
var x = (event.clientX || 0) - bbox.left
|
var x = (event.clientX || 0) - bbox.left
|
||||||
@ -375,78 +612,26 @@ class ImageEditor {
|
|||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
// do drawing-related stuff
|
// do drawing-related stuff
|
||||||
if (type == "mousedown" || (type == "mouseenter" && event.buttons == 1)) {
|
if (type == "mousedown" || (type == "mouseenter" && event.buttons == 1)) {
|
||||||
if (this.dropper_active) {
|
this.drawing = true
|
||||||
var img_rgb = this.layers.background.ctx.getImageData(x, y, 1, 1).data
|
this.tool.begin(this, this.ctx_current, x, y)
|
||||||
var drw_rgb = this.ctx_current.getImageData(x, y, 1, 1).data
|
this.tool.begin(this, this.ctx_overlay, x, y, true)
|
||||||
var drw_opacity = drw_rgb[3] / 255
|
this.history.editBegin(x, y)
|
||||||
var test = rgbToHex({
|
|
||||||
r: (drw_rgb[0] * drw_opacity) + (img_rgb[0] * (1 - drw_opacity)),
|
|
||||||
g: (drw_rgb[1] * drw_opacity) + (img_rgb[1] * (1 - drw_opacity)),
|
|
||||||
b: (drw_rgb[2] * drw_opacity) + (img_rgb[2] * (1 - drw_opacity)),
|
|
||||||
})
|
|
||||||
this.custom_color_input.value = test
|
|
||||||
this.custom_color_input.dispatchEvent(new Event("change"))
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.drawing = true
|
|
||||||
this.ctx_overlay.beginPath()
|
|
||||||
this.ctx_overlay.moveTo(x, y)
|
|
||||||
this.ctx_current.beginPath()
|
|
||||||
this.ctx_current.moveTo(x, y)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (type == "mouseup" || type == "mousemove") {
|
if (type == "mouseup" || type == "mousemove") {
|
||||||
if (this.drawing) {
|
if (this.drawing) {
|
||||||
if (x > 0 && y > 0) {
|
if (x > 0 && y > 0) {
|
||||||
this.ctx_current.lineTo(x, y)
|
this.tool.move(this, this.ctx_current, x, y)
|
||||||
this.ctx_overlay.lineTo(x, y)
|
this.tool.move(this, this.ctx_overlay, x, y, true)
|
||||||
|
this.history.editMove(x, y)
|
||||||
// This isnt super efficient, but its the only way ive found to have clean updating for the drawing
|
|
||||||
this.ctx_overlay.clearRect(0, 0, this.width, this.height)
|
|
||||||
if (this.eraser_active) {
|
|
||||||
this.ctx_overlay.globalCompositeOperation = "source-over"
|
|
||||||
this.ctx_overlay.globalAlpha = 1
|
|
||||||
this.ctx_overlay.filter = "none"
|
|
||||||
this.ctx_overlay.drawImage(this.canvas_current, 0, 0)
|
|
||||||
this.setBrush(this.layers.overlay)
|
|
||||||
this.canvas_current.style.opacity = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ctx_overlay.stroke()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (type == "mouseup" || type == "mouseout") {
|
if (type == "mouseup" || type == "mouseout") {
|
||||||
if (this.drawing) {
|
if (this.drawing) {
|
||||||
this.drawing = false
|
this.drawing = false
|
||||||
this.ctx_current.stroke()
|
this.tool.end(this, this.ctx_current, x, y)
|
||||||
this.ctx_overlay.clearRect(0, 0, this.width, this.height)
|
this.tool.end(this, this.ctx_overlay, x, y, true)
|
||||||
|
this.history.editEnd(x, y)
|
||||||
if (this.eraser_active) {
|
|
||||||
this.canvas_current.style.opacity = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// cursor-icon stuff
|
|
||||||
if (type == "mousemove") {
|
|
||||||
this.cursor_icon.style.left = `${x + 10}px`
|
|
||||||
this.cursor_icon.style.top = `${y + 20}px`
|
|
||||||
}
|
|
||||||
if (type == "mouseenter") {
|
|
||||||
this.cursor_icon.style.opacity = 1
|
|
||||||
}
|
|
||||||
if (type == "mouseout") {
|
|
||||||
this.cursor_icon.style.opacity = 0
|
|
||||||
}
|
|
||||||
if ([ "mouseenter", "mousemove", "keydown", "keyup" ].includes(type)) {
|
|
||||||
if (this.dropper_active && !event.ctrlKey) {
|
|
||||||
this.dropper_active = false
|
|
||||||
this.setCursorIcon()
|
|
||||||
}
|
|
||||||
else if (!this.dropper_active && event.ctrlKey) {
|
|
||||||
this.dropper_active = true
|
|
||||||
this.setCursorIcon("fa-solid fa-eye-dropper")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -465,7 +650,7 @@ class ImageEditor {
|
|||||||
// change the editor
|
// change the editor
|
||||||
this.setBrush()
|
this.setBrush()
|
||||||
if (section.name == "tool") {
|
if (section.name == "tool") {
|
||||||
this.setCursorIcon()
|
this.loadTool()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -485,8 +670,8 @@ imageEditor.setImage(null, 512, 512)
|
|||||||
imageInpainter.setImage(null, 512, 512)
|
imageInpainter.setImage(null, 512, 512)
|
||||||
|
|
||||||
document.getElementById("init_image_button_draw").addEventListener("click", () => {
|
document.getElementById("init_image_button_draw").addEventListener("click", () => {
|
||||||
document.getElementById("image-editor").classList.toggle("active")
|
imageEditor.show()
|
||||||
})
|
})
|
||||||
document.getElementById("init_image_button_inpaint").addEventListener("click", () => {
|
document.getElementById("init_image_button_inpaint").addEventListener("click", () => {
|
||||||
document.getElementById("image-inpainter").classList.toggle("active")
|
imageInpainter.show()
|
||||||
})
|
})
|
||||||
|
@ -1254,7 +1254,7 @@ function checkRandomSeed() {
|
|||||||
randomSeedField.addEventListener('input', checkRandomSeed)
|
randomSeedField.addEventListener('input', checkRandomSeed)
|
||||||
checkRandomSeed()
|
checkRandomSeed()
|
||||||
|
|
||||||
function showInitImagePreview() {
|
function loadImg2ImgFromFile() {
|
||||||
if (initImageSelector.files.length === 0) {
|
if (initImageSelector.files.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1270,27 +1270,32 @@ function showInitImagePreview() {
|
|||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
initImageSelector.addEventListener('change', showInitImagePreview)
|
initImageSelector.addEventListener('change', loadImg2ImgFromFile)
|
||||||
showInitImagePreview()
|
loadImg2ImgFromFile()
|
||||||
|
|
||||||
initImagePreview.addEventListener('load', function() {
|
function img2imgLoad() {
|
||||||
promptStrengthContainer.style.display = 'table-row'
|
promptStrengthContainer.style.display = 'table-row'
|
||||||
|
samplerSelectionContainer.style.display = "none"
|
||||||
initImagePreviewContainer.classList.add("has-image")
|
initImagePreviewContainer.classList.add("has-image")
|
||||||
|
|
||||||
initImageSizeBox.textContent = initImagePreview.naturalWidth + " x " + initImagePreview.naturalHeight
|
initImageSizeBox.textContent = initImagePreview.naturalWidth + " x " + initImagePreview.naturalHeight
|
||||||
imageEditor.setImage(this.src, initImagePreview.naturalWidth, initImagePreview.naturalHeight)
|
imageEditor.setImage(this.src, initImagePreview.naturalWidth, initImagePreview.naturalHeight)
|
||||||
imageInpainter.setImage(this.src, parseInt(widthField.value), parseInt(heightField.value))
|
imageInpainter.setImage(this.src, parseInt(widthField.value), parseInt(heightField.value))
|
||||||
})
|
}
|
||||||
|
|
||||||
initImageClearBtn.addEventListener('click', function() {
|
function img2imgUnload() {
|
||||||
initImageSelector.value = null
|
initImageSelector.value = null
|
||||||
initImagePreview.src = ''
|
initImagePreview.src = ''
|
||||||
maskSetting.checked = false
|
maskSetting.checked = false
|
||||||
|
|
||||||
promptStrengthContainer.style.display = 'none'
|
promptStrengthContainer.style.display = "none"
|
||||||
|
samplerSelectionContainer.style.display = ""
|
||||||
initImagePreviewContainer.classList.remove("has-image")
|
initImagePreviewContainer.classList.remove("has-image")
|
||||||
imageEditor.setImage(null, parseInt(widthField.value), parseInt(heightField.value))
|
imageEditor.setImage(null, parseInt(widthField.value), parseInt(heightField.value))
|
||||||
})
|
|
||||||
|
}
|
||||||
|
initImagePreview.addEventListener('load', img2imgLoad)
|
||||||
|
initImageClearBtn.addEventListener('click', img2imgUnload)
|
||||||
|
|
||||||
maskSetting.addEventListener('click', function() {
|
maskSetting.addEventListener('click', function() {
|
||||||
onDimensionChange()
|
onDimensionChange()
|
||||||
|
Loading…
Reference in New Issue
Block a user