mirror of
https://github.com/easydiffusion/easydiffusion.git
synced 2024-11-26 02:05:09 +01:00
Image Editor (#574)
* started implementing hamunii's image editor, and added a hamunii theme * fixed so active tab is main tab * added some testing stuff for image ediotr * re-implemented canvas drawing myself. just need to add layer stuff now * moved everything to an image editor class and implement it so it actually works nicely now * fixed a couple weird bugs and cleaned up the background image and sharpness stuff * cleaned up a lot of stuff about the editor, added tools, buttons, made it mostly work in the current ui * added inpainting support * updated with more nice changes/updates to the inpainting and drawing editor * made some more fixes and touchups to the image editor * removed a bunch of semicolons * remove old image inpainting system * updated to work properly on mobile * made a minor bugfix * fixed img_size_box alignment * Update index.html Co-authored-by: cmdr2 <secondary.cmdr2@gmail.com> Co-authored-by: cmdr2 <shashank.shekhar.global@gmail.com>
This commit is contained in:
parent
ca9413ccf4
commit
277140f218
@ -12,12 +12,11 @@
|
||||
<link rel="stylesheet" href="/media/css/auto-save.css">
|
||||
<link rel="stylesheet" href="/media/css/modifier-thumbnails.css">
|
||||
<link rel="stylesheet" href="/media/css/fontawesome-all.min.css">
|
||||
<link rel="stylesheet" href="/media/css/drawingboard.min.css">
|
||||
<link rel="stylesheet" href="/media/css/image-editor.css">
|
||||
<link rel="stylesheet" href="/media/css/jquery-confirm.min.css">
|
||||
<link rel="manifest" href="/media/manifest.webmanifest">
|
||||
<script src="/media/js/jquery-3.6.1.min.js"></script>
|
||||
<script src="/media/js/jquery-confirm.min.js"></script>
|
||||
<script src="/media/js/drawingboard.min.js"></script>
|
||||
<script src="/media/js/marked.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
@ -65,7 +64,7 @@
|
||||
</div>
|
||||
|
||||
<div id="editor-inputs-init-image" class="row">
|
||||
<label for="init_image">Initial Image (img2img) <small>(optional)</small> </label> <input id="init_image" name="init_image" type="file" /><br/>
|
||||
<label for="init_image">Initial Image (img2img) <small>(optional)</small> </label>
|
||||
|
||||
<div id="init_image_preview_container" class="image_preview_container">
|
||||
<div id="init_image_wrapper">
|
||||
@ -73,16 +72,22 @@
|
||||
<span id="init_image_size_box"></span>
|
||||
<button class="init_image_clear image_clear_btn"><i class="fa-solid fa-xmark"></i></button>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<input id="enable_mask" name="enable_mask" type="checkbox">
|
||||
<label for="enable_mask">
|
||||
In-Painting (beta)
|
||||
<a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/Inpainting" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip right">Click to learn more about InPainting</span></i></a>
|
||||
<small>(select the area which the AI will paint into)</small>
|
||||
</label>
|
||||
<div id="inpaintingEditor"></div>
|
||||
<div class="button">
|
||||
<i class="fa-regular fa-folder-open"></i>
|
||||
Browse
|
||||
<input id="init_image" name="init_image" type="file" />
|
||||
</div>
|
||||
<div id="init_image_button_draw" class="button">
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
Draw
|
||||
</div>
|
||||
<div id="init_image_button_inpaint" class="button">
|
||||
<i class="fa-solid fa-paintbrush"></i>
|
||||
Inpaint
|
||||
<input id="enable_mask" name="enable_mask" type="checkbox">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="editor-inputs-tags-container" class="row">
|
||||
@ -330,6 +335,38 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="image-editor" class="popup image-editor-popup">
|
||||
<div>
|
||||
<i class="close-button fa-solid fa-xmark"></i>
|
||||
<h1>Image Editor</h1>
|
||||
<div class="flex-container">
|
||||
<div class="editor-controls-left"></div>
|
||||
<div class="editor-controls-center">
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="editor-controls-right">
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="image-inpainter" class="popup image-editor-popup">
|
||||
<div>
|
||||
<i class="close-button fa-solid fa-xmark"></i>
|
||||
<h1>Inpainter</h1>
|
||||
<div class="flex-container">
|
||||
<div class="editor-controls-left"></div>
|
||||
<div class="editor-controls-center">
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="editor-controls-right">
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="footer-spacer"></div>
|
||||
<div id="footer">
|
||||
<div class="line-separator"> </div>
|
||||
@ -343,16 +380,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script src="media/js/utils.js"></script>
|
||||
<script src="media/js/parameters.js"></script>
|
||||
<script src="media/js/plugins.js"></script>
|
||||
<script src="media/js/inpainting-editor.js"></script>
|
||||
<script src="media/js/image-modifiers.js"></script>
|
||||
<script src="media/js/auto-save.js"></script>
|
||||
<script src="media/js/main.js"></script>
|
||||
<script src="media/js/themes.js"></script>
|
||||
<script src="media/js/dnd.js"></script>
|
||||
<script src="media/js/image-editor.js"></script>
|
||||
<script>
|
||||
async function init() {
|
||||
await initSettings()
|
||||
|
5
ui/media/css/drawingboard.min.css
vendored
5
ui/media/css/drawingboard.min.css
vendored
File diff suppressed because one or more lines are too long
199
ui/media/css/image-editor.css
Normal file
199
ui/media/css/image-editor.css
Normal file
@ -0,0 +1,199 @@
|
||||
.editor-controls-left {
|
||||
padding-left: 32px;
|
||||
text-align: left;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.editor-options-container {
|
||||
display: flex;
|
||||
row-gap: 10px;
|
||||
max-width: 210px;
|
||||
}
|
||||
|
||||
.editor-options-container > * {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.editor-options-container > * > * {
|
||||
position: inherit;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 16px;
|
||||
background: var(--background-color3);
|
||||
cursor: pointer;
|
||||
transition: opacity 0.25s;
|
||||
}
|
||||
.editor-options-container > * > *:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.editor-options-container > * > *.active {
|
||||
border: 2px solid #3584e4;
|
||||
}
|
||||
|
||||
.image_editor_opacity .editor-options-container > * > *:not(.active) {
|
||||
border: 1px solid var(--background-color3);
|
||||
}
|
||||
|
||||
.image_editor_color .editor-options-container {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.image_editor_color .editor-options-container > * {
|
||||
flex: 20%;
|
||||
}
|
||||
.image_editor_color .editor-options-container > * > * {
|
||||
position: relative;
|
||||
}
|
||||
.image_editor_color .editor-options-container > * > *.active::before {
|
||||
content: "\f00c";
|
||||
display: var(--fa-display,inline-block);
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
line-height: 1;
|
||||
text-rendering: auto;
|
||||
font-family: var(--fa-style-family, "Font Awesome 6 Free");
|
||||
font-weight: var(--fa-style, 900);
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%) scale(125%);
|
||||
color: black;
|
||||
}
|
||||
.image_editor_color .editor-options-container > *:first-child {
|
||||
flex: 100%;
|
||||
}
|
||||
.image_editor_color .editor-options-container > *:first-child > * {
|
||||
width: 100%;
|
||||
}
|
||||
.image_editor_color .editor-options-container > *:first-child > * > input {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.image_editor_color .editor-options-container > *:first-child > * > span {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
opacity: 0.5;
|
||||
}
|
||||
.image_editor_color .editor-options-container > *:first-child > *.active > span {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.image_editor_tool .editor-options-container > * {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.editor-controls-center {
|
||||
/* background: var(--background-color2); */
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.editor-controls-center > div {
|
||||
position: relative;
|
||||
background: black;
|
||||
}
|
||||
|
||||
.editor-controls-center canvas {
|
||||
position: absolute;
|
||||
left: 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 {
|
||||
padding: 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
.editor-controls-right > div:last-child {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 200px;
|
||||
gap: 5px;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.image-editor-button {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
border-radius: 16px;
|
||||
background: var(--background-color3);
|
||||
}
|
||||
|
||||
#init_image_button_inpaint .input-toggle {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
}
|
||||
|
||||
#init_image_button_inpaint .input-toggle input:not(:checked) ~ label {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
.image-editor-popup {
|
||||
--popup-margin: 16px;
|
||||
--popup-padding: 24px;
|
||||
}
|
||||
|
||||
.image-editor-popup > div {
|
||||
margin: var(--popup-margin);
|
||||
padding: var(--popup-padding);
|
||||
min-height: calc(100vh - (2 * var(--popup-margin)));
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.image-editor-popup h1 {
|
||||
position: absolute;
|
||||
top: 32px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 700px) {
|
||||
.image-editor-popup > div {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.image-editor-popup h1 {
|
||||
position: relative;
|
||||
transform: none;
|
||||
left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.image-editor-popup > div > div {
|
||||
min-height: calc(100vh - (2 * var(--popup-margin)) - (2 * var(--popup-padding)));
|
||||
}
|
||||
|
||||
.inpainter .image_editor_color {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.inpainter .editor-canvas-background {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ code {
|
||||
display: block;
|
||||
}
|
||||
.image_preview_container {
|
||||
margin-top: 10pt;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.image_clear_btn {
|
||||
position: absolute;
|
||||
@ -275,32 +275,6 @@ img {
|
||||
transform: translateY(25%);
|
||||
}
|
||||
|
||||
#inpaintingEditor {
|
||||
width: 300pt;
|
||||
height: 300pt;
|
||||
margin-top: 5pt;
|
||||
}
|
||||
.drawing-board-canvas-wrapper {
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
.drawing-board-controls {
|
||||
min-width: 273px;
|
||||
}
|
||||
.drawing-board-control > button {
|
||||
background-color: #eee;
|
||||
border-radius: 3pt;
|
||||
}
|
||||
.drawing-board-control-inner {
|
||||
background-color: #eee;
|
||||
border-radius: 3pt;
|
||||
}
|
||||
#inpaintingEditor canvas {
|
||||
opacity: 0.6;
|
||||
}
|
||||
#enable_mask {
|
||||
margin-top: 8pt;
|
||||
}
|
||||
|
||||
#top-nav {
|
||||
position: relative;
|
||||
background: var(--background-color4);
|
||||
@ -484,8 +458,48 @@ img {
|
||||
#prompt_from_file {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#init_image_preview_container:not(.has-image) #inpaintingEditor,
|
||||
#init_image_preview_container:not(.has-image) #samplerSelection,
|
||||
#init_image_preview_container:not(.has-image) #init_image_wrapper,
|
||||
#init_image_preview_container:not(.has-image) #init_image_button_inpaint {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#init_image_preview_container:not(.has-image) {
|
||||
grid-template-columns: 50% 50%;
|
||||
}
|
||||
|
||||
#init_image_preview_container {
|
||||
padding: 4px;
|
||||
display: grid;
|
||||
grid-template-columns: max-content auto;
|
||||
grid-template-rows: 33% 33% 33%;
|
||||
gap: 8px
|
||||
}
|
||||
|
||||
#init_image_preview_container > div:not(:first-child) {
|
||||
position: relative;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
#init_image_preview_container > div:not(:first-child) > input {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#init_image_wrapper {
|
||||
grid-row: span 3;
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
max-height: 150px;
|
||||
}
|
||||
|
||||
#init_image_preview {
|
||||
max-width: 150px;
|
||||
max-height: 150px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
@ -493,23 +507,18 @@ img {
|
||||
border-radius: 6px;
|
||||
transition: all 1s ease-in-out;
|
||||
}
|
||||
|
||||
/*
|
||||
#init_image_preview:hover {
|
||||
max-width: 500px;
|
||||
max-height: 1000px;
|
||||
|
||||
transition: all 1s 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
#init_image_wrapper {
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
}
|
||||
} */
|
||||
|
||||
#init_image_size_box {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
bottom: 3px;
|
||||
bottom: 0px;
|
||||
padding: 3px;
|
||||
background: black;
|
||||
color: white;
|
||||
@ -561,6 +570,10 @@ option {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="file"] * {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
@ -599,12 +612,21 @@ input[type="file"] {
|
||||
}
|
||||
|
||||
button,
|
||||
input::file-selector-button {
|
||||
input::file-selector-button,
|
||||
.button {
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
background: var(--button-color);
|
||||
color: var(--button-text-color);
|
||||
border: var(--button-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button i {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
input::file-selector-button {
|
||||
@ -969,8 +991,8 @@ input::file-selector-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#tab-content-wrapper {
|
||||
border-top: 8px solid var(--background-color1);
|
||||
#tab-content-wrapper > * {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.tab-content-inner {
|
||||
@ -1007,7 +1029,8 @@ i.active {
|
||||
float: right;
|
||||
font-weight: bold;
|
||||
}
|
||||
button:hover {
|
||||
button:hover,
|
||||
.button:hover {
|
||||
transition-duration: 0.1s;
|
||||
background: hsl(var(--accent-hue), 100%, calc(var(--accent-lightness) + 6%));
|
||||
}
|
||||
|
@ -134,6 +134,7 @@
|
||||
--input-border-color: #005E05;
|
||||
}
|
||||
|
||||
|
||||
.theme-gnomie {
|
||||
--background-color1: #242424;
|
||||
--background-color2: #353535;
|
||||
@ -158,4 +159,3 @@
|
||||
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.25);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
|
4
ui/media/js/drawingboard.min.js
vendored
4
ui/media/js/drawingboard.min.js
vendored
File diff suppressed because one or more lines are too long
492
ui/media/js/image-editor.js
Normal file
492
ui/media/js/image-editor.js
Normal file
@ -0,0 +1,492 @@
|
||||
var editorControlsLeft = document.getElementById("image-editor-controls-left")
|
||||
|
||||
const IMAGE_EDITOR_MAX_SIZE = 800
|
||||
|
||||
const IMAGE_EDITOR_BUTTONS = [
|
||||
{
|
||||
name: "Clear",
|
||||
icon: "fa-solid fa-xmark",
|
||||
handler: editor => {
|
||||
editor.clear()
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Cancel",
|
||||
icon: "fa-regular fa-circle-xmark",
|
||||
handler: editor => {
|
||||
editor.close()
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Save",
|
||||
icon: "fa-solid fa-floppy-disk",
|
||||
handler: editor => {
|
||||
editor.saveImage()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const IMAGE_EDITOR_TOOLS = [
|
||||
{
|
||||
id: "draw",
|
||||
name: "Draw",
|
||||
icon: "fa-solid fa-pencil"
|
||||
},
|
||||
{
|
||||
id: "erase",
|
||||
name: "Erase",
|
||||
icon: "fa-solid fa-eraser"
|
||||
}
|
||||
]
|
||||
|
||||
var IMAGE_EDITOR_SECTIONS = [
|
||||
{
|
||||
name: "tool",
|
||||
title: "Tool",
|
||||
default: "draw",
|
||||
options: Array.from(IMAGE_EDITOR_TOOLS.map(t => t.id)),
|
||||
initElement: (element, option) => {
|
||||
var tool_info = IMAGE_EDITOR_TOOLS.find(t => t.id == option)
|
||||
element.className = "image-editor-button button"
|
||||
var sub_element = document.createElement("div")
|
||||
var icon = document.createElement("i")
|
||||
tool_info.icon.split(" ").forEach(c => icon.classList.add(c))
|
||||
sub_element.appendChild(icon)
|
||||
sub_element.append(tool_info.name)
|
||||
element.appendChild(sub_element)
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "color",
|
||||
title: "Color",
|
||||
default: "#f1c232",
|
||||
options: [
|
||||
"custom",
|
||||
"#ea9999", "#e06666", "#cc0000", "#990000", "#660000",
|
||||
"#f9cb9c", "#f6b26b", "#e69138", "#b45f06", "#783f04",
|
||||
"#ffe599", "#ffd966", "#f1c232", "#bf9000", "#7f6000",
|
||||
"#b6d7a8", "#93c47d", "#6aa84f", "#38761d", "#274e13",
|
||||
"#a4c2f4", "#6d9eeb", "#3c78d8", "#1155cc", "#1c4587",
|
||||
"#b4a7d6", "#8e7cc3", "#674ea7", "#351c75", "#20124d",
|
||||
"#d5a6bd", "#c27ba0", "#a64d79", "#741b47", "#4c1130",
|
||||
"#ffffff", "#c0c0c0", "#838383", "#525252", "#000000",
|
||||
],
|
||||
initElement: (element, option) => {
|
||||
if (option == "custom") {
|
||||
var input = document.createElement("input")
|
||||
input.type = "color"
|
||||
element.appendChild(input)
|
||||
var span = document.createElement("span")
|
||||
span.textContent = "Custom"
|
||||
element.appendChild(span)
|
||||
}
|
||||
else {
|
||||
element.style.background = option
|
||||
}
|
||||
},
|
||||
getCustom: editor => {
|
||||
var input = editor.popup.querySelector(".image_editor_color input")
|
||||
return input.value
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "brush_size",
|
||||
title: "Brush Size",
|
||||
default: 48,
|
||||
options: [ 16, 24, 32, 48, 64 ],
|
||||
initElement: (element, option) => {
|
||||
element.parentElement.style.flex = option
|
||||
element.style.width = option + "px"
|
||||
element.style.height = option + "px"
|
||||
element.style["border-radius"] = (option / 2).toFixed() + "px"
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "opacity",
|
||||
title: "Opacity",
|
||||
default: 0,
|
||||
options: [ 0, 0.25, 0.5, 0.75, 1 ],
|
||||
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`
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "sharpness",
|
||||
title: "Sharpness",
|
||||
default: 0,
|
||||
options: [ 0, 0.05, 0.1, 0.2, 0.3 ],
|
||||
initElement: (element, option) => {
|
||||
var size = 32
|
||||
var blur_amount = parseInt(option * size)
|
||||
var sub_element = document.createElement("div")
|
||||
sub_element.style.background = `var(--background-color3)`
|
||||
sub_element.style.filter = `blur(${blur_amount}px)`
|
||||
sub_element.style.width = `${size - 4}px`
|
||||
sub_element.style.height = `${size - 4}px`
|
||||
sub_element.style['border-radius'] = `${size}px`
|
||||
element.style.background = "none"
|
||||
element.appendChild(sub_element)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
class ImageEditor {
|
||||
constructor(popup, inpainter = false) {
|
||||
this.inpainter = inpainter
|
||||
this.popup = popup
|
||||
if (inpainter) {
|
||||
this.popup.classList.add("inpainter")
|
||||
}
|
||||
this.drawing = false
|
||||
this.dropper_active = false
|
||||
this.container = popup.querySelector(".editor-controls-center > div")
|
||||
this.cursor_icon = document.createElement("i")
|
||||
this.layers = {}
|
||||
var layer_names = [
|
||||
"background",
|
||||
"drawing",
|
||||
"overlay"
|
||||
]
|
||||
layer_names.forEach(name => {
|
||||
let canvas = document.createElement("canvas")
|
||||
canvas.className = `editor-canvas-${name}`
|
||||
this.container.appendChild(canvas)
|
||||
this.layers[name] = {
|
||||
name: name,
|
||||
canvas: canvas,
|
||||
ctx: canvas.getContext("2d")
|
||||
}
|
||||
})
|
||||
|
||||
this.setSize(512, 512)
|
||||
|
||||
this.cursor_icon.classList.add("cursor-icon")
|
||||
this.container.appendChild(this.cursor_icon)
|
||||
|
||||
// add mouse handlers
|
||||
this.container.addEventListener("mousedown", this.mouseHandler.bind(this))
|
||||
this.container.addEventListener("mouseup", this.mouseHandler.bind(this))
|
||||
this.container.addEventListener("mousemove", this.mouseHandler.bind(this))
|
||||
this.container.addEventListener("mouseout", this.mouseHandler.bind(this))
|
||||
this.container.addEventListener("mouseenter", this.mouseHandler.bind(this))
|
||||
|
||||
this.container.addEventListener("touchstart", this.mouseHandler.bind(this))
|
||||
this.container.addEventListener("touchmove", this.mouseHandler.bind(this))
|
||||
this.container.addEventListener("touchcancel", 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
|
||||
this.options = {}
|
||||
this.optionElements = {}
|
||||
IMAGE_EDITOR_SECTIONS.forEach(section => {
|
||||
section.id = `image_editor_${section.name}`
|
||||
var sectionElement = document.createElement("div")
|
||||
sectionElement.className = section.id
|
||||
|
||||
var title = document.createElement("h4")
|
||||
title.innerText = section.title
|
||||
sectionElement.appendChild(title)
|
||||
|
||||
var optionsContainer = document.createElement("div")
|
||||
optionsContainer.classList.add("editor-options-container")
|
||||
|
||||
this.optionElements[section.name] = []
|
||||
section.options.forEach((option, index) => {
|
||||
var optionHolder = document.createElement("div")
|
||||
var optionElement = document.createElement("div")
|
||||
optionHolder.appendChild(optionElement)
|
||||
section.initElement(optionElement, option)
|
||||
optionElement.addEventListener("click", target => this.selectOption(section.name, index))
|
||||
optionsContainer.appendChild(optionHolder)
|
||||
this.optionElements[section.name].push(optionElement)
|
||||
})
|
||||
this.selectOption(section.name, section.options.indexOf(section.default))
|
||||
|
||||
sectionElement.appendChild(optionsContainer)
|
||||
|
||||
this.popup.querySelector(".editor-controls-left").appendChild(sectionElement)
|
||||
})
|
||||
|
||||
this.custom_color_input = this.popup.querySelector(`input[type="color"]`)
|
||||
this.custom_color_input.addEventListener("change", () => {
|
||||
this.custom_color_input.parentElement.style.background = this.custom_color_input.value
|
||||
this.selectOption("color", 0)
|
||||
})
|
||||
|
||||
if (this.inpainter) {
|
||||
this.selectOption("color", IMAGE_EDITOR_SECTIONS.find(s => s.name == "color").options.indexOf("#ffffff"))
|
||||
}
|
||||
|
||||
// initialize the right-side controls
|
||||
var buttonContainer = document.createElement("div")
|
||||
IMAGE_EDITOR_BUTTONS.forEach(button => {
|
||||
var element = document.createElement("div")
|
||||
var icon = document.createElement("i")
|
||||
element.className = "image-editor-button button"
|
||||
icon.className = button.icon
|
||||
element.appendChild(icon)
|
||||
element.append(button.name)
|
||||
buttonContainer.appendChild(element)
|
||||
element.addEventListener("click", event => button.handler(this))
|
||||
})
|
||||
this.popup.querySelector(".editor-controls-right").appendChild(buttonContainer)
|
||||
}
|
||||
setSize(width, height) {
|
||||
if (width == this.width && height == this.height) {
|
||||
return
|
||||
}
|
||||
|
||||
var max_size = Math.min(parseInt(window.innerWidth * 0.9), width, 768)
|
||||
if (width > height) {
|
||||
var multiplier = max_size / width
|
||||
width = (multiplier * width).toFixed()
|
||||
height = (multiplier * height).toFixed()
|
||||
}
|
||||
else {
|
||||
var multiplier = max_size / height
|
||||
width = (multiplier * width).toFixed()
|
||||
height = (multiplier * height).toFixed()
|
||||
}
|
||||
this.width = width
|
||||
this.height = height
|
||||
|
||||
this.container.style.width = width + "px"
|
||||
this.container.style.height = height + "px"
|
||||
|
||||
Object.values(this.layers).forEach(layer => {
|
||||
layer.canvas.width = width
|
||||
layer.canvas.height = height
|
||||
})
|
||||
|
||||
if (this.inpainter) {
|
||||
this.saveImage() // We've reset the size of the image so inpainting is different
|
||||
}
|
||||
this.setBrush()
|
||||
}
|
||||
setCursorIcon(icon_class = null) {
|
||||
if (icon_class == null) {
|
||||
var tool = this.getOptionValue("tool")
|
||||
icon_class = IMAGE_EDITOR_TOOLS.find(t => t.id == tool).icon
|
||||
}
|
||||
this.cursor_icon.className = `cursor-icon ${icon_class}`
|
||||
}
|
||||
setImage(url, width, height) {
|
||||
this.setSize(width, height)
|
||||
this.layers.drawing.ctx.clearRect(0, 0, this.width, this.height)
|
||||
this.layers.background.ctx.clearRect(0, 0, this.width, this.height)
|
||||
if (url) {
|
||||
var image = new Image()
|
||||
image.onload = () => {
|
||||
this.layers.background.ctx.drawImage(image, 0, 0, this.width, this.height)
|
||||
}
|
||||
image.src = url
|
||||
}
|
||||
else {
|
||||
this.layers.background.ctx.fillStyle = "#ffffff"
|
||||
this.layers.background.ctx.beginPath()
|
||||
this.layers.background.ctx.rect(0, 0, this.width, this.height)
|
||||
this.layers.background.ctx.fill()
|
||||
}
|
||||
}
|
||||
saveImage() {
|
||||
if (!this.inpainter) {
|
||||
// This is not an inpainter, so save the image as the new img2img input
|
||||
this.layers.background.ctx.drawImage(this.layers.drawing.canvas, 0, 0, this.width, this.height)
|
||||
var base64 = this.layers.background.canvas.toDataURL()
|
||||
initImagePreview.src = base64 // this will trigger the rest of the app to use it
|
||||
}
|
||||
else {
|
||||
// This is an inpainter, so make sure the toggle is set accordingly
|
||||
var is_blank = !this.layers.drawing.ctx
|
||||
.getImageData(0, 0, this.width, this.height).data
|
||||
.some(channel => channel !== 0)
|
||||
maskSetting.checked = !is_blank
|
||||
}
|
||||
this.close()
|
||||
}
|
||||
getImg() { // a drop-in replacement of the drawingboard version
|
||||
return this.layers.drawing.canvas.toDataURL()
|
||||
}
|
||||
close() {
|
||||
this.popup.classList.remove("active")
|
||||
}
|
||||
clear() {
|
||||
this.ctx_current.clearRect(0, 0, this.width, this.height)
|
||||
}
|
||||
get eraser_active() {
|
||||
return this.getOptionValue("tool") == "erase"
|
||||
}
|
||||
setBrush(layer = null) {
|
||||
if (layer) {
|
||||
layer.ctx.lineCap = "round"
|
||||
layer.ctx.lineJoin = "round"
|
||||
layer.ctx.lineWidth = this.getOptionValue("brush_size")
|
||||
layer.ctx.fillStyle = this.getOptionValue("color")
|
||||
layer.ctx.strokeStyle = this.getOptionValue("color")
|
||||
var sharpness = parseInt(this.getOptionValue("sharpness") * this.getOptionValue("brush_size"))
|
||||
layer.ctx.filter = sharpness == 0 ? `none` : `blur(${sharpness}px)`
|
||||
layer.ctx.globalAlpha = (1 - this.getOptionValue("opacity"))
|
||||
layer.ctx.globalCompositeOperation = this.eraser_active ? "destination-out" : "source-over"
|
||||
}
|
||||
else {
|
||||
Object.values([ "drawing", "overlay" ]).map(name => this.layers[name]).forEach(l => {
|
||||
this.setBrush(l)
|
||||
})
|
||||
}
|
||||
}
|
||||
get ctx_overlay() {
|
||||
return this.layers.overlay.ctx
|
||||
}
|
||||
get ctx_current() { // the idea is this will help support having custom layers and editing each one
|
||||
return this.layers.drawing.ctx
|
||||
}
|
||||
get canvas_current() {
|
||||
return this.layers.drawing.canvas
|
||||
}
|
||||
mouseHandler(event) {
|
||||
var bbox = this.layers.overlay.canvas.getBoundingClientRect()
|
||||
var x = (event.clientX || 0) - bbox.left
|
||||
var y = (event.clientY || 0) - bbox.top
|
||||
var type = event.type;
|
||||
var touchmap = {
|
||||
touchstart: "mousedown",
|
||||
touchmove: "mousemove",
|
||||
touchend: "mouseup",
|
||||
touchcancel: "mouseup"
|
||||
}
|
||||
if (type in touchmap) {
|
||||
type = touchmap[type]
|
||||
if (event.touches && event.touches[0]) {
|
||||
var touch = event.touches[0]
|
||||
var x = (touch.clientX || 0) - bbox.left
|
||||
var y = (touch.clientY || 0) - bbox.top
|
||||
}
|
||||
}
|
||||
|
||||
// do drawing-related stuff
|
||||
if (type == "mousedown" || (type == "mouseenter" && event.buttons == 1)) {
|
||||
if (this.dropper_active) {
|
||||
var img_rgb = this.layers.background.ctx.getImageData(x, y, 1, 1).data
|
||||
var drw_rgb = this.ctx_current.getImageData(x, y, 1, 1).data
|
||||
var drw_opacity = drw_rgb[3] / 255
|
||||
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 (this.drawing) {
|
||||
if (x > 0 && y > 0) {
|
||||
this.ctx_current.lineTo(x, y)
|
||||
this.ctx_overlay.lineTo(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 (this.drawing) {
|
||||
this.drawing = false
|
||||
this.ctx_current.stroke()
|
||||
this.ctx_overlay.clearRect(0, 0, this.width, this.height)
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
getOptionValue(section_name) {
|
||||
var section = IMAGE_EDITOR_SECTIONS.find(s => s.name == section_name)
|
||||
return this.options && section_name in this.options ? this.options[section_name] : section.default
|
||||
}
|
||||
selectOption(section_name, option_index) {
|
||||
var section = IMAGE_EDITOR_SECTIONS.find(s => s.name == section_name)
|
||||
var value = section.options[option_index]
|
||||
this.options[section_name] = value == "custom" ? section.getCustom(this) : value
|
||||
|
||||
this.optionElements[section_name].forEach(element => element.classList.remove("active"))
|
||||
this.optionElements[section_name][option_index].classList.add("active")
|
||||
|
||||
// change the editor
|
||||
this.setBrush()
|
||||
if (section.name == "tool") {
|
||||
this.setCursorIcon()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function rgbToHex(rgb) {
|
||||
function componentToHex(c) {
|
||||
var hex = parseInt(c).toString(16)
|
||||
return hex.length == 1 ? "0" + hex : hex
|
||||
}
|
||||
return "#" + componentToHex(rgb.r) + componentToHex(rgb.g) + componentToHex(rgb.b)
|
||||
}
|
||||
|
||||
const imageEditor = new ImageEditor(document.getElementById("image-editor"))
|
||||
const imageInpainter = new ImageEditor(document.getElementById("image-inpainter"), true)
|
||||
|
||||
imageEditor.setImage(null, 512, 512)
|
||||
imageInpainter.setImage(null, 512, 512)
|
||||
|
||||
document.getElementById("init_image_button_draw").addEventListener("click", () => {
|
||||
document.getElementById("image-editor").classList.toggle("active")
|
||||
})
|
||||
document.getElementById("init_image_button_inpaint").addEventListener("click", () => {
|
||||
document.getElementById("image-inpainter").classList.toggle("active")
|
||||
})
|
@ -1,41 +0,0 @@
|
||||
const INPAINTING_EDITOR_SIZE = 450
|
||||
|
||||
let inpaintingEditorContainer = document.querySelector('#inpaintingEditor')
|
||||
let inpaintingEditor = new DrawingBoard.Board('inpaintingEditor', {
|
||||
color: "#ffffff",
|
||||
background: false,
|
||||
size: 30,
|
||||
webStorage: false,
|
||||
controls: [{'DrawingMode': {'filler': false}}, 'Size', 'Navigation']
|
||||
})
|
||||
let inpaintingEditorCanvasBackground = document.querySelector('.drawing-board-canvas-wrapper')
|
||||
|
||||
function resizeInpaintingEditor(widthValue, heightValue) {
|
||||
if (widthValue === heightValue) {
|
||||
widthValue = INPAINTING_EDITOR_SIZE
|
||||
heightValue = INPAINTING_EDITOR_SIZE
|
||||
} else if (widthValue > heightValue) {
|
||||
heightValue = (heightValue / widthValue) * INPAINTING_EDITOR_SIZE
|
||||
widthValue = INPAINTING_EDITOR_SIZE
|
||||
} else {
|
||||
widthValue = (widthValue / heightValue) * INPAINTING_EDITOR_SIZE
|
||||
heightValue = INPAINTING_EDITOR_SIZE
|
||||
}
|
||||
if (inpaintingEditor.opts.aspectRatio === (widthValue / heightValue).toFixed(3)) {
|
||||
// Same ratio, don't reset the canvas.
|
||||
return
|
||||
}
|
||||
inpaintingEditor.opts.aspectRatio = (widthValue / heightValue).toFixed(3)
|
||||
|
||||
inpaintingEditorContainer.style.width = widthValue + 'px'
|
||||
inpaintingEditorContainer.style.height = heightValue + 'px'
|
||||
inpaintingEditor.opts.enlargeYourContainer = true
|
||||
|
||||
inpaintingEditor.opts.size = inpaintingEditor.ctx.lineWidth
|
||||
inpaintingEditor.resize()
|
||||
|
||||
inpaintingEditor.ctx.lineCap = "round"
|
||||
inpaintingEditor.ctx.lineJoin = "round"
|
||||
inpaintingEditor.ctx.lineWidth = inpaintingEditor.opts.size
|
||||
inpaintingEditor.setColor(inpaintingEditor.opts.color)
|
||||
}
|
@ -59,14 +59,6 @@ let serverStatusColor = document.querySelector('#server-status-color')
|
||||
let serverStatusMsg = document.querySelector('#server-status-msg')
|
||||
|
||||
|
||||
document.querySelector('.drawing-board-control-navigation-back').innerHTML = '<i class="fa-solid fa-rotate-left"></i>'
|
||||
document.querySelector('.drawing-board-control-navigation-forward').innerHTML = '<i class="fa-solid fa-rotate-right"></i>'
|
||||
|
||||
let maskResetButton = document.querySelector('.drawing-board-control-navigation-reset')
|
||||
maskResetButton.innerHTML = 'Clear'
|
||||
maskResetButton.style.fontWeight = 'normal'
|
||||
maskResetButton.style.fontSize = '10pt'
|
||||
|
||||
let serverState = {'status': 'Offline', 'time': Date.now()}
|
||||
let bellPending = false
|
||||
|
||||
@ -335,11 +327,7 @@ function onUseAsInputClick(req, img) {
|
||||
initImageSelector.value = null
|
||||
initImagePreview.src = imgData
|
||||
|
||||
initImagePreviewContainer.style.display = 'block'
|
||||
inpaintingEditorContainer.style.display = 'none'
|
||||
promptStrengthContainer.style.display = 'table-row'
|
||||
maskSetting.checked = false
|
||||
samplerSelectionContainer.style.display = 'none'
|
||||
}
|
||||
|
||||
function onDownloadImageClick(req, img) {
|
||||
@ -805,7 +793,7 @@ function getCurrentUserRequest() {
|
||||
// newTask.reqBody.mask = maskImagePreview.src
|
||||
// }
|
||||
if (maskSetting.checked) {
|
||||
newTask.reqBody.mask = inpaintingEditor.getImg()
|
||||
newTask.reqBody.mask = imageInpainter.getImg()
|
||||
}
|
||||
newTask.reqBody.sampler = 'ddim'
|
||||
} else {
|
||||
@ -1092,13 +1080,14 @@ numOutputsTotalField.addEventListener('change', renameMakeImageButton)
|
||||
numOutputsParallelField.addEventListener('change', renameMakeImageButton)
|
||||
|
||||
function onDimensionChange() {
|
||||
if (!maskSetting.checked) {
|
||||
return
|
||||
}
|
||||
let widthValue = parseInt(widthField.value)
|
||||
let heightValue = parseInt(heightField.value)
|
||||
|
||||
resizeInpaintingEditor(widthValue, heightValue)
|
||||
if (!initImagePreviewContainer.classList.contains("has-image")) {
|
||||
imageEditor.setImage(null, widthValue, heightValue)
|
||||
}
|
||||
else {
|
||||
imageInpainter.setImage(initImagePreview.src, widthValue, heightValue)
|
||||
}
|
||||
}
|
||||
|
||||
diskPathField.disabled = !saveToDiskField.checked
|
||||
@ -1221,10 +1210,6 @@ checkRandomSeed()
|
||||
|
||||
function showInitImagePreview() {
|
||||
if (initImageSelector.files.length === 0) {
|
||||
initImagePreviewContainer.style.display = 'none'
|
||||
// inpaintingEditorContainer.style.display = 'none'
|
||||
promptStrengthContainer.style.display = 'none'
|
||||
// maskSetting.style.display = 'none'
|
||||
return
|
||||
}
|
||||
|
||||
@ -1232,13 +1217,7 @@ function showInitImagePreview() {
|
||||
let file = initImageSelector.files[0]
|
||||
|
||||
reader.addEventListener('load', function(event) {
|
||||
// console.log(file.name, reader.result)
|
||||
initImagePreview.src = reader.result
|
||||
initImagePreviewContainer.style.display = 'block'
|
||||
inpaintingEditorContainer.style.display = 'none'
|
||||
promptStrengthContainer.style.display = 'table-row'
|
||||
samplerSelectionContainer.style.display = 'none'
|
||||
// maskSetting.checked = false
|
||||
})
|
||||
|
||||
if (file) {
|
||||
@ -1249,34 +1228,25 @@ initImageSelector.addEventListener('change', showInitImagePreview)
|
||||
showInitImagePreview()
|
||||
|
||||
initImagePreview.addEventListener('load', function() {
|
||||
inpaintingEditorCanvasBackground.style.backgroundImage = "url('" + this.src + "')"
|
||||
// maskSetting.style.display = 'block'
|
||||
// inpaintingEditorContainer.style.display = 'block'
|
||||
promptStrengthContainer.style.display = 'table-row'
|
||||
initImagePreviewContainer.classList.add("has-image")
|
||||
|
||||
initImageSizeBox.textContent = initImagePreview.naturalWidth + " x " + initImagePreview.naturalHeight
|
||||
initImageSizeBox.style.display = 'block'
|
||||
imageEditor.setImage(this.src, initImagePreview.naturalWidth, initImagePreview.naturalHeight)
|
||||
imageInpainter.setImage(this.src, parseInt(widthField.value), parseInt(heightField.value))
|
||||
})
|
||||
|
||||
initImageClearBtn.addEventListener('click', function() {
|
||||
initImageSelector.value = null
|
||||
// maskImageSelector.value = null
|
||||
|
||||
initImagePreview.src = ''
|
||||
// maskImagePreview.src = ''
|
||||
maskSetting.checked = false
|
||||
|
||||
initImagePreviewContainer.style.display = 'none'
|
||||
// inpaintingEditorContainer.style.display = 'none'
|
||||
// maskImagePreviewContainer.style.display = 'none'
|
||||
|
||||
// maskSetting.style.display = 'none'
|
||||
|
||||
promptStrengthContainer.style.display = 'none'
|
||||
samplerSelectionContainer.style.display = 'table-row'
|
||||
initImageSizeBox.style.display = 'none'
|
||||
initImagePreviewContainer.classList.remove("has-image")
|
||||
imageEditor.setImage(null, parseInt(widthField.value), parseInt(heightField.value))
|
||||
})
|
||||
|
||||
maskSetting.addEventListener('click', function() {
|
||||
inpaintingEditorContainer.style.display = (this.checked ? 'block' : 'none')
|
||||
onDimensionChange()
|
||||
})
|
||||
|
||||
@ -1316,9 +1286,22 @@ document.querySelectorAll('.popup').forEach(popup => {
|
||||
}
|
||||
})
|
||||
|
||||
var tabElements = [];
|
||||
var tabElements = []
|
||||
function selectTab(tab_id) {
|
||||
let tabInfo = tabElements.find(t => t.tab.id == tab_id)
|
||||
if (!tabInfo.tab.classList.contains("active")) {
|
||||
tabElements.forEach(info => {
|
||||
if (info.tab.classList.contains("active")) {
|
||||
info.tab.classList.toggle("active")
|
||||
info.content.classList.toggle("active")
|
||||
}
|
||||
})
|
||||
tabInfo.tab.classList.toggle("active")
|
||||
tabInfo.content.classList.toggle("active")
|
||||
}
|
||||
}
|
||||
function linkTabContents(tab) {
|
||||
var name = tab.id.replace("tab-", "");
|
||||
var name = tab.id.replace("tab-", "")
|
||||
var content = document.getElementById(`tab-content-${name}`)
|
||||
tabElements.push({
|
||||
name: name,
|
||||
@ -1326,18 +1309,7 @@ function linkTabContents(tab) {
|
||||
content: content
|
||||
})
|
||||
|
||||
tab.addEventListener("click", event => {
|
||||
if (!tab.classList.contains("active")) {
|
||||
tabElements.forEach(tabInfo => {
|
||||
if (tabInfo.tab.classList.contains("active")) {
|
||||
tabInfo.tab.classList.toggle("active")
|
||||
tabInfo.content.classList.toggle("active")
|
||||
}
|
||||
})
|
||||
tab.classList.toggle("active")
|
||||
content.classList.toggle("active")
|
||||
}
|
||||
})
|
||||
tab.addEventListener("click", event => selectTab(tab.id))
|
||||
}
|
||||
|
||||
document.querySelectorAll(".tab").forEach(linkTabContents)
|
||||
|
Loading…
Reference in New Issue
Block a user