diff --git a/README.md b/README.md index cf0a451..7fb61dc 100644 --- a/README.md +++ b/README.md @@ -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 + + + - 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)` - + ## Contributing diff --git a/aspect_ratio_helper/_constants.py b/aspect_ratio_helper/_constants.py index 4760a21..58c14d0 100644 --- a/aspect_ratio_helper/_constants.py +++ b/aspect_ratio_helper/_constants.py @@ -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' diff --git a/aspect_ratio_helper/_settings.py b/aspect_ratio_helper/_settings.py index 7113b50..5caaa24 100644 --- a/aspect_ratio_helper/_settings.py +++ b/aspect_ratio_helper/_settings.py @@ -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, ), ) diff --git a/docs/options.png b/docs/options.png deleted file mode 100644 index c0b4d0e..0000000 Binary files a/docs/options.png and /dev/null differ diff --git a/docs/opts.png b/docs/opts.png new file mode 100644 index 0000000..7603791 Binary files /dev/null and b/docs/opts.png differ diff --git a/javascript/aspectRatioController.js b/javascript/aspectRatioController.js new file mode 100644 index 0000000..9c55a8a --- /dev/null +++ b/javascript/aspectRatioController.js @@ -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 = ` +