Merge branch 'main' into workspaces

Signed-off-by: Victor Seiji Hariki <victorseijih@gmail.com>
This commit is contained in:
Victor Seiji Hariki 2023-01-15 01:30:49 -03:00
commit ff687c5f64
23 changed files with 1408 additions and 637 deletions

View file

@ -1,37 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ""
labels: bug
assignees: zero01101, seijihariki
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Environment (please complete the following information):**
- OS and Version: [e.g. iOS 15, win10x64 v22h2, palmOS 3]
- GPU: [e.g. GTX1660, RTX3090, RX750, Voodoo2]
- Browser and Version: [e.g. chrome 108, safari 16.1]
- Any browser extensions: [rationale: [microsoft editor chrome addon hates overmask](https://github.com/zero01101/openOutpaint/discussions/88#discussioncomment-4498341)]
- Commit of openOutpaint: [e.g. 3d29847, https://github.com/zero01101/openOutpaint/commit/b9fcc7ad00be46f05dd6b54cfac7c05904c84f87]
- Commit of A1111 webUI: [e.g. c5bdba2, https://github.com/AUTOMATIC1111/stable-diffusion-webui/commit/5f1dfbbc959855fd90ba80c0c76301d2063772fa]
- How you interact with A1111 webUI: [e.g. local hardware, colab, cloud host like linode]
**Additional context**
Add any other context about the problem here.

91
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View file

@ -0,0 +1,91 @@
name: Bug Report
description: You think somethings is broken in the UI
title: "[Bug]: "
labels: ["bug"]
assignees: zero01101, seijihariki
body:
- type: markdown
attributes:
value: |
*Please complete this form with as much detailed information as possible.*
- type: textarea
id: what-did
attributes:
label: What happened?
description: What happened that you weren't expecting, or what happened incorrectly?
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to reproduce the problem
description: Please provide us with precise step-by-step information on how to reproduce the issue
value: |
1. Go to ....
2. Press ....
3. ... [etc]
validations:
required: true
- type: textarea
id: what-should
attributes:
label: What should have happened?
description: Describe what you believe should have ocurred instead of what actually happened.
validations:
required: true
- type: input
id: commit
attributes:
label: Commit where the problem happens
description: Which commit are you running? (i.e. https://github.com/zero01101/openOutpaint/commit/bf21c19ae352800d9e1b37bb490e817b6848e533, bf21c19)
validations:
required: true
- type: dropdown
id: platforms
attributes:
label: What platforms do you use to access openOutpaint?
multiple: true
options:
- Windows
- Linux
- MacOS
- iOS
- Android
- Other/Cloud
validations:
required: true
- type: dropdown
id: browsers
attributes:
label: What browsers do you use to access the UI ?
multiple: true
options:
- Mozilla Firefox
- Google Chrome
- Brave
- Apple Safari
- Microsoft Edge
- Opera
- Other (please list in additional information)
validations:
required: true
- type: textarea
id: browser-extensions
attributes:
label: Browser Extensions/Addons
description: Please list all browser extensions/addons you have running. Some have been known to cause issues with openOutpaint.
validations:
required: true
- type: textarea
id: webui-commandline
attributes:
label: AUTOMATIC1111 webUI Commandline Arguments
description: Please list all used commandline arguments passed to A1111 webUI (i.e. `--api`).
validations:
required: true
- type: textarea
id: misc
attributes:
label: Additional information
description: Please provide us with any relevant additional information, context, screenshots, etc.

View file

@ -1,19 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ""
labels: enhancement
assignees: zero01101, seijihariki
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View file

@ -0,0 +1,48 @@
name: Feature request
description: Suggest an idea for this project
title: "[Feature Request]: "
labels: ["enhancement"]
assignees: zero01101, seijihariki
body:
- type: markdown
attributes:
value: |
*Please complete this form with as much detailed information as possible.*
- type: textarea
id: related
attributes:
label: Is your feature request related to a problem? Please describe.
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
validations:
required: true
- type: textarea
id: feature
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen, preferably with example use-case scenario.
validations:
required: true
- type: textarea
id: workflow
attributes:
label: Proposed workflow
description: Please provide us with step by step information on how you'd like the feature to be accessed and used
value: |
1. Go to ....
2. Press ....
3. ...
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: true
- type: textarea
id: misc
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request here.

View file

@ -29,16 +29,16 @@ this is a completely vanilla javascript and html canvas outpainting convenience
- optional (visibly) inverted mask mode - red masks get mutated, blue masks stay the same, but you can't take both pills at once
- inpainting color brush to bring out your inner vincent van bob ross
- dedicated img2img tool with optional border masking for enhanced output coherence with existing subject matter
- marquee select tool to select regions and arbitrarily scale, create stamps, move chunks, peek at lower layers, do all sorts of damage
- marquee select tool to select regions and arbitrarily scale, rotate, create stamps, move chunks, peek at lower layers, do all sorts of damage
- optionally decoupled cursor size and output resolution
- interrogate tool
- floating control panel to easily change models/samplers/steps/prompts/CFG/etc options for each dream summoned from the latent void _(NOTE: model switching requires A1111 webUI to be on commit [5a6387e](https://github.com/AUTOMATIC1111/stable-diffusion-webui/commit/5a6387e189dc365c47a7979b9040d5b6fdd7ba43) or more recent)_
- floating toolbox with handy keyboard shortcuts
- optional grid snapping for precision
- optional hi-res fix for blank/txt2img dreams
- **_NOTE: as of v0.0.12.5/webUI commit [ef27a18](https://github.com/AUTOMATIC1111/stable-diffusion-webui/commit/ef27a18b6b7cb1a8eebdc9b2e88d25baf2c2414d), HRfix has been COMPLETELY reworked and no longer works even remotely the same and webUI would actively use the newly "invalid" firstpass sizes as the_ desired output resolution _after yelling about them being invalid, thus openOutpaint's implementation is no longer compatible with versions of A1111 predating that. You will be alerted to the outdated webUI and the HRfix option will become disabled in this event._**
- **_NOTE: as of v0.0.12.5/webUI commit [ef27a18](https://github.com/AUTOMATIC1111/stable-diffusion-webui/commit/ef27a18b6b7cb1a8eebdc9b2e88d25baf2c2414d), HRfix has been COMPLETELY reworked and no longer works remotely the same, thus openOutpaint's implementation is no longer compatible with versions of A1111 predating that. You will be alerted to the outdated webUI and the HRfix option will become limited to simply using [reticle dimensions / 2] in this event. Please see the [manual entry](https://github.com/zero01101/openOutpaint/wiki/Manual#hrfix) regarding HRfix and its available options._**
- optional overmasking for potentially better seams between outpaints - set overmask px value to 0 to disable the feature
- import arbitrary images and scale/stamp on the canvas whenever, wherever you'd like
- import arbitrary images and rotate/scale/stamp on the canvas whenever, wherever you'd like
- upscaler support for final output images
- saves your preferences/imported images to browser localstorage for maximum convenience
- reset to defaults button to unsave your preferences if things go squirrely
@ -48,6 +48,8 @@ this is a completely vanilla javascript and html canvas outpainting convenience
## operation
**_NOTE: [PLEASE SEE DOCUMENTATION REGARDING NEW HRfix FEATURES](https://github.com/zero01101/openOutpaint/wiki/Manual#hrfix) IMPLEMENTED AS OF webUI COMMIT [ef27a18](https://github.com/AUTOMATIC1111/stable-diffusion-webui/commit/ef27a18b6b7cb1a8eebdc9b2e88d25baf2c2414d)_**
### prerequisities
you'll obviously need A1111's webUI installed before you can use this, thus you're presumed to have an operational python install up and running to boot.

View file

@ -17,7 +17,9 @@
bottom: 15%;
mask-size: contain;
-webkit-mask-size: contain;
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
max-height: 70%;
aspect-ratio: 1;

View file

@ -644,3 +644,9 @@ select > .style-select-option:active {
.button.tool.active {
background-color: rgb(60, 60, 130);
}
/* Miscellaneous garbage */
.thirdwidth {
max-width: 33%;
}

View file

@ -25,6 +25,7 @@
.layer-render-target .collection {
position: absolute;
transform-origin: 0px 0px;
}
.layer-render-target .collection > .collection-input-overlay {
@ -45,7 +46,7 @@
right: 0;
}
#layer-overlay {
.overlay-canvas {
position: fixed;
top: 0;

View file

@ -231,6 +231,7 @@ div.autocomplete > .autocomplete-list {
}
div.autocomplete > .autocomplete-list > .autocomplete-option {
position: relative;
cursor: pointer;
overflow: hidden;

View file

@ -158,7 +158,9 @@
background-color: #293d3d77;
-webkit-mask-image: url("../../res/icons/chevron-up.svg");
mask-image: url("../../res/icons/chevron-up.svg");
-webkit-mask-size: contain;
mask-size: contain;
width: 60px;

View file

@ -5,15 +5,15 @@
<title>openOutpaint 🐠</title>
<!-- CSS Variables -->
<link href="css/colors.css?v=3f81e80" rel="stylesheet" />
<link href="css/icons.css?v=caa702e" rel="stylesheet" />
<link href="css/icons.css?v=9ae0466" rel="stylesheet" />
<link href="css/index.css?v=69d3b9e" rel="stylesheet" />
<link href="css/layers.css?v=b4fbf61" rel="stylesheet" />
<link href="css/index.css?v=5b8d4d6" rel="stylesheet" />
<link href="css/layers.css?v=92c0352" rel="stylesheet" />
<link href="css/ui/generic.css?v=4b9afe2" rel="stylesheet" />
<link href="css/ui/generic.css?v=802bd41" rel="stylesheet" />
<link href="css/ui/history.css?v=0b03861" rel="stylesheet" />
<link href="css/ui/layers.css?v=4fd95fe" rel="stylesheet" />
<link href="css/ui/layers.css?v=ae472cd" rel="stylesheet" />
<link href="css/ui/toolbar.css?v=109c78f" rel="stylesheet" />
<!-- Tool Specific CSS -->
@ -49,6 +49,7 @@
</div>
</div>
</label>
<!-- Prompts section -->
<button type="button" class="collapsible">Prompts</button>
<div class="content prompt">
@ -98,10 +99,20 @@
step="1" />
<br />
<input type="checkbox" id="cbxHRFix" onchange="changeHiResFix()" />
<label for="cbxHRFix">Apply txt2img HRfix</label>
<label for="cbxHRFix">Apply Txt2Img HRfix</label>
<br />
<input
type="checkbox"
id="cbxHRFSquare"
onchange="changeHiResSquare()"
class="hrfix" />
<label for="cbxHRFSquare" class="hrfix">
Square Firstpass Aspect
</label>
<br class="hrfix" />
<div id="hrFixScale" class="hrfix"></div>
<div id="hrFixLockPx" class="hrfix"></div>
<div id="hrFixSteps" class="hrfix"></div>
<label id="hrFixLabel" class="hrfix">Choose HRfix upscaler</label>
<div id="hrFixUpscaler" class="hrfix"></div>
<div id="hrDenoising" class="hrfix"></div>
@ -191,7 +202,13 @@
<br />
<span id="version">
<a href="https://github.com/zero01101/openOutpaint" target="_blank">
Alpha release v0.0.12.5
Alpha release v0.0.13
</a>
<br />
<a
href="https://github.com/zero01101/openOutpaint/wiki/Manual"
target="_blank">
User Manual
</a>
</span>
<br />
@ -303,7 +320,8 @@
<div id="layer-render" class="layer-render-target"></div>
<!-- Overlay -->
<canvas id="layer-overlay" class="layer-overlay"></canvas>
<canvas id="layer-overlay" class="overlay-canvas"></canvas>
<canvas id="layer-debug-overlay" class="overlay-canvas"></canvas>
<!-- Page Overlay -->
<div id="page-overlay-wrapper" class="page-overlay invisible">
@ -315,7 +333,7 @@
<div class="ui separator"></div>
<iframe
id="page-overlay"
src="pages/configuration.html?v=3d710ce"></iframe>
src="pages/configuration.html?v=973baf2"></iframe>
</div>
</div>
@ -324,26 +342,28 @@
<script src="js/defaults.js?v=5b06818" type="text/javascript"></script>
<!-- Base Libs -->
<script src="js/lib/util.js?v=7f6847c" type="text/javascript"></script>
<script src="js/lib/util.js?v=49a78a6" type="text/javascript"></script>
<script src="js/lib/events.js?v=2ab7933" type="text/javascript"></script>
<script src="js/lib/workspaces.js?v=4fbd55b" type="text/javascript"></script>
<script
src="js/lib/workspaces.js?v=4fbd55b"
type="text/javascript"></script>
<script src="js/lib/input.js?v=09298ac" type="text/javascript"></script>
<script src="js/lib/layers.js?v=a1f8aea" type="text/javascript"></script>
<script src="js/lib/commands.js?v=bf23c83" type="text/javascript"></script>
<script src="js/lib/toolbar.js?v=ca3fccf" type="text/javascript"></script>
<script src="js/lib/toolbar.js?v=306d637" type="text/javascript"></script>
<script src="js/lib/ui.js?v=76ede2b" type="text/javascript"></script>
<script
src="js/initalize/layers.populate.js?v=c81f0a5"
src="js/initalize/layers.populate.js?v=39785ac"
type="text/javascript"></script>
<!-- Configuration -->
<script src="js/config.js?v=8da6a43" type="text/javascript"></script>
<script src="js/config.js?v=e0345e0" type="text/javascript"></script>
<!-- Content -->
<script src="js/prompt.js?v=7a1c68c" type="text/javascript"></script>
<script src="js/index.js?v=a0ae6b5" type="text/javascript"></script>
<script src="js/index.js?v=9d20cb0" type="text/javascript"></script>
<script
src="js/ui/floating/history.js?v=fc92d14"
@ -354,20 +374,20 @@
<!-- Load Tools -->
<script
src="js/ui/tool/generic.js?v=2bcd36d"
src="js/ui/tool/generic.js?v=3e678e0"
type="text/javascript"></script>
<script src="js/ui/tool/dream.js?v=1f10ae6" type="text/javascript"></script>
<script src="js/ui/tool/dream.js?v=eb98dc9" type="text/javascript"></script>
<script
src="js/ui/tool/maskbrush.js?v=1e8a893"
type="text/javascript"></script>
<script
src="js/ui/tool/colorbrush.js?v=8acb4f6"
src="js/ui/tool/colorbrush.js?v=3f8c01a"
type="text/javascript"></script>
<script
src="js/ui/tool/select.js?v=ade791e"
src="js/ui/tool/select.js?v=e27bbdf"
type="text/javascript"></script>
<script src="js/ui/tool/stamp.js?v=3c07ac8" type="text/javascript"></script>
<script src="js/ui/tool/stamp.js?v=4a86ff8" type="text/javascript"></script>
<script
src="js/ui/tool/interrogate.js?v=e579ff1"
type="text/javascript"></script>

View file

@ -5,9 +5,37 @@
*/
const config = makeReadOnly(
{
// Grid Size
gridSize: 64,
// Scroll Tick Limit (How much must scroll to reach next tick)
wheelTickSize: 50,
/** Select Tool */
// Handle Draw Size
handleDrawSize: 12,
// Handle Draw Hover Scale
handleDrawHoverScale: 1.3,
// Handle Detect Size
handleDetectSize: 20,
// Rotate Handle Distance (from selection)
rotateHandleDistance: 32,
// Rotation Snapping Distance
rotationSnappingDistance: (10 * Math.PI) / 180,
// Rotation Snapping Angles
rotationSnappingAngles: [
(-Math.PI * 4) / 4,
(-Math.PI * 3) / 4,
(-Math.PI * 2) / 4,
(-Math.PI * 1) / 4,
0,
(Math.PI * 1) / 4,
(Math.PI * 2) / 4,
(Math.PI * 3) / 4,
(Math.PI * 4) / 4,
],
// Endpoint
api: makeReadOnly({path: "/sdapi/v1/"}),
},

View file

@ -111,6 +111,10 @@ var stableDiffusionData = {
//firstphase_height: 0, //20230102 welp looks like the entire way HRfix is implemented has become bonkersly different
hr_scale: 2.0,
hr_upscaler: "None",
hr_second_pass_steps: 0,
hr_resize_x: 0,
hr_resize_y: 0,
hr_square_aspect: false,
styles: [],
// here's some more fields that might be useful
@ -139,6 +143,7 @@ var stableDiffusionData = {
var host = "";
var url = "/sdapi/v1/";
const basePixelCount = 64; //64 px - ALWAYS 64 PX
var focused = true;
function getSDData() {
const w = workspaces.current.settings;
@ -174,8 +179,10 @@ function startup() {
changeSmoothRendering();
changeSeed();
changeHiResFix();
changeHiResSquare();
changeRestoreFaces();
changeSyncCursorSize();
checkFocus();
}
function setFixedHost(h, changePromptMessage) {
@ -343,7 +350,23 @@ async function testHostConnection() {
let checkInProgress = false;
const checkConnection = async (notify = false) => {
const checkConnection = async (
notify = false,
simpleProgressStatus = false
) => {
const apiIssueResult = () => {
setConnectionStatus("apiissue");
const message = `The host is online, but the API seems to be disabled.\nHave you run the webui with the flag '--api', or is the flag '--gradio-debug' currently active?`;
console.error(message);
if (notify) alert(message);
};
const offlineResult = () => {
setConnectionStatus("offline");
const message = `The connection with the host returned an error: ${response.status} - ${response.statusText}`;
console.error(message);
if (notify) alert(message);
};
if (checkInProgress)
throw new CheckInProgressError(
"Check is currently in progress, please try again"
@ -352,50 +375,63 @@ async function testHostConnection() {
var url = document.getElementById("host").value + "/startup-events";
// Attempt normal request
try {
// Check if API is available
const response = await fetch(
document.getElementById("host").value + "/sdapi/v1/options"
);
const optionsdata = await response.json();
if (optionsdata["use_scale_latent_for_hires_fix"]) {
const message = `You are using an outdated version of A1111 webUI.\nThe HRfix options will not work until you update to at least commit ef27a18 or newer.\n(https://github.com/AUTOMATIC1111/stable-diffusion-webui/commit/ef27a18b6b7cb1a8eebdc9b2e88d25baf2c2414d)\nHRfix will fallback to half-resolution only.`;
console.warn(message);
if (notify) alert(message);
// Hide all new hrfix options
document
.querySelectorAll(".hrfix")
.forEach((el) => (el.style.display = "none"));
// We are using old HRFix
global.isOldHRFix = true;
stableDiffusionData.enable_hr = false;
}
switch (response.status) {
case 200: {
setConnectionStatus("online");
// Load data as soon as connection is first stablished
if (firstTimeOnline) {
getConfig();
getStyles();
getSamplers();
getUpscalers();
getModels();
firstTimeOnline = false;
if (simpleProgressStatus) {
const response = await fetch(
document.getElementById("host").value + "/sdapi/v1/progress" // seems to be the "lightest" endpoint?
);
switch (response.status) {
case 200: {
setConnectionStatus("online");
break;
}
case 404: {
apiIssueResult();
break;
}
default: {
offlineResult();
}
break;
}
case 404: {
setConnectionStatus("apiissue");
const message = `The host is online, but the API seems to be disabled.\nHave you run the webui with the flag '--api', or is the flag '--gradio-debug' currently active?`;
console.error(message);
} else {
// Check if API is available
const response = await fetch(
document.getElementById("host").value + "/sdapi/v1/options"
);
const optionsdata = await response.json();
if (optionsdata["use_scale_latent_for_hires_fix"]) {
const message = `You are using an outdated version of A1111 webUI.\nThe HRfix options will not work until you update to at least commit ef27a18 or newer.\n(https://github.com/AUTOMATIC1111/stable-diffusion-webui/commit/ef27a18b6b7cb1a8eebdc9b2e88d25baf2c2414d)\nHRfix will fallback to half-resolution only.`;
console.warn(message);
if (notify) alert(message);
break;
// Hide all new hrfix options
document
.querySelectorAll(".hrfix")
.forEach((el) => (el.style.display = "none"));
// We are using old HRFix
global.isOldHRFix = true;
stableDiffusionData.enable_hr = false;
}
default: {
setConnectionStatus("offline");
const message = `The connection with the host returned an error: ${response.status} - ${response.statusText}`;
console.error(message);
if (notify) alert(message);
switch (response.status) {
case 200: {
setConnectionStatus("online");
// Load data as soon as connection is first stablished
if (firstTimeOnline) {
getConfig();
getStyles();
getSamplers();
getUpscalers();
getModels();
firstTimeOnline = false;
}
break;
}
case 404: {
apiIssueResult();
break;
}
default: {
offlineResult();
}
}
}
} catch (e) {
@ -421,7 +457,9 @@ async function testHostConnection() {
return status;
};
await checkConnection(!urlParams.has("noprompt"));
if (focused || firstTimeOnline) {
await checkConnection(!urlParams.has("noprompt"));
}
// On click, attempt to refresh
connectionIndicator.onclick = async () => {
@ -433,15 +471,23 @@ async function testHostConnection() {
}
};
// Checks every 5 seconds if offline, 30 seconds if online
// Checks every 5 seconds if offline, 60 seconds if online
const checkAgain = () => {
setTimeout(
async () => {
await checkConnection();
checkFocus();
if (focused || firstTimeOnline) {
setTimeout(
async () => {
let simple = !firstTimeOnline;
await checkConnection(false, simple);
checkAgain();
},
connectionStatus ? 60000 : 5000
);
} else {
setTimeout(() => {
checkAgain();
},
connectionStatus ? 30000 : 5000
);
}, 60000);
}
};
checkAgain();
@ -692,6 +738,17 @@ const lockPxSlider = makeSlider(
1
);
const hrStepsSlider = makeSlider(
"HRfix Steps",
document.getElementById("hrFixSteps"),
"hr_second_pass_steps",
0,
localStorage.getItem("openoutpaint/settings.max-steps") || 70,
5,
0,
1
);
function changeMaskBlur() {
stableDiffusionData.mask_blur = parseInt(
document.getElementById("maskBlur").value
@ -704,33 +761,43 @@ function changeSeed() {
localStorage.setItem("openoutpaint/seed", stableDiffusionData.seed);
}
function changeHRFX() {
stableDiffusionData.hr_resize_x =
document.getElementById("hr_resize_x").value;
}
function changeHRFY() {
stableDiffusionData.hr_resize_y =
document.getElementById("hr_resize_y").value;
}
function changeHiResFix() {
stableDiffusionData.enable_hr = Boolean(
document.getElementById("cbxHRFix").checked
);
localStorage.setItem("openoutpaint/enable_hr", stableDiffusionData.enable_hr);
var hrfSlider = document.getElementById("hrFixScale");
var hrfOpotions = document.getElementById("hrFixUpscaler");
var hrfLabel = document.getElementById("hrFixLabel");
var hrfDenoiseSlider = document.getElementById("hrDenoising");
var hrfLockPxSlider = document.getElementById("hrFixLockPx");
// var hrfSlider = document.getElementById("hrFixScale");
// var hrfOpotions = document.getElementById("hrFixUpscaler");
// var hrfLabel = document.getElementById("hrFixLabel");
// var hrfDenoiseSlider = document.getElementById("hrDenoising");
// var hrfLockPxSlider = document.getElementById("hrFixLockPx");
if (stableDiffusionData.enable_hr) {
hrfSlider.classList.remove("invisible");
hrfOpotions.classList.remove("invisible");
hrfLabel.classList.remove("invisible");
hrfDenoiseSlider.classList.remove("invisible");
hrfLockPxSlider.classList.remove("invisible");
//state.ctxmenu.keepUnmaskedBlurSliderLinebreak.classList.add("invisible");
document
.querySelectorAll(".hrfix")
.forEach((el) => el.classList.remove("invisible"));
} else {
hrfSlider.classList.add("invisible");
hrfOpotions.classList.add("invisible");
hrfLabel.classList.add("invisible");
hrfDenoiseSlider.classList.add("invisible");
hrfLockPxSlider.classList.add("invisible");
//state.ctxmenu.keepUnmaskedBlurSliderLinebreak.classList.remove("invisible");
document
.querySelectorAll(".hrfix")
.forEach((el) => el.classList.add("invisible"));
}
}
function changeHiResSquare() {
stableDiffusionData.hr_square_aspect = Boolean(
document.getElementById("cbxHRFSquare").checked
);
}
function changeRestoreFaces() {
stableDiffusionData.restore_faces = Boolean(
document.getElementById("cbxRestoreFaces").checked
@ -828,7 +895,17 @@ async function getUpscalers() {
.split(",")
.map((v) => v.trim()); // need "None" for stupid hrfix changes razza frazza
const upscalers = upscalersPlusNone.filter((v) => v !== "None"); // converting the result to a list of upscalers
// upscalersPlusNone.push([
// "Latent",
// "Latent (antialiased)",
// "Latent (bicubic)",
// "Latent (bicubic, antialiased)",
// "Latent (nearest)",
// ]);
upscalersPlusNone.push("Latent");
upscalersPlusNone.push("Latent (antialiased)");
upscalersPlusNone.push("Latent (bicubic)");
upscalersPlusNone.push("Latent (bicubic, antialiased)");
upscalersPlusNone.push("Latent (nearest)"); // GRUMBLE GRUMBLE
upscalerAutoComplete.options = upscalers.map((u) => {
@ -1214,3 +1291,24 @@ function resetToDefaults() {
localStorage.clear();
}
}
document.addEventListener("visibilitychange", () => {
checkFocus();
});
window.addEventListener("blur", () => {
checkFocus();
});
window.addEventListener("focus", () => {
checkFocus();
});
function checkFocus() {
let hasFocus = document.hasFocus();
if (document.hidden || !hasFocus) {
focused = false;
} else {
focused = true;
}
}

View file

@ -160,61 +160,72 @@ debugLayer.hide(); // Hidden by default
* The global viewport object (may be modularized in the future). All
* coordinates given are of the center of the viewport
*
* cx and cy are the viewport's world coordinates, scaled to zoom level.
* _x and _y are actual coordinates in the DOM space
* cx and cy are the viewport's world coordinates.
*
* The transform() function does some transforms and writes them to the
* provided element.
*/
const viewport = {
get cx() {
return this._x * this.zoom;
},
class Viewport {
cx = 0;
cy = 0;
set cx(v) {
return (this._x = v / this.zoom);
},
_x: 0,
get cy() {
return this._y * this.zoom;
},
set cy(v) {
return (this._y = v / this.zoom);
},
_y: 0,
zoom: 1,
rotation: 0,
zoom = 1;
/**
* Gets viewport width in canvas coordinates
*/
get w() {
return (window.innerWidth * 1) / this.zoom;
},
return window.innerWidth * this.zoom;
}
/**
* Gets viewport height in canvas coordinates
*/
get h() {
return (window.innerHeight * 1) / this.zoom;
},
return window.innerHeight * this.zoom;
}
constructor(x, y) {
this.x = x;
this.y = y;
}
get v2c() {
const m = new DOMMatrix();
m.translateSelf(-this.w / 2, -this.h / 2);
m.translateSelf(this.cx, this.cy);
m.scaleSelf(this.zoom);
return m;
}
get c2v() {
return this.v2c.invertSelf();
}
viewToCanvas(x, y) {
return {
x: this.cx + this.w * (x / window.innerWidth - 0.5),
y: this.cy + this.h * (y / window.innerHeight - 0.5),
};
},
if (x.x !== undefined) return this.v2c.transformPoint(x);
return this.v2c.transformPoint({x, y});
}
canvasToView(x, y) {
return {
x: window.innerWidth * ((x - this.cx) / this.w) + window.innerWidth / 2,
y: window.innerHeight * ((y - this.cy) / this.h) + window.innerHeight / 2,
};
},
if (x.x !== undefined) return this.c2v.transformPoint(x);
return this.c2v.transformPoint({x, y});
}
/**
* Apply transformation
*
* @param {HTMLElement} el Element to apply CSS transform to
*/
transform(el) {
el.style.transformOrigin = `${this.cx}px ${this.cy}px`;
el.style.transform = `scale(${this.zoom}) translate(${-(
this._x -
this.w / 2
)}px, ${-(this._y - this.h / 2)}px)`;
},
};
el.style.transformOrigin = "0px 0px";
el.style.transform = this.c2v;
}
}
const viewport = new Viewport(0, 0);
viewport.cx = imageCollection.size.w / 2;
viewport.cy = imageCollection.size.h / 2;
@ -296,7 +307,7 @@ mouse.listen.camera.onwheel.on((evn) => {
const pcy = viewport.cy;
// Apply zoom
viewport.zoom *= 1 - evn.delta * 0.0002;
viewport.zoom *= 1 + evn.delta * 0.0002;
// Apply normal zoom (center of viewport)
viewport.cx = pcx;
@ -305,13 +316,13 @@ mouse.listen.camera.onwheel.on((evn) => {
viewport.transform(imageCollection.element);
// Calculate new viewport center and move
const newCursorPosition = viewport.viewToCanvas(evn.x, evn.y);
viewport.cx = pcx - (newCursorPosition.x - cursorPosition.x);
viewport.cy = pcy - (newCursorPosition.y - cursorPosition.y);
//const newCursorPosition = viewport.viewToCanvas(evn.x, evn.y);
//viewport.cx = pcx - (newCursorPosition.x - cursorPosition.x);
//viewport.cy = pcy - (newCursorPosition.y - cursorPosition.y);
viewport.transform(imageCollection.element);
//viewport.transform(imageCollection.element);
toolbar.currentTool.redraw();
toolbar._current_tool.redrawui && toolbar._current_tool.redrawui();
});
const cameraPaintStart = (evn) => {
@ -320,8 +331,8 @@ const cameraPaintStart = (evn) => {
const cameraPaint = (evn) => {
if (worldInit) {
viewport.cx = worldInit.x + (evn.ix - evn.x) / viewport.zoom;
viewport.cy = worldInit.y + (evn.iy - evn.y) / viewport.zoom;
viewport.cx = worldInit.x + (evn.ix - evn.x) * viewport.zoom;
viewport.cy = worldInit.y + (evn.iy - evn.y) * viewport.zoom;
// Limits
viewport.cx = Math.max(
@ -337,6 +348,9 @@ const cameraPaint = (evn) => {
}
viewport.transform(imageCollection.element);
toolbar._current_tool.state.redrawui &&
toolbar._current_tool.state.redrawui();
if (global.debug) {
debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height);
debugCtx.fillStyle = "#F0F";

View file

@ -90,14 +90,20 @@ const toolbar = {
name: toolname,
enabled: false,
_element: null,
state: {},
state: {
redrawui: () => tool.state.redraw && tool.state.redraw(),
},
options,
/**
* If the tool has a redraw() function in its state, then run it
*/
redraw: () => {
tool.state.redrawui && tool.state.redrawui();
tool.state.redraw && tool.state.redraw();
},
redrawui: () => {
tool.state.redrawui && tool.state.redrawui();
},
enable: (opt = null) => {
if (toolbar._locked) return;

View file

@ -32,6 +32,31 @@ class BoundingBox {
w = 0;
h = 0;
/** @type {Point} */
get tl() {
return {x: this.x, y: this.y};
}
/** @type {Point} */
get tr() {
return {x: this.x + this.w, y: this.y};
}
/** @type {Point} */
get bl() {
return {x: this.x, y: this.y + this.h};
}
/** @type {Point} */
get br() {
return {x: this.x + this.w, y: this.y + this.h};
}
/** @type {Point} */
get center() {
return {x: this.x + this.w / 2, y: this.y + this.h / 2};
}
constructor({x, y, w, h} = {x: 0, y: 0, w: 0, h: 0}) {
this.x = x;
this.y = y;
@ -64,6 +89,18 @@ class BoundingBox {
h: maxy - miny,
});
}
/**
* Returns a transformed bounding box (using top-left, bottom-right points)
*
* @param {DOMMatrix} transform Transformation matrix to transform points
*/
transform(transform) {
return BoundingBox.fromStartEnd(
transform.transformPoint({x: this.x, y: this.y}),
transform.transformPoint({x: this.x + this.w, y: this.y + this.h})
);
}
}
/**

View file

@ -176,7 +176,7 @@ const colorBrushTool = () =>
uiCtx.arc(
vcp.x,
vcp.y,
(state.eyedropper ? 50 : state.brushSize / 2) * viewport.zoom,
(state.eyedropper ? 50 : state.brushSize / 2) / viewport.zoom,
0,
2 * Math.PI,
true
@ -197,7 +197,7 @@ const colorBrushTool = () =>
uiCtx.arc(
vcp.x,
vcp.y,
(state.brushSize / 2) * viewport.zoom,
state.brushSize / (2 * viewport.zoom),
0,
2 * Math.PI,
true

View file

@ -156,6 +156,7 @@ const _dream = async (endpoint, request) => {
generating(false);
}
var responseSubdata = JSON.parse(data.info);
console.debug(responseSubdata);
var returnData = {
images: data.images,
seeds: responseSubdata.all_seeds,
@ -176,6 +177,7 @@ const _dream = async (endpoint, request) => {
* @returns {Promise<HTMLImageElement | null>}
*/
const _generate = async (endpoint, request, bb, options = {}) => {
var alertCount = 0;
defaultOpt(options, {
drawEvery: 0.2 / request.n_iter,
keepUnmask: null,
@ -515,6 +517,17 @@ const _generate = async (endpoint, request, bb, options = {}) => {
});
};
const removeImg = async () => {
if (!images[at]) return;
images.splice(at, 1);
seeds.splice(at, 1);
if (at >= images.length) at = 0;
imageindextxt.textContent = `${at}/${images.length - 1}`;
var seed = seeds[at];
seedbtn.title = "Use seed " + seed;
redraw();
};
const makeMore = async () => {
const moreQ = await waitQueue();
try {
@ -531,9 +544,14 @@ const _generate = async (endpoint, request, bb, options = {}) => {
seeds.push(...dreamData.seeds);
imageindextxt.textContent = `${at}/${images.length - 1}`;
} catch (e) {
alert(
`Error generating images. Please try again or see console for more details`
);
if (alertCount < 2) {
alert(
`Error generating images. Please try again or see console for more details`
);
} else {
eagerGenerateCount = 0;
}
alertCount++;
console.warn(`[dream] Error generating images:`);
console.warn(e);
} finally {
@ -589,6 +607,9 @@ const _generate = async (endpoint, request, bb, options = {}) => {
case "+":
makeMore();
break;
case "-":
removeImg();
break;
default:
switch (evn.code) {
case "ArrowRight":
@ -652,7 +673,11 @@ const _generate = async (endpoint, request, bb, options = {}) => {
const oncancelhandler = mouse.listen.world.btn.right.onclick.on(
(evn, state) => {
if (!state.dream_processed && bb.contains(evn.x, evn.y)) {
discardImg();
if (images.length > 1) {
removeImg();
} else {
discardImg();
}
imageCollection.inputElement.style.cursor = "auto";
state.dream_processed = true;
}
@ -736,6 +761,12 @@ const _generate = async (endpoint, request, bb, options = {}) => {
morebtn.addEventListener("click", makeMore);
imageSelectMenu.appendChild(morebtn);
const removebtn = document.createElement("button");
removebtn.textContent = "-";
removebtn.title = "Remove From Batch";
removebtn.addEventListener("click", removeImg);
imageSelectMenu.appendChild(removebtn);
const acceptbtn = document.createElement("button");
acceptbtn.textContent = "Y";
acceptbtn.title = "Apply Current";
@ -815,41 +846,64 @@ const dream_generate_callback = async (bb, resolution, state) => {
// Use txt2img if canvas is blank
if (isCanvasBlank(0, 0, bb.w, bb.h, visibleCanvas)) {
if (
!global.isOldHRFix &&
request.enable_hr &&
localStorage.getItem("openoutpaint/settings.hrfix-liar") == "true"
) {
if (!global.isOldHRFix && request.enable_hr) {
/**
* try and make the new HRfix method useful for our purposes
* since it now returns an image that's been upscaled x the hr_scale parameter,
* we cheekily lie to SD and tell it that the original dimensions are _divided_
* by the scale factor so it returns something about the same size as we wanted initially
*/
// ok so instead, only do that if stableDiffusionData.hr_fix_lock_px > 0
if (stableDiffusionData.hr_fix_lock_px > 0) {
// find the appropriate scale factor for hrfix
// laziness convenience
let lockpx = stableDiffusionData.hr_fix_lock_px;
if (lockpx > 0) {
// find the most appropriate scale factor for hrfix
var widthFactor =
request.width / stableDiffusionData.hr_fix_lock_px <= 4
? request.width / stableDiffusionData.hr_fix_lock_px
: 4;
request.width / lockpx <= 4 ? request.width / lockpx : 4;
var heightFactor =
request.height / stableDiffusionData.hr_fix_lock_px <= 4
? request.height / stableDiffusionData.hr_fix_lock_px
: 4;
request.height / lockpx <= 4 ? request.height / lockpx : 4;
var factor = heightFactor > widthFactor ? heightFactor : widthFactor;
request.hr_scale = hrFixScaleSlider.value = factor < 1 ? 1 : factor;
}
// moar laziness convenience
var divW = Math.floor(request.width / request.hr_scale);
var divH = Math.floor(request.height / request.hr_scale);
var newWidth = Math.floor(request.width / request.hr_scale);
var newHeight = Math.floor(request.height / request.hr_scale);
request.width = newWidth;
request.height = newHeight;
if (localStorage.getItem("openoutpaint/settings.hrfix-liar") == "true") {
/**
* since it now returns an image that's been upscaled x the hr_scale parameter,
* we cheekily lie to SD and tell it that the original dimensions are _divided_
* by the scale factor so it returns something about the same size as we wanted initially
*/
var firstpassWidth = divW;
var firstpassHeight = divH; // liar's firstpass output resolution
var desiredWidth = request.width;
var desiredHeight = request.height; // truthful desired output resolution
} else {
// use scale normally, dump supersampled image into undersized reticle
var desiredWidth = request.width * request.hr_scale;
var desiredHeight = request.height * request.hr_scale; //desired 2nd-pass output resolution
var firstpassWidth = request.width;
var firstpassHeight = request.height;
}
// ensure firstpass "resolution" complies with lockpx
if (lockpx > 0) {
//sigh repeated loop
firstpassWidth = divW < lockpx ? divW : lockpx;
firstpassHeight = divH < lockpx ? divH : lockpx;
}
if (stableDiffusionData.hr_square_aspect) {
larger =
firstpassWidth > firstpassHeight ? firstpassWidth : firstpassHeight;
firstpassWidth = firstpassHeight = larger;
}
request.width = firstpassWidth;
request.height = firstpassHeight;
request.hr_resize_x = desiredWidth;
request.hr_resize_y = desiredHeight;
}
// For compatibility with the old HRFix API
if (global.isOldHRFix && request.enable_hr) {
// For compatibility with the old HRFix API
request.firstphase_width = request.width / 2;
request.firstphase_height = request.height / 2;
}
@ -1187,10 +1241,16 @@ const _dream_onwheel = (evn, state) => {
return;
}
// A simple but (I hope) effective fix for mouse wheel behavior
_dream_wheel_accum += evn.delta;
let delta = evn.delta;
if (evn.evn.shiftKey) delta *= 0.01;
if (Math.abs(_dream_wheel_accum) > config.wheelTickSize) {
// A simple but (I hope) effective fix for mouse wheel behavior
_dream_wheel_accum += delta;
if (
!evn.evn.shiftKey &&
Math.abs(_dream_wheel_accum) > config.wheelTickSize
) {
// Snap to next or previous position
const v =
state.cursorSize -
@ -1199,6 +1259,12 @@ const _dream_onwheel = (evn, state) => {
state.cursorSize = state.setCursorSize(v + snap(v, 0, 128));
state.mousemovecb(evn);
_dream_wheel_accum = 0; // Zero accumulation
} else if (evn.evn.shiftKey && Math.abs(_dream_wheel_accum) >= 1) {
const v = state.cursorSize - _dream_wheel_accum;
state.cursorSize = state.setCursorSize(v);
state.mousemovecb(evn);
_dream_wheel_accum = 0; // Zero accumulation
}
};
@ -1888,11 +1954,10 @@ const img2imgTool = () =>
return;
}
const bbvp = {
...viewport.canvasToView(bb.x, bb.y),
w: viewport.zoom * bb.w,
h: viewport.zoom * bb.h,
};
const bbvp = BoundingBox.fromStartEnd(
viewport.canvasToView(bb.tl),
viewport.canvasToView(bb.br)
);
// For displaying border mask
const bbCanvas = document.createElement("canvas");

View file

@ -27,11 +27,7 @@ const _tool = {
reticleStyle: global.hasActiveInput ? "#BBF" : "#FFF",
});
const bbvp = {
...viewport.canvasToView(bb.x, bb.y),
w: viewport.zoom * bb.w,
h: viewport.zoom * bb.h,
};
const bbvp = bb.transform(viewport.c2v);
uiCtx.save();
@ -174,6 +170,8 @@ const _tool = {
/**
* Gets the selection bounding box
*
* @returns {BoundingBox}
*/
get bb() {
if (this._dirty_bb && this._selected) {
@ -273,4 +271,389 @@ const _tool = {
return selection;
},
/**
* Processes cursor position
*
* @param {Point} wpoint World coordinate of the cursor
* @param {boolean} snapToGrid Snap to grid
*/
_process_cursor(wpoint, snapToGrid) {
// Get cursor positions
let x = wpoint.x;
let y = wpoint.y;
let sx = x;
let sy = y;
if (snapToGrid) {
sx += snap(x, 0, config.gridSize);
sy += snap(y, 0, config.gridSize);
}
const vpc = viewport.canvasToView(x, y);
const vpsc = viewport.canvasToView(sx, sy);
return {
// World Coordinates
x,
y,
sx,
sy,
// Viewport Coordinates
vpx: vpc.x,
vpy: vpc.y,
vpsx: vpsc.x,
vpsy: vpsc.y,
};
},
/**
* Represents a marquee selection with an image
*/
MarqueeSelection: class {
/** @type {HTMLCanvasElement} */
canvas;
_dirty = false;
_position = {x: 0, y: 0};
/**
* @type {Point}
*/
get position() {
return this._position;
}
set position(v) {
this._dirty = true;
this._position = v;
}
_scale = {x: 1, y: 1};
/**
* @type {Point}
*/
get scale() {
return this._scale;
}
set scale(v) {
if (v.x === 0 || v.y === 0) return;
this._dirty = true;
this._scale = v;
}
_rotation = 0;
get rotation() {
return this._rotation;
}
set rotation(v) {
this._dirty = true;
this._rotation = v;
}
/**
* @param {HTMLCanvasElement} canvas Selected image canvas
* @param {Point} position Initial position of the selection
*/
constructor(canvas, position = {x: 0, y: 0}) {
this.canvas = canvas;
this.position = position;
}
/** @type {DOMMatrix} */
_rtmatrix = null;
get rtmatrix() {
if (!this._rtmatrix || this._dirty) {
const m = new DOMMatrix();
m.translateSelf(this.position.x, this.position.y);
m.rotateSelf((this.rotation * 180) / Math.PI);
this._rtmatrix = m;
}
return this._rtmatrix;
}
/** @type {DOMMatrix} */
_matrix = null;
get matrix() {
if (!this._matrix || this._dirty) {
this._matrix = this.rtmatrix.scaleSelf(this.scale.x, this.scale.y);
}
return this._matrix;
}
/**
* If the main marquee box contains a given point
*
* @param {number} x X coordinate of the point
* @param {number} y Y coordinate of the point
*/
contains(x, y) {
const p = this.matrix.invertSelf().transformPoint({x, y});
return (
Math.abs(p.x) < this.canvas.width / 2 &&
Math.abs(p.y) < this.canvas.height / 2
);
}
hoveringRotateHandle(x, y, scale = 1) {
const localc = this.rtmatrix.inverse().transformPoint({x, y});
const localrh = {
x: 0,
y:
(-this.scale.y * this.canvas.height) / 2 -
config.rotateHandleDistance * scale,
};
const dx = Math.abs(localc.x - localrh.x);
const dy = Math.abs(localc.y - localrh.y);
return (
dx * dx + dy * dy <
(scale * scale * config.handleDetectSize * config.handleDetectSize) / 4
);
}
hoveringHandle(x, y, scale = 1) {
const localbb = new BoundingBox({
x: (this.scale.x * -this.canvas.width) / 2,
y: (this.scale.y * -this.canvas.height) / 2,
w: this.canvas.width * this.scale.x,
h: this.canvas.height * this.scale.y,
});
const localc = this.rtmatrix.inverse().transformPoint({x, y});
const ontl =
Math.max(
Math.abs(localc.x - localbb.tl.x),
Math.abs(localc.y - localbb.tl.y)
) <
(config.handleDetectSize / 2) * scale;
const ontr =
Math.max(
Math.abs(localc.x - localbb.tr.x),
Math.abs(localc.y - localbb.tr.y)
) <
(config.handleDetectSize / 2) * scale;
const onbl =
Math.max(
Math.abs(localc.x - localbb.bl.x),
Math.abs(localc.y - localbb.bl.y)
) <
(config.handleDetectSize / 2) * scale;
const onbr =
Math.max(
Math.abs(localc.x - localbb.br.x),
Math.abs(localc.y - localbb.br.y)
) <
(config.handleDetectSize / 2) * scale;
return {onHandle: ontl || ontr || onbl || onbr, ontl, ontr, onbl, onbr};
}
hoveringBox(x, y) {
const localbb = new BoundingBox({
x: -this.canvas.width / 2,
y: -this.canvas.height / 2,
w: this.canvas.width,
h: this.canvas.height,
});
const localc = this.matrix.inverse().transformPoint({x, y});
return (
!this.hoveringHandle(x, y).onHandle &&
localbb.contains(localc.x, localc.y)
);
}
/**
* Draws the marquee selector box
*
* @param {CanvasRenderingContext2D} context A context for rendering the box to
* @param {Point} cursor Cursor position
* @param {DOMMatrix} transform A transformation matrix to transform the position by
*/
drawBox(context, cursor, transform = new DOMMatrix()) {
const drawscale =
1 / Math.sqrt(transform.a * transform.a + transform.b * transform.b);
const m = transform.multiply(this.matrix);
context.save();
const localbb = new BoundingBox({
x: -this.canvas.width / 2,
y: -this.canvas.height / 2,
w: this.canvas.width,
h: this.canvas.height,
});
// Line Style
context.strokeStyle = "#FFF";
context.lineWidth = 2;
const tl = m.transformPoint(localbb.tl);
const tr = m.transformPoint(localbb.tr);
const bl = m.transformPoint(localbb.bl);
const br = m.transformPoint(localbb.br);
const bbc = m.transformPoint({x: 0, y: 0});
context.beginPath();
context.arc(bbc.x, bbc.y, 5, 0, Math.PI * 2);
context.stroke();
context.setLineDash([4, 2]);
// Draw main rectangle
context.beginPath();
context.moveTo(tl.x, tl.y);
context.lineTo(tr.x, tr.y);
context.lineTo(br.x, br.y);
context.lineTo(bl.x, bl.y);
context.lineTo(tl.x, tl.y);
context.stroke();
// Draw rotation handle
context.setLineDash([]);
const hm = new DOMMatrix().rotateSelf((this.rotation * 180) / Math.PI);
const tm = m.transformPoint({x: 0, y: -this.canvas.height / 2});
const rho = hm.transformPoint({x: 0, y: -config.rotateHandleDistance});
const rh = {x: tm.x + rho.x, y: tm.y + rho.y};
let handleRadius = config.handleDrawSize / 2;
if (this.hoveringRotateHandle(cursor.x, cursor.y, drawscale))
handleRadius *= config.handleDrawHoverScale;
context.beginPath();
context.moveTo(tm.x, tm.y);
context.lineTo(rh.x, rh.y);
context.stroke();
context.beginPath();
context.arc(rh.x, rh.y, handleRadius, 0, 2 * Math.PI);
context.stroke();
// Draw handles
const drawHandle = (pt, hover) => {
let hsz = config.handleDrawSize / 2;
if (hover) hsz *= config.handleDrawHoverScale;
const htl = hm.transformPoint({x: -hsz, y: -hsz});
const htr = hm.transformPoint({x: hsz, y: -hsz});
const hbr = hm.transformPoint({x: hsz, y: hsz});
const hbl = hm.transformPoint({x: -hsz, y: hsz});
context.beginPath();
context.moveTo(htl.x + pt.x, htl.y + pt.y);
context.lineTo(htr.x + pt.x, htr.y + pt.y);
context.lineTo(hbr.x + pt.x, hbr.y + pt.y);
context.lineTo(hbl.x + pt.x, hbl.y + pt.y);
context.lineTo(htl.x + pt.x, htl.y + pt.y);
context.stroke();
};
context.strokeStyle = "#FFF";
context.lineWidth = 2;
context.setLineDash([]);
const {ontl, ontr, onbl, onbr} = this.hoveringHandle(
cursor.x,
cursor.y,
drawscale
);
drawHandle(tl, ontl);
drawHandle(tr, ontr);
drawHandle(bl, onbl);
drawHandle(br, onbr);
context.restore();
return () => {
const border = config.handleDrawSize * config.handleDrawHoverScale;
const minx = Math.min(tl.x, tr.x, bl.x, br.x, rh.x) - border;
const maxx = Math.max(tl.x, tr.x, bl.x, br.x, rh.x) + border;
const miny = Math.min(tl.y, tr.y, bl.y, br.y, rh.y) - border;
const maxy = Math.max(tl.y, tr.y, bl.y, br.y, rh.y) + border;
context.clearRect(minx, miny, maxx - minx, maxy - miny);
};
}
/**
* Draws the selected image
*
* @param {CanvasRenderingContext2D} context A context for rendering the image to
* @param {CanvasRenderingContext2D} peekctx A context for rendering the layer peeking to
* @param {object} options
* @param {DOMMatrix} options.transform A transformation matrix to transform the position by
* @param {number} options.opacity Opacity of the peek display
*/
drawImage(context, peekctx, options = {}) {
defaultOpt(options, {
transform: new DOMMatrix(),
opacity: 0.4,
});
context.save();
peekctx.save();
const m = options.transform.multiply(this.matrix);
// Draw image
context.setTransform(m);
context.drawImage(
this.canvas,
-this.canvas.width / 2,
-this.canvas.height / 2,
this.canvas.width,
this.canvas.height
);
// Draw peek
peekctx.filter = `opacity(${options.opacity * 100}%)`;
peekctx.setTransform(m);
peekctx.drawImage(
this.canvas,
-this.canvas.width / 2,
-this.canvas.height / 2,
this.canvas.width,
this.canvas.height
);
peekctx.restore();
context.restore();
return () => {
// Here we only save transform for performance
const pt = context.getTransform();
const ppt = context.getTransform();
context.setTransform(m);
peekctx.setTransform(m);
context.clearRect(
-this.canvas.width / 2 - 10,
-this.canvas.height / 2 - 10,
this.canvas.width + 20,
this.canvas.height + 20
);
peekctx.clearRect(
-this.canvas.width / 2 - 10,
-this.canvas.height / 2 - 10,
this.canvas.width + 20,
this.canvas.height + 20
);
context.setTransform(pt);
peekctx.setTransform(ppt);
};
}
},
};

View file

@ -11,6 +11,7 @@ const selectTransformTool = () =>
mouse.listen.world.onmousemove.on(state.movecb);
mouse.listen.world.btn.left.onclick.on(state.clickcb);
mouse.listen.world.btn.left.ondragstart.on(state.dragstartcb);
mouse.listen.world.btn.left.ondrag.on(state.dragcb);
mouse.listen.world.btn.left.ondragend.on(state.dragendcb);
// Canvas right mouse handler
@ -29,12 +30,19 @@ const selectTransformTool = () =>
keyboard.onShortcut({ctrl: true, key: "KeyX"}, state.ctrlxcb);
state.selected = null;
// Register Layer
state.originalDisplayLayer = imageCollection.registerLayer(null, {
after: uil.layer,
category: "select-display",
});
},
(state, opt) => {
// Clear all those listeners and shortcuts we set up
mouse.listen.world.onmousemove.clear(state.movecb);
mouse.listen.world.btn.left.onclick.clear(state.clickcb);
mouse.listen.world.btn.left.ondragstart.clear(state.dragstartcb);
mouse.listen.world.btn.left.ondrag.clear(state.dragcb);
mouse.listen.world.btn.left.ondragend.clear(state.dragendcb);
mouse.listen.world.btn.right.onclick.clear(state.cancelcb);
@ -55,6 +63,10 @@ const selectTransformTool = () =>
// Clears overlay
imageCollection.inputElement.style.cursor = "auto";
// Delete Layer
imageCollection.deleteLayer(state.originalDisplayLayer);
state.originalDisplayLayer = null;
},
{
init: (state) => {
@ -69,7 +81,6 @@ const selectTransformTool = () =>
state.selectionPeekOpacity = 40;
state.original = null;
state.dragging = null;
state._selected = null;
Object.defineProperty(state, "selected", {
get: () => state._selected,
@ -80,7 +91,6 @@ const selectTransformTool = () =>
return (state._selected = v);
},
});
state.moving = null;
// Some things to easy request for a redraw
state.lastMouseTarget = null;
@ -97,423 +107,334 @@ const selectTransformTool = () =>
}
};
/** @type {{selected: Point, offset: Point} | null} */
let moving = null;
/** @type {{handle: Point} | null} */
let scaling = null;
let rotating = false;
// Clears selection and make things right
state.reset = (erase = false) => {
if (state.selected && !erase)
state.originalLayer.ctx.drawImage(
state.original.image,
state.original.layer.ctx.drawImage(
state.selected.canvas,
state.original.x,
state.original.y
);
if (state.originalDisplayLayer) {
imageCollection.deleteLayer(state.originalDisplayLayer);
state.originalDisplayLayer = null;
state.originalDisplayLayer.clear();
}
if (state.dragging) state.dragging = null;
else state.selected = null;
state.rotation = 0;
state.original = null;
moving = null;
scaling = null;
rotating = null;
state.redraw();
};
// Selection bounding box object. Has some witchery to deal with handles.
const selectionBB = (x1, y1, x2, y2) => {
x1 = Math.round(x1);
y1 = Math.round(y1);
x2 = Math.round(x2);
y2 = Math.round(y2);
return {
original: {
x: Math.min(x1, x2),
y: Math.min(y1, y2),
w: Math.abs(x1 - x2),
h: Math.abs(y1 - y2),
},
x: Math.min(x1, x2),
y: Math.min(y1, y2),
w: Math.abs(x1 - x2),
h: Math.abs(y1 - y2),
updateOriginal() {
this.original.x = this.x;
this.original.y = this.y;
this.original.w = this.w;
this.original.h = this.h;
},
contains(x, y) {
return (
this.x <= x &&
x <= this.x + this.w &&
this.y <= y &&
y <= this.y + this.h
);
},
handles() {
const _createHandle = (x, y, originOffset = null) => {
return {
x,
y,
scaleTo: (tx, ty, keepAspectRatio = true) => {
const origin = {
x: this.original.x + this.original.w / 2,
y: this.original.y + this.original.h / 2,
};
let nx = tx;
let ny = ty;
// Selection Handlers
const selection = _tool._draggable_selection(state);
let xRatio = (nx - origin.x) / (x - origin.x);
let yRatio = (ny - origin.y) / (y - origin.y);
if (keepAspectRatio)
xRatio = yRatio = Math.min(xRatio, yRatio);
// UI Erasers
let eraseSelectedBox = () => null;
let eraseSelectedImage = () => null;
let eraseCursor = () => null;
let eraseSelection = () => null;
if (Number.isFinite(xRatio)) {
let left = this.original.x;
let right = this.original.x + this.original.w;
// Redraw UI
state.redrawui = () => {
// Get cursor positions
const {x, y, sx, sy} = _tool._process_cursor(
state.lastMouseMove,
state.snapToGrid
);
left = (left - origin.x) * xRatio + origin.x;
right = (right - origin.x) * xRatio + origin.x;
this.x = left;
this.w = right - left;
}
if (Number.isFinite(yRatio)) {
let top = this.original.y;
let bottom = this.original.y + this.original.h;
top = (top - origin.y) * yRatio + origin.y;
bottom = (bottom - origin.y) * yRatio + origin.y;
this.y = top;
this.h = bottom - top;
}
},
};
};
const size = viewport.zoom * 10;
return [
_createHandle(this.x, this.y, size),
_createHandle(this.x + this.w, this.y, size),
_createHandle(this.x, this.y + this.h, size),
_createHandle(this.x + this.w, this.y + this.h, size),
];
},
};
};
// Mouse move handler. As always, also renders cursor
state.movecb = (evn) => {
ovLayer.clear();
uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
state.erasePrevCursor && state.erasePrevCursor();
imageCollection.inputElement.style.cursor = "auto";
state.lastMouseTarget = evn.target;
state.lastMouseMove = evn;
let x = evn.x;
let y = evn.y;
if (state.snapToGrid) {
x += snap(evn.x, 0, 64);
y += snap(evn.y, 0, 64);
}
const vpc = viewport.canvasToView(x, y);
uiCtx.save();
// Update scale
if (state.scaling) {
state.scaling.scaleTo(x, y, state.keepAspectRatio);
}
// Update position
if (state.moving) {
state.selected.x = Math.round(x - state.moving.offset.x);
state.selected.y = Math.round(y - state.moving.offset.y);
state.selected.updateOriginal();
}
// Draw dragging box
if (state.dragging) {
uiCtx.setLineDash([2, 2]);
uiCtx.lineWidth = 1;
uiCtx.strokeStyle = "#FFF";
const ix = state.dragging.ix;
const iy = state.dragging.iy;
const bb = selectionBB(ix, iy, x, y);
const bbvp = {
...viewport.canvasToView(bb.x, bb.y),
w: viewport.zoom * bb.w,
h: viewport.zoom * bb.h,
};
uiCtx.strokeRect(bbvp.x, bbvp.y, bbvp.w, bbvp.h);
uiCtx.setLineDash([]);
}
eraseSelectedBox();
if (state.selected) {
ovCtx.lineWidth = 1;
ovCtx.strokeStyle = "#FFF";
const bb = {
x: state.selected.x,
y: state.selected.y,
w: state.selected.w,
h: state.selected.h,
};
const bbvp = {
...viewport.canvasToView(bb.x, bb.y),
w: viewport.zoom * bb.w,
h: viewport.zoom * bb.h,
};
// Draw Image
ovCtx.save();
ovCtx.filter = `opacity(${state.selectionPeekOpacity}%)`;
ovCtx.drawImage(
state.selected.image,
0,
0,
state.selected.image.width,
state.selected.image.height,
state.selected.x,
state.selected.y,
state.selected.w,
state.selected.h
eraseSelectedBox = state.selected.drawBox(
uiCtx,
{x, y},
viewport.c2v
);
ovCtx.restore();
}
};
state.originalDisplayLayer.clear();
state.originalDisplayLayer.ctx.save();
state.originalDisplayLayer.ctx.drawImage(
state.selected.image,
0,
0,
state.selected.image.width,
state.selected.image.height,
state.selected.x,
state.selected.y,
state.selected.w,
state.selected.h
// Mouse Move Handler
state.movecb = (evn) => {
state.lastMouseMove = evn;
// Get cursor positions
const {x, y, sx, sy} = _tool._process_cursor(evn, state.snapToGrid);
// Erase Cursor
eraseSelectedBox();
eraseSelectedImage();
eraseSelection();
eraseCursor();
imageCollection.inputElement.style.cursor = "default";
// Draw Box and Selected Image
if (state.selected) {
eraseSelectedBox = state.selected.drawBox(
uiCtx,
{x, y},
viewport.c2v
);
state.originalDisplayLayer.ctx.restore();
// Draw selection box
uiCtx.strokeStyle = "#FFF";
uiCtx.setLineDash([4, 2]);
uiCtx.strokeRect(bbvp.x, bbvp.y, bbvp.w, bbvp.h);
uiCtx.setLineDash([]);
// Draw Scaling/Rotation Origin
uiCtx.beginPath();
uiCtx.arc(
bbvp.x + bbvp.w / 2,
bbvp.y + bbvp.h / 2,
5,
0,
2 * Math.PI
);
uiCtx.stroke();
// Draw Scaling Handles
let cursorInHandle = false;
state.selected.handles().forEach((handle) => {
const bbvph = {
...viewport.canvasToView(handle.x, handle.y),
w: 10,
h: 10,
};
bbvph.x -= 5;
bbvph.y -= 5;
const inhandle =
evn.evn.clientX > bbvph.x &&
evn.evn.clientX < bbvph.x + bbvph.w &&
evn.evn.clientY > bbvph.y &&
evn.evn.clientY < bbvph.y + bbvph.h;
if (inhandle) {
cursorInHandle = true;
uiCtx.strokeRect(
bbvph.x - 1,
bbvph.y - 1,
bbvph.w + 2,
bbvph.h + 2
);
} else {
uiCtx.strokeRect(bbvph.x, bbvph.y, bbvph.w, bbvph.h);
}
});
// Change cursor
if (cursorInHandle || state.selected.contains(evn.x, evn.y))
if (
state.selected.hoveringBox(x, y) ||
state.selected.hoveringHandle(x, y, viewport.zoom).onHandle ||
state.selected.hoveringRotateHandle(x, y, viewport.zoom)
) {
imageCollection.inputElement.style.cursor = "pointer";
}
eraseSelectedImage = state.selected.drawImage(
state.originalDisplayLayer.ctx,
ovCtx,
{opacity: state.selectionPeekOpacity / 100}
);
}
// Draw current cursor location
state.erasePrevCursor = _tool._cursor_draw(x, y);
// Draw Selection
if (selection.exists) {
uiCtx.save();
uiCtx.setLineDash([2, 2]);
uiCtx.lineWidth = 2;
uiCtx.strokeStyle = "#FFF";
uiCtx.restore();
const bbvp = selection.bb.transform(viewport.c2v);
uiCtx.beginPath();
uiCtx.strokeRect(bbvp.x, bbvp.y, bbvp.w, bbvp.h);
uiCtx.stroke();
eraseSelection = () =>
uiCtx.clearRect(
bbvp.x - 10,
bbvp.y - 10,
bbvp.w + 20,
bbvp.h + 20
);
uiCtx.restore();
}
// Draw cursor
eraseCursor = _tool._cursor_draw(sx, sy);
};
// Handles left mouse clicks
state.clickcb = (evn) => {
if (
!state.original ||
(state.originalLayer === uil.layer &&
state.original.x === state.selected.x &&
state.original.y === state.selected.y &&
state.original.w === state.selected.w &&
state.original.h === state.selected.h)
state.selected &&
!(
state.selected.rotation === 0 &&
state.selected.scale.x === 1 &&
state.selected.scale.y === 1 &&
state.selected.position.x === state.original.sx &&
state.selected.position.y === state.original.sy &&
state.original.layer === uil.layer
)
) {
state.reset();
return;
}
// If something is selected, commit changes to the canvas
if (state.selected) {
state.originalLayer.ctx.drawImage(
state.selected.image,
// Put original image back
state.original.layer.ctx.drawImage(
state.selected.canvas,
state.original.x,
state.original.y
);
commands.runCommand("eraseImage", "Image Transform Erase", {
...state.original,
ctx: state.originalLayer.ctx,
// Erase Original Selection Area
commands.runCommand("eraseImage", "Transform Tool Erase", {
ctx: state.original.layer.ctx,
x: state.original.x,
y: state.original.y,
w: state.selected.canvas.width,
h: state.selected.canvas.height,
});
commands.runCommand("drawImage", "Image Transform Draw", {
image: state.selected.image,
x: Math.round(state.selected.x),
y: Math.round(state.selected.y),
w: Math.round(state.selected.w),
h: Math.round(state.selected.h),
// Draw Image
const {canvas, bb} = cropCanvas(state.originalDisplayLayer.canvas, {
border: 10,
});
commands.runCommand("drawImage", "Transform Tool Apply", {
image: canvas,
...bb,
});
state.reset(true);
} else {
state.reset();
}
};
// Handles left mouse drag start events
state.dragstartcb = (evn) => {
const {
x: ix,
y: iy,
sx: six,
sy: siy,
} = _tool._process_cursor({x: evn.ix, y: evn.iy}, state.snapToGrid);
const {x, y, sx, sy} = _tool._process_cursor(evn, state.snapToGrid);
if (state.selected) {
const hoveringBox = state.selected.hoveringBox(ix, iy);
const hoveringHandle = state.selected.hoveringHandle(
ix,
iy,
viewport.zoom
);
const hoveringRotateHandle = state.selected.hoveringRotateHandle(
ix,
iy,
viewport.zoom
);
if (hoveringBox) {
// Start dragging
moving = {
selected: state.selected.position,
offset: {
x: six - state.selected.position.x,
y: siy - state.selected.position.y,
},
};
return;
} else if (hoveringHandle.onHandle) {
// Start scaling
let handle = {x: 0, y: 0};
const lbb = new BoundingBox({
x: -state.selected.canvas.width / 2,
y: -state.selected.canvas.height / 2,
w: state.selected.canvas.width,
h: state.selected.canvas.height,
});
if (hoveringHandle.ontl) {
handle = lbb.tl;
} else if (hoveringHandle.ontr) {
handle = lbb.tr;
} else if (hoveringHandle.onbl) {
handle = lbb.bl;
} else {
handle = lbb.br;
}
scaling = {
handle,
};
return;
} else if (hoveringRotateHandle) {
rotating = true;
return;
}
}
selection.dragstartcb(evn);
};
const transform = (evn, x, y, sx, sy) => {
if (moving) {
state.selected.position = {
x: sx - moving.offset.x,
y: sy - moving.offset.y,
};
}
if (scaling) {
/** @type {DOMMatrix} */
const m = state.selected.rtmatrix.invertSelf();
const lscursor = m.transformPoint({x: sx, y: sy});
const xs = lscursor.x / scaling.handle.x;
const xy = lscursor.y / scaling.handle.y;
if (!state.keepAspectRatio) state.selected.scale = {x: xs, y: xy};
else {
const scale = Math.max(xs, xy);
state.selected.scale = {x: scale, y: scale};
}
}
if (rotating) {
const center = state.selected.matrix.transformPoint({x: 0, y: 0});
let angle = Math.atan2(x - center.x, center.y - y);
if (evn.evn.shiftKey)
angle =
config.rotationSnappingAngles.find(
(v) => Math.abs(v - angle) < config.rotationSnappingDistance
) ?? angle;
state.selected.rotation = angle;
}
};
// Handles left mouse drag events
state.dragstartcb = (evn) => {
let ix = evn.ix;
let iy = evn.iy;
if (state.snapToGrid) {
ix += snap(evn.ix, 0, 64);
iy += snap(evn.iy, 0, 64);
}
state.dragcb = (evn) => {
const {x, y, sx, sy} = _tool._process_cursor(evn, state.snapToGrid);
// If is selected, check if drag is in handles/body and act accordingly
if (state.selected) {
const handles = state.selected.handles();
if (state.selected) transform(evn, x, y, sx, sy);
const activeHandle = handles.find((v) => {
const vpc = viewport.canvasToView(v.x, v.y);
const tlc = viewport.viewToCanvas(vpc.x - 5, vpc.y - 5);
const brc = viewport.viewToCanvas(vpc.x + 5, vpc.y + 5);
const bb = {
x: tlc.x,
y: tlc.y,
w: brc.x - tlc.x,
h: brc.y - tlc.y,
};
return (
evn.ix > bb.x &&
evn.ix < bb.x + bb.w &&
evn.iy > bb.y &&
evn.iy < bb.y + bb.h
);
});
if (activeHandle) {
state.scaling = activeHandle;
return;
} else if (state.selected.contains(ix, iy)) {
state.moving = {
offset: {x: ix - state.selected.x, y: iy - state.selected.y},
};
return;
}
}
// If it is not, just create new selection
state.reset();
state.dragging = {ix, iy};
if (selection.exists) selection.dragcb(evn);
};
// Handles left mouse drag end events
state.dragendcb = (evn) => {
let x = evn.x;
let y = evn.y;
if (state.snapToGrid) {
x += snap(evn.x, 0, 64);
y += snap(evn.y, 0, 64);
const {x, y, sx, sy} = _tool._process_cursor(evn, state.snapToGrid);
if (selection.exists) {
selection.dragendcb(evn);
const bb = selection.bb;
state.reset();
if (selection.exists && bb.w !== 0 && bb.h !== 0) {
const canvas = document.createElement("canvas");
canvas.width = bb.w;
canvas.height = bb.h;
canvas
.getContext("2d")
.drawImage(
uil.canvas,
bb.x,
bb.y,
bb.w,
bb.h,
0,
0,
bb.w,
bb.h
);
uil.ctx.clearRect(bb.x, bb.y, bb.w, bb.h);
state.original = {
...bb,
sx: selection.bb.center.x,
sy: selection.bb.center.y,
layer: uil.layer,
};
state.selected = new _tool.MarqueeSelection(canvas, bb.center);
}
selection.deselect();
}
// If we are scaling, stop scaling and do some handler magic
if (state.scaling) {
state.selected.updateOriginal();
state.scaling = null;
// If we are moving the selection, just... stop
} else if (state.moving) {
state.moving = null;
/**
* If we are dragging, create a cutout selection area and save to an auxiliar image
* We will be rendering the image to the overlay, so it will not be noticeable
*/
} else if (state.dragging) {
state.original = selectionBB(
state.dragging.ix,
state.dragging.iy,
x,
y
);
state.selected = selectionBB(
state.dragging.ix,
state.dragging.iy,
x,
y
);
state.originalLayer = uil.layer;
state.originalDisplayLayer = imageCollection.registerLayer(null, {
after: uil.layer,
category: "select-display",
});
if (state.selected) transform(evn, x, y, sx, sy);
// Cut out selected portion of the image for manipulation
const cvs = document.createElement("canvas");
cvs.width = state.selected.w;
cvs.height = state.selected.h;
const ctx = cvs.getContext("2d");
moving = null;
scaling = null;
rotating = false;
ctx.drawImage(
uil.canvas,
state.selected.x,
state.selected.y,
state.selected.w,
state.selected.h,
0,
0,
state.selected.w,
state.selected.h
);
uil.ctx.clearRect(
state.selected.x,
state.selected.y,
state.selected.w,
state.selected.h
);
state.selected.image = cvs;
state.original.image = cvs;
if (state.selected.w === 0 || state.selected.h === 0)
state.selected = null;
state.dragging = null;
}
state.redraw();
};
@ -550,11 +471,11 @@ const selectTransformTool = () =>
ctx.clearRect(0, 0, state.selected.w, state.selected.h);
ctx.drawImage(
state.selected.image,
state.selected.canvas,
0,
0,
state.selected.image.width,
state.selected.image.height,
state.selected.canvas.width,
state.selected.canvas.height,
0,
0,
state.selected.w,
@ -684,7 +605,7 @@ const selectTransformTool = () =>
saveSelectionButton.onclick = () => {
downloadCanvas({
cropToContent: false,
canvas: state.selected.image,
canvas: state.selected.canvas,
});
};
@ -695,7 +616,7 @@ const selectTransformTool = () =>
createResourceButton.title = "Saves Selection as a Resource";
createResourceButton.onclick = () => {
const image = document.createElement("img");
image.src = state.selected.image.toDataURL();
image.src = state.selected.canvas.toDataURL();
image.onload = () => {
tools.stamp.state.addResource("Selection Resource", image);
tools.stamp.enable();

View file

@ -1,3 +1,41 @@
/**
* Generic wheel handler
*/
let _stamp_wheel_accum = 0;
const _stamp_onwheel = (evn, state) => {
if (evn.mode !== WheelEvent.DOM_DELTA_PIXEL) {
// We don't really handle non-pixel scrolling
return;
}
let delta = evn.delta;
if (evn.evn.shiftKey) delta *= 0.01;
// A simple but (I hope) effective fix for mouse wheel behavior
_stamp_wheel_accum += delta;
if (
!evn.evn.shiftKey &&
Math.abs(_stamp_wheel_accum) > config.wheelTickSize
) {
// Snap to next or previous position
const v =
state.scale - 0.1 * (_stamp_wheel_accum / Math.abs(_stamp_wheel_accum));
state.setScale(v + snap(v, 0, 0.1));
state.redraw(evn);
_stamp_wheel_accum = 0; // Zero accumulation
} else if (evn.evn.shiftKey && Math.abs(_stamp_wheel_accum) >= 1) {
const v = state.scale - _stamp_wheel_accum * 0.01;
state.setScale(v);
state.redraw(evn);
_stamp_wheel_accum = 0; // Zero accumulation
}
};
const stampTool = () =>
toolbar.registerTool(
"./res/icons/file-up.svg",
@ -14,6 +52,12 @@ const stampTool = () =>
mouse.listen.world.btn.left.onclick.on(state.drawcb);
mouse.listen.world.btn.right.onclick.on(state.cancelcb);
mouse.listen.world.btn.left.ondragstart.on(state.dragstartcb);
mouse.listen.world.btn.left.ondrag.on(state.dragcb);
mouse.listen.world.btn.left.ondragend.on(state.dragendcb);
mouse.listen.world.onwheel.on(state.onwheelcb);
// For calls from other tools to paste image
if (opt && opt.image) {
state.addResource(
@ -41,6 +85,12 @@ const stampTool = () =>
mouse.listen.world.btn.left.onclick.clear(state.drawcb);
mouse.listen.world.btn.right.onclick.clear(state.cancelcb);
mouse.listen.world.btn.left.ondragstart.clear(state.dragstartcb);
mouse.listen.world.btn.left.ondrag.clear(state.dragcb);
mouse.listen.world.btn.left.ondragend.clear(state.dragendcb);
mouse.listen.world.onwheel.clear(state.onwheelcb);
ovLayer.clear();
},
{
@ -54,7 +104,15 @@ const stampTool = () =>
state.lastMouseMove = {x: 0, y: 0};
state.block_res_change = true;
// Current Rotation
let rotation = 0;
let rotating = null;
// Current Scale
state.scale = 1;
state.selectResource = (resource, nolock = true, deselect = true) => {
rotation = 0;
state.setScale(1);
if (nolock && state.ctxmenu.uploadButton.disabled) return;
console.debug(
@ -290,32 +348,65 @@ const stampTool = () =>
syncResources();
};
state.movecb = (evn) => {
let x = evn.x;
let y = evn.y;
if (state.snapToGrid) {
x += snap(evn.x, 0, 64);
y += snap(evn.y, 0, 64);
state.onwheelcb = (evn) => {
_stamp_onwheel(evn, state);
};
state.dragstartcb = (evn) => {
const {x, y, sx, sy} = _tool._process_cursor(evn, state.snapToGrid);
rotating = {x: sx, y: sy};
};
state.dragcb = (evn) => {
if (rotating) {
rotation = Math.atan2(rotating.x - evn.x, evn.y - rotating.y);
if (evn.evn.shiftKey)
rotation =
config.rotationSnappingAngles.find(
(v) =>
Math.abs(v - rotation) < config.rotationSnappingDistance
) ?? rotation;
}
};
const vpc = viewport.canvasToView(x, y);
uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
state.erasePrevCursor && state.erasePrevCursor();
state.dragendcb = (evn) => {
rotating = null;
};
uiCtx.save();
let erasePrevCursor = () => null;
state.movecb = (evn) => {
const {x, y, sx, sy} = _tool._process_cursor(evn, state.snapToGrid);
// Erase Previous Cursors
erasePrevCursor();
state.lastMouseMove = evn;
ovLayer.clear();
let px = sx;
let py = sy;
if (rotating) {
px = rotating.x;
py = rotating.y;
}
// Draw selected image
if (state.selected) {
ovCtx.drawImage(state.selected.image, x, y);
ovCtx.save();
ovCtx.translate(px, py);
ovCtx.scale(state.scale, state.scale);
ovCtx.rotate(rotation);
ovCtx.drawImage(state.selected.image, 0, 0);
ovCtx.restore();
}
// Draw current cursor location
state.erasePrevCursor = _tool._cursor_draw(x, y);
uiCtx.restore();
erasePrevCursor = _tool._cursor_draw(px, py);
};
state.redraw = () => {
@ -323,20 +414,16 @@ const stampTool = () =>
};
state.drawcb = (evn) => {
let x = evn.x;
let y = evn.y;
if (state.snapToGrid) {
x += snap(evn.x, 0, 64);
y += snap(evn.y, 0, 64);
}
const {x, y, sx, sy} = _tool._process_cursor(evn, state.snapToGrid);
const resource = state.selected;
if (resource) {
const {canvas, bb} = cropCanvas(ovCanvas, {border: 10});
commands.runCommand("drawImage", "Image Stamp", {
image: resource.image,
x,
y,
image: canvas,
x: bb.x,
y: bb.y,
});
if (resource.temporary) {
@ -380,6 +467,16 @@ const stampTool = () =>
);
state.ctxmenu.snapToGridLabel = array;
// Scale Slider
const scaleSlider = _toolbar_input.slider(state, "scale", "Scale", {
min: 0.01,
max: 10,
step: 0.1,
textStep: 0.001,
});
state.ctxmenu.scaleSlider = scaleSlider.slider;
state.setScale = scaleSlider.setValue;
// Create resource list
const uploadButtonId = `upload-btn-${guid()}`;
@ -528,6 +625,7 @@ const stampTool = () =>
},
populateContextMenu: (menu, state) => {
menu.appendChild(state.ctxmenu.snapToGridLabel);
menu.appendChild(state.ctxmenu.scaleSlider);
menu.appendChild(state.ctxmenu.resourceManager);
},
shortcut: "U",

View file

@ -5,15 +5,15 @@
<title>openOutpaint 🐠</title>
<!-- CSS Variables -->
<link href="../css/colors.css?v=3f81e80" rel="stylesheet" />
<link href="../css/icons.css?v=caa702e" rel="stylesheet" />
<link href="../css/icons.css?v=9ae0466" rel="stylesheet" />
<link href="../css/index.css?v=69d3b9e" rel="stylesheet" />
<link href="../css/layers.css?v=b4fbf61" rel="stylesheet" />
<link href="../css/index.css?v=5b8d4d6" rel="stylesheet" />
<link href="../css/layers.css?v=92c0352" rel="stylesheet" />
<link href="../css/ui/generic.css?v=4b9afe2" rel="stylesheet" />
<link href="../css/ui/generic.css?v=802bd41" rel="stylesheet" />
<link href="../css/ui/history.css?v=0b03861" rel="stylesheet" />
<link href="../css/ui/layers.css?v=4fd95fe" rel="stylesheet" />
<link href="../css/ui/layers.css?v=ae472cd" rel="stylesheet" />
<link href="../css/ui/toolbar.css?v=109c78f" rel="stylesheet" />
<!-- Tool Specific CSS -->
@ -84,11 +84,12 @@
step="0.1"
value="30.0" />
</label>
<!-- <p>Refresh the page to apply aabove.</p> -->
<hr />
<label style="display: flex">
Lie to HRfix:
<input id="hrfix-liar" class="canvas-size-input" type="checkbox" />
</label>
<p>Refresh the page to apply settings.</p>
<script>
const canvasWidth = document.getElementById("canvas-width");
@ -127,8 +128,11 @@
localStorage.getItem("openoutpaint/settings.min-cfg") || -1;
maxCfg.value =
localStorage.getItem("openoutpaint/settings.max-cfg") || 30;
hrfixLiar.checked =
localStorage.getItem("openoutpaint/settings.hrfix-liar") || true;
let _enable_dishonesty =
localStorage.getItem("openoutpaint/settings.hrfix-liar") === null
? true
: localStorage.getItem("openoutpaint/settings.hrfix-liar") === "true";
hrfixLiar.checked = _enable_dishonesty;
writeToLocalStorage();

View file

@ -8,8 +8,8 @@
<iframe
id="openoutpaint"
style="width: 100%; height: 800px"
src="../index.html?v=95a96ad"
src="../index.html?v=95a96ad"
src="../index.html?v=daf18de"
src="../index.html?v=daf18de"
frameborder="0"></iframe>
<button id="add-res">Add Resource</button>
<script>