Implement JavaScript aspect ratios (#25)

* Implement JavaScript aspect ratios

- Add options to enable and specify JavaScript aspect ratios
- Add JavaScript to load when enabled, which adds dropdown & functionality
- Update docs for JavaScript aspect ratios
- Add video to README.md for JavaScript aspect ratios
- Update options image on README.md
This commit is contained in:
thomas
2023-03-24 00:58:00 +00:00
committed by GitHub
parent d602ddf3bf
commit be365af8f9
7 changed files with 332 additions and 5 deletions

View File

@@ -6,6 +6,18 @@ Install via the extensions tab on the [AUTOMATIC1111 webui](https://github.com/A
## Features
- JavaScript aspect ratio controls
- Adds a dropdown of configurable aspect ratios, to which the dimensions will auto-scale
- When selected, you will only be able to modify the higher dimension
- The smaller or equivalent dimension will scale accordingly
- If "Lock/🔒" is selected, the aspect ratio of the current dimensions will be kept
- If you click the "Swap/⇅" button, the current dimensions will swap
- Configurable aspect ratios will also flip, reducing the need for duplication of config
https://user-images.githubusercontent.com/22506439/227396634-7a63671a-fd38-419a-b734-a3d26647cc1d.mp4
</br>
- Scale to maximum dimension
- Upon clicking, the width and height will scale according to the configured maximum value
- Aspect ratio will be retained, the smaller or equivalent dimension will be scaled to match
@@ -29,6 +41,9 @@ Install via the extensions tab on the [AUTOMATIC1111 webui](https://github.com/A
- Determines whether the 'Aspect Ratio Helper' accordion expands by default
- UI Component order (`MaxDimensionScaler, PredefinedAspectRatioButtons, PredefinedPercentageButtons`)
- Determines the order in which the UI components will render
- Enable JavaScript aspect ratio controls
- JavaScript aspect ratio buttons `(Off, 🔓, 1:1, 4:3, 16:9, 9:16, 21:9)`
- i.e `Off, 🔓, 1:1, 4:3, 16:9, 9:16, 21:9`, `Off, 🔓, 9:2, 1:3`
- Show maximum dimension button (`True`)
- Maximum dimension default (`1024`)
- Show pre-defined aspect ratio buttons (`True`)
@@ -43,7 +58,7 @@ Install via the extensions tab on the [AUTOMATIC1111 webui](https://github.com/A
- `Raw percentage (50%, 150%)`
- `Multiplication (x0.5, x1.5)`
![settings.png](docs%2Foptions.png)
![settings.png](docs%2Fopts.png)
## Contributing

View File

@@ -5,6 +5,8 @@ MIN_DIMENSION = 64
ARH_EXPAND_BY_DEFAULT_KEY = 'arh_expand_by_default'
ARH_UI_COMPONENT_ORDER_KEY = 'arh_ui_component_order_key'
ARH_JAVASCRIPT_ASPECT_RATIO_SHOW_KEY = 'arh_javascript_aspect_ratio_show'
ARH_JAVASCRIPT_ASPECT_RATIOS_KEY = 'arh_javascript_aspect_ratio'
ARH_SHOW_MAX_WIDTH_OR_HEIGHT_KEY = 'arh_show_max_width_or_height'
ARH_MAX_WIDTH_OR_HEIGHT_KEY = 'arh_max_width_or_height'
ARH_SHOW_PREDEFINED_PERCENTAGES_KEY = 'arh_show_predefined_percentages'

View File

@@ -27,6 +27,10 @@ OPT_KEY_TO_DEFAULT_MAP = {
_constants.ARH_EXPAND_BY_DEFAULT_KEY: False,
_constants.ARH_UI_COMPONENT_ORDER_KEY:
DEFAULT_UI_COMPONENT_ORDER_KEY,
_constants.ARH_JAVASCRIPT_ASPECT_RATIO_SHOW_KEY: False,
_constants.ARH_JAVASCRIPT_ASPECT_RATIOS_KEY:
'Off, 🔓, 1:1, 3:2, 4:3, 5:4, 16:9, 1.85:1, 2.35:1, 2.39:1, 2.40:1, '
'21:9, 1.375:1, 1.66:1, 1.75:1',
_constants.ARH_SHOW_MAX_WIDTH_OR_HEIGHT_KEY: True,
_constants.ARH_MAX_WIDTH_OR_HEIGHT_KEY:
_constants.MAX_DIMENSION / 2,
@@ -83,8 +87,6 @@ def sort_components_by_keys(
def on_ui_settings():
section = 'aspect_ratio_helper', _constants.EXTENSION_NAME
# default ui options
shared.opts.add_option(
key=_constants.ARH_EXPAND_BY_DEFAULT_KEY,
@@ -93,7 +95,7 @@ def on_ui_settings():
_constants.ARH_EXPAND_BY_DEFAULT_KEY,
),
label='Expand by default',
section=section,
section=_constants.SECTION,
),
)
shared.opts.add_option(
@@ -111,7 +113,28 @@ def on_ui_settings():
)
],
},
section=section,
section=_constants.SECTION,
),
)
shared.opts.add_option(
key=_constants.ARH_JAVASCRIPT_ASPECT_RATIO_SHOW_KEY,
info=shared.OptionInfo(
default=OPT_KEY_TO_DEFAULT_MAP.get(
_constants.ARH_JAVASCRIPT_ASPECT_RATIO_SHOW_KEY,
),
label='Enable JavaScript aspect ratio controls',
section=_constants.SECTION,
),
)
shared.opts.add_option(
key=_constants.ARH_JAVASCRIPT_ASPECT_RATIOS_KEY,
info=shared.OptionInfo(
default=OPT_KEY_TO_DEFAULT_MAP.get(
_constants.ARH_JAVASCRIPT_ASPECT_RATIOS_KEY,
),
label='JavaScript aspect ratio buttons'
' (Off, 🔓, 1:1, 4:3, 16:9, 9:16, 21:9)',
section=_constants.SECTION,
),
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

BIN
docs/opts.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@@ -0,0 +1,260 @@
// https://github.com/Gerschel/stable-diffusion-webui/blob/742d86eed4d07eef7db65b3d943f85bdbafc26e4/javascript/ComponentControllers.js#L176
class ContainerController {
constructor(element) {
this.element = element;
this.num = this.element.querySelector('input[type=number]');
this.range = this.element.querySelector('input[type=range]');
}
getVal() {
return this.num.value;
}
disable() {
this.num.setAttribute('disabled', true);
this.range.setAttribute('disabled', true);
}
enable() {
this.num.removeAttribute('disabled');
this.range.removeAttribute('disabled');
}
updateVal(text) {
this.num.value = text;
this.range.value = text;
}
updateMin(text) {
this.num.min = text;
this.range.min = text;
}
eventHandler() {
this.element.dispatchEvent(
new Event("input")
);
this.num.dispatchEvent(
new Event("input")
);
this.range.dispatchEvent(
new Event("input")
);
}
setVal(text) {
this.updateVal(text);
this.eventHandler();
}
}
function _reverseAspectRatio(ar) {
if (['Off', '🔓'].includes(ar)) return;
const [width, height] = ar.split(":");
return `${height}:${width}`;
}
class AspectRatioController {
constructor(widthContainer, heightContainer, aspectRatio = "Off") {
this.widthContainer = new ContainerController(widthContainer);
this.heightContainer = new ContainerController(heightContainer);
this.max = 2048;
this.min = 64;
this.dimensions = {
widthInput: this.widthContainer.num,
widthRange: this.widthContainer.range,
heightInput: this.heightContainer.num,
heightRange: this.heightContainer.range,
};
Object.values(this.dimensions).forEach(dimension => {
dimension.step = 1;
dimension.addEventListener('change', (e) => {
e.preventDefault()
this._syncValues(dimension);
});
})
this.setAspectRatio(aspectRatio);
}
setAspectRatio(aspectRatio) {
this.aspectRatio = aspectRatio;
if (aspectRatio === "Off") {
this.widthContainer.enable();
this.heightContainer.enable();
this.widthContainer.updateMin(this.min);
this.heightContainer.updateMin(this.min);
return;
}
const lockedSetting = [
this.widthContainer.getVal(),
this.heightContainer.getVal(),
];
const [widthRatio, heightRatio] = this._clampToBoundaries(
...(
aspectRatio !== '🔓'
? aspectRatio.split(':')
: lockedSetting
).map(Number)
)
this.widthRatio = widthRatio;
this.heightRatio = heightRatio;
if (widthRatio >= heightRatio) {
this.heightContainer.disable();
this.widthContainer.enable();
const minimum = Math.max(
Math.round(this.min * widthRatio / heightRatio), this.min
);
this.widthContainer.updateMin(minimum);
this.heightContainer.updateMin(this.min);
} else {
this.widthContainer.disable();
this.heightContainer.enable();
const minimum = Math.max(
Math.round(this.min * heightRatio / widthRatio), this.min
);
this.heightContainer.updateMin(minimum);
this.widthContainer.updateMin(this.min);
}
this._syncValues();
}
_syncValues(changedElement) {
if (this.aspectRatio === "Off") return;
if (!changedElement) {
changedElement = {
value: Math.max(
...Object.values(this.dimensions).map(x => x.value)
)
}
}
const aspectRatio = this.widthRatio / this.heightRatio;
let w, h;
if (this.widthRatio >= this.heightRatio) {
w = Math.round(changedElement.value);
h = Math.round(changedElement.value / aspectRatio);
} else {
h = Math.round(changedElement.value);
w = Math.round(changedElement.value * aspectRatio);
}
const [width, height] = this._clampToBoundaries(w, h)
this.widthContainer.setVal(width);
this.heightContainer.setVal(height);
}
_clampToBoundaries(width, height) {
const aspectRatio = width / height;
const MAX_DIMENSION = this.max;
const MIN_DIMENSION = this.min;
if (width > MAX_DIMENSION) {
width = MAX_DIMENSION;
height = Math.round(width / aspectRatio);
}
if (height > MAX_DIMENSION) {
height = MAX_DIMENSION;
width = Math.round(height * aspectRatio);
}
if (width < MIN_DIMENSION) {
width = MIN_DIMENSION;
height = Math.round(width / aspectRatio);
}
if (height < MIN_DIMENSION) {
height = MIN_DIMENSION;
width = Math.round(height * aspectRatio);
}
if (width < MIN_DIMENSION) {
width = MIN_DIMENSION;
} else if (width > MAX_DIMENSION) {
width = MAX_DIMENSION;
}
if (height < MIN_DIMENSION) {
height = MIN_DIMENSION;
} else if (height > MAX_DIMENSION) {
height = MAX_DIMENSION;
}
return [width, height]
}
static observeStartup(page, key) {
let observer = new MutationObserver(() => {
const widthContainer = gradioApp().querySelector(`#${page}_width`);
const heightContainer = gradioApp().querySelector(`#${page}_height`);
if (widthContainer && heightContainer) {
observer.disconnect();
if (!window.opts.arh_javascript_aspect_ratio_show) return;
const switchBtn = gradioApp().getElementById(page + '_res_switch_btn');
if (!switchBtn) return;
const wrapperDiv = document.createElement('div');
wrapperDiv.setAttribute("id", `${page}_size_toolbox`);
wrapperDiv.setAttribute("class", "flex flex-col relative col gap-4");
wrapperDiv.setAttribute("style", "min-width: min(320px, 100%); flex-grow: 0");
wrapperDiv.innerHTML = `
<div id="${page}_ratio" class="gr-block gr-box relative w-full border-solid border border-gray-200 gr-padded">
<select id="${page}_select_aspect_ratio" class="gr-box gr-input w-full disabled:cursor-not-allowed">
${
window.opts.arh_javascript_aspect_ratio.split(',').map(r => {
return '<option class="ar-option">' + r.trim() + '</option>'
}).join('\n')
}
</select>
</div>
`;
const parent = switchBtn.parentNode;
parent.removeChild(switchBtn);
wrapperDiv.appendChild(switchBtn);
parent.insertBefore(wrapperDiv, parent.lastChild.previousElementSibling);
const controller = new AspectRatioController(widthContainer, heightContainer);
const aspectRatioSelect = gradioApp().getElementById(`${page}_select_aspect_ratio`);
aspectRatioSelect.onchange = () => {
const options = Array.from(aspectRatioSelect);
const picked = options[aspectRatioSelect.selectedIndex].value;
controller.setAspectRatio(picked);
};
switchBtn.onclick = () => {
Array.from(gradioApp().querySelectorAll('.ar-option')).forEach(el => {
const reversed = _reverseAspectRatio(el.value);
if (reversed) {
el.value = reversed;
el.textContent = reversed;
}
});
const options = Array.from(aspectRatioSelect);
let picked = options[aspectRatioSelect.selectedIndex].value;
if (picked === '🔓') {
picked = `${controller.heightRatio}:${controller.widthRatio}`
}
controller.setAspectRatio(picked);
};
window[key] = controller;
}
});
observer.observe(gradioApp(), {childList: true, subtree: true});
}
}
document.addEventListener("DOMContentLoaded", () => {
window.__txt2imgAspectRatioController = AspectRatioController.observeStartup(
"txt2img", "__txt2imgAspectRatioController"
);
window.__img2imgAspectRatioController = AspectRatioController.observeStartup(
"img2img", "__img2imgAspectRatioController"
);
});

27
style.css Normal file
View File

@@ -0,0 +1,27 @@
#txt2img_size_toolbox, #img2img_size_toolbox{
min-width: unset !important;
gap: 0;
}
#txt2img_ratio, #img2img_ratio {
padding: 0px;
min-width: unset;
max-width: fit-content;
}
#txt2img_ratio select, #img2img_ratio select {
-o-appearance: none;
-ms-appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-image: unset;
padding-right: unset;
min-width: 40px;
max-width: 40px;
min-height: 40px;
max-height: 40px;
line-height: 40px;
padding: 0;
text-align: center;
}