commit
ec4054529c
16 changed files with 983 additions and 477 deletions
|
@ -17,7 +17,9 @@
|
||||||
bottom: 15%;
|
bottom: 15%;
|
||||||
|
|
||||||
mask-size: contain;
|
mask-size: contain;
|
||||||
|
-webkit-mask-size: contain;
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
|
-webkit-mask-repeat: no-repeat;
|
||||||
|
|
||||||
max-height: 70%;
|
max-height: 70%;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
|
|
||||||
.layer-render-target .collection {
|
.layer-render-target .collection {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
transform-origin: 0px 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layer-render-target .collection > .collection-input-overlay {
|
.layer-render-target .collection > .collection-input-overlay {
|
||||||
|
@ -45,7 +46,7 @@
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#layer-overlay {
|
.overlay-canvas {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
|
@ -231,6 +231,7 @@ div.autocomplete > .autocomplete-list {
|
||||||
}
|
}
|
||||||
|
|
||||||
div.autocomplete > .autocomplete-list > .autocomplete-option {
|
div.autocomplete > .autocomplete-list > .autocomplete-option {
|
||||||
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
|
@ -158,7 +158,9 @@
|
||||||
|
|
||||||
background-color: #293d3d77;
|
background-color: #293d3d77;
|
||||||
|
|
||||||
|
-webkit-mask-image: url("../../res/icons/chevron-up.svg");
|
||||||
mask-image: url("../../res/icons/chevron-up.svg");
|
mask-image: url("../../res/icons/chevron-up.svg");
|
||||||
|
-webkit-mask-size: contain;
|
||||||
mask-size: contain;
|
mask-size: contain;
|
||||||
|
|
||||||
width: 60px;
|
width: 60px;
|
||||||
|
|
32
index.html
32
index.html
|
@ -5,15 +5,15 @@
|
||||||
<title>openOutpaint 🐠</title>
|
<title>openOutpaint 🐠</title>
|
||||||
<!-- CSS Variables -->
|
<!-- CSS Variables -->
|
||||||
<link href="css/colors.css?v=3f81e80" rel="stylesheet" />
|
<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=5b8d4d6" rel="stylesheet" />
|
<link href="css/index.css?v=5b8d4d6" rel="stylesheet" />
|
||||||
<link href="css/layers.css?v=b4fbf61" 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/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" />
|
<link href="css/ui/toolbar.css?v=109c78f" rel="stylesheet" />
|
||||||
|
|
||||||
<!-- Tool Specific CSS -->
|
<!-- Tool Specific CSS -->
|
||||||
|
@ -49,6 +49,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<!-- Prompts section -->
|
<!-- Prompts section -->
|
||||||
<button type="button" class="collapsible">Prompts</button>
|
<button type="button" class="collapsible">Prompts</button>
|
||||||
<div class="content prompt">
|
<div class="content prompt">
|
||||||
|
@ -319,7 +320,8 @@
|
||||||
<div id="layer-render" class="layer-render-target"></div>
|
<div id="layer-render" class="layer-render-target"></div>
|
||||||
|
|
||||||
<!-- Overlay -->
|
<!-- 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 -->
|
<!-- Page Overlay -->
|
||||||
<div id="page-overlay-wrapper" class="page-overlay invisible">
|
<div id="page-overlay-wrapper" class="page-overlay invisible">
|
||||||
|
@ -331,7 +333,7 @@
|
||||||
<div class="ui separator"></div>
|
<div class="ui separator"></div>
|
||||||
<iframe
|
<iframe
|
||||||
id="page-overlay"
|
id="page-overlay"
|
||||||
src="pages/configuration.html?v=3d710ce"></iframe>
|
src="pages/configuration.html?v=7fca00b"></iframe>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -339,21 +341,21 @@
|
||||||
<script src="js/global.js?v=3a1cde6" type="text/javascript"></script>
|
<script src="js/global.js?v=3a1cde6" type="text/javascript"></script>
|
||||||
|
|
||||||
<!-- Base Libs -->
|
<!-- 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/events.js?v=2ab7933" type="text/javascript"></script>
|
||||||
<script src="js/lib/input.js?v=09298ac" 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/layers.js?v=a1f8aea" type="text/javascript"></script>
|
||||||
<script src="js/lib/commands.js?v=bf23c83" 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/lib/ui.js?v=76ede2b" type="text/javascript"></script>
|
||||||
|
|
||||||
<script
|
<script
|
||||||
src="js/initalize/layers.populate.js?v=c81f0a5"
|
src="js/initalize/layers.populate.js?v=39785ac"
|
||||||
type="text/javascript"></script>
|
type="text/javascript"></script>
|
||||||
|
|
||||||
<!-- Configuration -->
|
<!-- Configuration -->
|
||||||
<script src="js/config.js?v=8da6a43" type="text/javascript"></script>
|
<script src="js/config.js?v=e0345e0" type="text/javascript"></script>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<script src="js/prompt.js?v=7a1c68c" type="text/javascript"></script>
|
<script src="js/prompt.js?v=7a1c68c" type="text/javascript"></script>
|
||||||
|
@ -368,20 +370,20 @@
|
||||||
|
|
||||||
<!-- Load Tools -->
|
<!-- Load Tools -->
|
||||||
<script
|
<script
|
||||||
src="js/ui/tool/generic.js?v=2bcd36d"
|
src="js/ui/tool/generic.js?v=3e678e0"
|
||||||
type="text/javascript"></script>
|
type="text/javascript"></script>
|
||||||
|
|
||||||
<script src="js/ui/tool/dream.js?v=f18c203" type="text/javascript"></script>
|
<script src="js/ui/tool/dream.js?v=eb98dc9" type="text/javascript"></script>
|
||||||
<script
|
<script
|
||||||
src="js/ui/tool/maskbrush.js?v=1e8a893"
|
src="js/ui/tool/maskbrush.js?v=1e8a893"
|
||||||
type="text/javascript"></script>
|
type="text/javascript"></script>
|
||||||
<script
|
<script
|
||||||
src="js/ui/tool/colorbrush.js?v=8acb4f6"
|
src="js/ui/tool/colorbrush.js?v=3f8c01a"
|
||||||
type="text/javascript"></script>
|
type="text/javascript"></script>
|
||||||
<script
|
<script
|
||||||
src="js/ui/tool/select.js?v=ade791e"
|
src="js/ui/tool/select.js?v=e27bbdf"
|
||||||
type="text/javascript"></script>
|
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
|
<script
|
||||||
src="js/ui/tool/interrogate.js?v=e579ff1"
|
src="js/ui/tool/interrogate.js?v=e579ff1"
|
||||||
type="text/javascript"></script>
|
type="text/javascript"></script>
|
||||||
|
|
28
js/config.js
28
js/config.js
|
@ -5,9 +5,37 @@
|
||||||
*/
|
*/
|
||||||
const config = makeReadOnly(
|
const config = makeReadOnly(
|
||||||
{
|
{
|
||||||
|
// Grid Size
|
||||||
|
gridSize: 64,
|
||||||
|
|
||||||
// Scroll Tick Limit (How much must scroll to reach next tick)
|
// Scroll Tick Limit (How much must scroll to reach next tick)
|
||||||
wheelTickSize: 50,
|
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
|
// Endpoint
|
||||||
api: makeReadOnly({path: "/sdapi/v1/"}),
|
api: makeReadOnly({path: "/sdapi/v1/"}),
|
||||||
},
|
},
|
||||||
|
|
|
@ -160,61 +160,72 @@ debugLayer.hide(); // Hidden by default
|
||||||
* The global viewport object (may be modularized in the future). All
|
* The global viewport object (may be modularized in the future). All
|
||||||
* coordinates given are of the center of the viewport
|
* coordinates given are of the center of the viewport
|
||||||
*
|
*
|
||||||
* cx and cy are the viewport's world coordinates, scaled to zoom level.
|
* cx and cy are the viewport's world coordinates.
|
||||||
* _x and _y are actual coordinates in the DOM space
|
|
||||||
*
|
*
|
||||||
* The transform() function does some transforms and writes them to the
|
* The transform() function does some transforms and writes them to the
|
||||||
* provided element.
|
* provided element.
|
||||||
*/
|
*/
|
||||||
const viewport = {
|
class Viewport {
|
||||||
get cx() {
|
cx = 0;
|
||||||
return this._x * this.zoom;
|
cy = 0;
|
||||||
},
|
|
||||||
|
|
||||||
set cx(v) {
|
zoom = 1;
|
||||||
return (this._x = v / this.zoom);
|
|
||||||
},
|
/**
|
||||||
_x: 0,
|
* Gets viewport width in canvas coordinates
|
||||||
get cy() {
|
*/
|
||||||
return this._y * this.zoom;
|
|
||||||
},
|
|
||||||
set cy(v) {
|
|
||||||
return (this._y = v / this.zoom);
|
|
||||||
},
|
|
||||||
_y: 0,
|
|
||||||
zoom: 1,
|
|
||||||
rotation: 0,
|
|
||||||
get w() {
|
get w() {
|
||||||
return (window.innerWidth * 1) / this.zoom;
|
return window.innerWidth * this.zoom;
|
||||||
},
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets viewport height in canvas coordinates
|
||||||
|
*/
|
||||||
get h() {
|
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) {
|
viewToCanvas(x, y) {
|
||||||
return {
|
if (x.x !== undefined) return this.v2c.transformPoint(x);
|
||||||
x: this.cx + this.w * (x / window.innerWidth - 0.5),
|
return this.v2c.transformPoint({x, y});
|
||||||
y: this.cy + this.h * (y / window.innerHeight - 0.5),
|
}
|
||||||
};
|
|
||||||
},
|
|
||||||
canvasToView(x, y) {
|
canvasToView(x, y) {
|
||||||
return {
|
if (x.x !== undefined) return this.c2v.transformPoint(x);
|
||||||
x: window.innerWidth * ((x - this.cx) / this.w) + window.innerWidth / 2,
|
return this.c2v.transformPoint({x, y});
|
||||||
y: window.innerHeight * ((y - this.cy) / this.h) + window.innerHeight / 2,
|
}
|
||||||
};
|
|
||||||
},
|
|
||||||
/**
|
/**
|
||||||
* Apply transformation
|
* Apply transformation
|
||||||
*
|
*
|
||||||
* @param {HTMLElement} el Element to apply CSS transform to
|
* @param {HTMLElement} el Element to apply CSS transform to
|
||||||
*/
|
*/
|
||||||
transform(el) {
|
transform(el) {
|
||||||
el.style.transformOrigin = `${this.cx}px ${this.cy}px`;
|
el.style.transformOrigin = "0px 0px";
|
||||||
el.style.transform = `scale(${this.zoom}) translate(${-(
|
el.style.transform = this.c2v;
|
||||||
this._x -
|
}
|
||||||
this.w / 2
|
}
|
||||||
)}px, ${-(this._y - this.h / 2)}px)`;
|
|
||||||
},
|
const viewport = new Viewport(0, 0);
|
||||||
};
|
|
||||||
|
|
||||||
viewport.cx = imageCollection.size.w / 2;
|
viewport.cx = imageCollection.size.w / 2;
|
||||||
viewport.cy = imageCollection.size.h / 2;
|
viewport.cy = imageCollection.size.h / 2;
|
||||||
|
@ -296,7 +307,7 @@ mouse.listen.camera.onwheel.on((evn) => {
|
||||||
const pcy = viewport.cy;
|
const pcy = viewport.cy;
|
||||||
|
|
||||||
// Apply zoom
|
// Apply zoom
|
||||||
viewport.zoom *= 1 - evn.delta * 0.0002;
|
viewport.zoom *= 1 + evn.delta * 0.0002;
|
||||||
|
|
||||||
// Apply normal zoom (center of viewport)
|
// Apply normal zoom (center of viewport)
|
||||||
viewport.cx = pcx;
|
viewport.cx = pcx;
|
||||||
|
@ -305,13 +316,13 @@ mouse.listen.camera.onwheel.on((evn) => {
|
||||||
viewport.transform(imageCollection.element);
|
viewport.transform(imageCollection.element);
|
||||||
|
|
||||||
// Calculate new viewport center and move
|
// Calculate new viewport center and move
|
||||||
const newCursorPosition = viewport.viewToCanvas(evn.x, evn.y);
|
//const newCursorPosition = viewport.viewToCanvas(evn.x, evn.y);
|
||||||
viewport.cx = pcx - (newCursorPosition.x - cursorPosition.x);
|
//viewport.cx = pcx - (newCursorPosition.x - cursorPosition.x);
|
||||||
viewport.cy = pcy - (newCursorPosition.y - cursorPosition.y);
|
//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) => {
|
const cameraPaintStart = (evn) => {
|
||||||
|
@ -320,8 +331,8 @@ const cameraPaintStart = (evn) => {
|
||||||
|
|
||||||
const cameraPaint = (evn) => {
|
const cameraPaint = (evn) => {
|
||||||
if (worldInit) {
|
if (worldInit) {
|
||||||
viewport.cx = worldInit.x + (evn.ix - evn.x) / viewport.zoom;
|
viewport.cx = worldInit.x + (evn.ix - evn.x) * viewport.zoom;
|
||||||
viewport.cy = worldInit.y + (evn.iy - evn.y) / viewport.zoom;
|
viewport.cy = worldInit.y + (evn.iy - evn.y) * viewport.zoom;
|
||||||
|
|
||||||
// Limits
|
// Limits
|
||||||
viewport.cx = Math.max(
|
viewport.cx = Math.max(
|
||||||
|
@ -337,6 +348,9 @@ const cameraPaint = (evn) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
viewport.transform(imageCollection.element);
|
viewport.transform(imageCollection.element);
|
||||||
|
toolbar._current_tool.state.redrawui &&
|
||||||
|
toolbar._current_tool.state.redrawui();
|
||||||
|
|
||||||
if (global.debug) {
|
if (global.debug) {
|
||||||
debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height);
|
debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height);
|
||||||
debugCtx.fillStyle = "#F0F";
|
debugCtx.fillStyle = "#F0F";
|
||||||
|
|
|
@ -90,14 +90,20 @@ const toolbar = {
|
||||||
name: toolname,
|
name: toolname,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
_element: null,
|
_element: null,
|
||||||
state: {},
|
state: {
|
||||||
|
redrawui: () => tool.state.redraw && tool.state.redraw(),
|
||||||
|
},
|
||||||
options,
|
options,
|
||||||
/**
|
/**
|
||||||
* If the tool has a redraw() function in its state, then run it
|
* If the tool has a redraw() function in its state, then run it
|
||||||
*/
|
*/
|
||||||
redraw: () => {
|
redraw: () => {
|
||||||
|
tool.state.redrawui && tool.state.redrawui();
|
||||||
tool.state.redraw && tool.state.redraw();
|
tool.state.redraw && tool.state.redraw();
|
||||||
},
|
},
|
||||||
|
redrawui: () => {
|
||||||
|
tool.state.redrawui && tool.state.redrawui();
|
||||||
|
},
|
||||||
enable: (opt = null) => {
|
enable: (opt = null) => {
|
||||||
if (toolbar._locked) return;
|
if (toolbar._locked) return;
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,31 @@ class BoundingBox {
|
||||||
w = 0;
|
w = 0;
|
||||||
h = 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}) {
|
constructor({x, y, w, h} = {x: 0, y: 0, w: 0, h: 0}) {
|
||||||
this.x = x;
|
this.x = x;
|
||||||
this.y = y;
|
this.y = y;
|
||||||
|
@ -64,6 +89,18 @@ class BoundingBox {
|
||||||
h: maxy - miny,
|
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})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -176,7 +176,7 @@ const colorBrushTool = () =>
|
||||||
uiCtx.arc(
|
uiCtx.arc(
|
||||||
vcp.x,
|
vcp.x,
|
||||||
vcp.y,
|
vcp.y,
|
||||||
(state.eyedropper ? 50 : state.brushSize / 2) * viewport.zoom,
|
(state.eyedropper ? 50 : state.brushSize / 2) / viewport.zoom,
|
||||||
0,
|
0,
|
||||||
2 * Math.PI,
|
2 * Math.PI,
|
||||||
true
|
true
|
||||||
|
@ -197,7 +197,7 @@ const colorBrushTool = () =>
|
||||||
uiCtx.arc(
|
uiCtx.arc(
|
||||||
vcp.x,
|
vcp.x,
|
||||||
vcp.y,
|
vcp.y,
|
||||||
(state.brushSize / 2) * viewport.zoom,
|
state.brushSize / (2 * viewport.zoom),
|
||||||
0,
|
0,
|
||||||
2 * Math.PI,
|
2 * Math.PI,
|
||||||
true
|
true
|
||||||
|
|
|
@ -1241,10 +1241,16 @@ const _dream_onwheel = (evn, state) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A simple but (I hope) effective fix for mouse wheel behavior
|
let delta = evn.delta;
|
||||||
_dream_wheel_accum += 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
|
// Snap to next or previous position
|
||||||
const v =
|
const v =
|
||||||
state.cursorSize -
|
state.cursorSize -
|
||||||
|
@ -1253,6 +1259,12 @@ const _dream_onwheel = (evn, state) => {
|
||||||
state.cursorSize = state.setCursorSize(v + snap(v, 0, 128));
|
state.cursorSize = state.setCursorSize(v + snap(v, 0, 128));
|
||||||
state.mousemovecb(evn);
|
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
|
_dream_wheel_accum = 0; // Zero accumulation
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1942,11 +1954,10 @@ const img2imgTool = () =>
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bbvp = {
|
const bbvp = BoundingBox.fromStartEnd(
|
||||||
...viewport.canvasToView(bb.x, bb.y),
|
viewport.canvasToView(bb.tl),
|
||||||
w: viewport.zoom * bb.w,
|
viewport.canvasToView(bb.br)
|
||||||
h: viewport.zoom * bb.h,
|
);
|
||||||
};
|
|
||||||
|
|
||||||
// For displaying border mask
|
// For displaying border mask
|
||||||
const bbCanvas = document.createElement("canvas");
|
const bbCanvas = document.createElement("canvas");
|
||||||
|
|
|
@ -27,11 +27,7 @@ const _tool = {
|
||||||
reticleStyle: global.hasActiveInput ? "#BBF" : "#FFF",
|
reticleStyle: global.hasActiveInput ? "#BBF" : "#FFF",
|
||||||
});
|
});
|
||||||
|
|
||||||
const bbvp = {
|
const bbvp = bb.transform(viewport.c2v);
|
||||||
...viewport.canvasToView(bb.x, bb.y),
|
|
||||||
w: viewport.zoom * bb.w,
|
|
||||||
h: viewport.zoom * bb.h,
|
|
||||||
};
|
|
||||||
|
|
||||||
uiCtx.save();
|
uiCtx.save();
|
||||||
|
|
||||||
|
@ -174,6 +170,8 @@ const _tool = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the selection bounding box
|
* Gets the selection bounding box
|
||||||
|
*
|
||||||
|
* @returns {BoundingBox}
|
||||||
*/
|
*/
|
||||||
get bb() {
|
get bb() {
|
||||||
if (this._dirty_bb && this._selected) {
|
if (this._dirty_bb && this._selected) {
|
||||||
|
@ -273,4 +271,389 @@ const _tool = {
|
||||||
|
|
||||||
return selection;
|
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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,6 +11,7 @@ const selectTransformTool = () =>
|
||||||
mouse.listen.world.onmousemove.on(state.movecb);
|
mouse.listen.world.onmousemove.on(state.movecb);
|
||||||
mouse.listen.world.btn.left.onclick.on(state.clickcb);
|
mouse.listen.world.btn.left.onclick.on(state.clickcb);
|
||||||
mouse.listen.world.btn.left.ondragstart.on(state.dragstartcb);
|
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.btn.left.ondragend.on(state.dragendcb);
|
||||||
|
|
||||||
// Canvas right mouse handler
|
// Canvas right mouse handler
|
||||||
|
@ -29,12 +30,19 @@ const selectTransformTool = () =>
|
||||||
keyboard.onShortcut({ctrl: true, key: "KeyX"}, state.ctrlxcb);
|
keyboard.onShortcut({ctrl: true, key: "KeyX"}, state.ctrlxcb);
|
||||||
|
|
||||||
state.selected = null;
|
state.selected = null;
|
||||||
|
|
||||||
|
// Register Layer
|
||||||
|
state.originalDisplayLayer = imageCollection.registerLayer(null, {
|
||||||
|
after: uil.layer,
|
||||||
|
category: "select-display",
|
||||||
|
});
|
||||||
},
|
},
|
||||||
(state, opt) => {
|
(state, opt) => {
|
||||||
// Clear all those listeners and shortcuts we set up
|
// Clear all those listeners and shortcuts we set up
|
||||||
mouse.listen.world.onmousemove.clear(state.movecb);
|
mouse.listen.world.onmousemove.clear(state.movecb);
|
||||||
mouse.listen.world.btn.left.onclick.clear(state.clickcb);
|
mouse.listen.world.btn.left.onclick.clear(state.clickcb);
|
||||||
mouse.listen.world.btn.left.ondragstart.clear(state.dragstartcb);
|
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.left.ondragend.clear(state.dragendcb);
|
||||||
|
|
||||||
mouse.listen.world.btn.right.onclick.clear(state.cancelcb);
|
mouse.listen.world.btn.right.onclick.clear(state.cancelcb);
|
||||||
|
@ -55,6 +63,10 @@ const selectTransformTool = () =>
|
||||||
|
|
||||||
// Clears overlay
|
// Clears overlay
|
||||||
imageCollection.inputElement.style.cursor = "auto";
|
imageCollection.inputElement.style.cursor = "auto";
|
||||||
|
|
||||||
|
// Delete Layer
|
||||||
|
imageCollection.deleteLayer(state.originalDisplayLayer);
|
||||||
|
state.originalDisplayLayer = null;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
init: (state) => {
|
init: (state) => {
|
||||||
|
@ -69,7 +81,6 @@ const selectTransformTool = () =>
|
||||||
state.selectionPeekOpacity = 40;
|
state.selectionPeekOpacity = 40;
|
||||||
|
|
||||||
state.original = null;
|
state.original = null;
|
||||||
state.dragging = null;
|
|
||||||
state._selected = null;
|
state._selected = null;
|
||||||
Object.defineProperty(state, "selected", {
|
Object.defineProperty(state, "selected", {
|
||||||
get: () => state._selected,
|
get: () => state._selected,
|
||||||
|
@ -80,7 +91,6 @@ const selectTransformTool = () =>
|
||||||
return (state._selected = v);
|
return (state._selected = v);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
state.moving = null;
|
|
||||||
|
|
||||||
// Some things to easy request for a redraw
|
// Some things to easy request for a redraw
|
||||||
state.lastMouseTarget = null;
|
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
|
// Clears selection and make things right
|
||||||
state.reset = (erase = false) => {
|
state.reset = (erase = false) => {
|
||||||
if (state.selected && !erase)
|
if (state.selected && !erase)
|
||||||
state.originalLayer.ctx.drawImage(
|
state.original.layer.ctx.drawImage(
|
||||||
state.original.image,
|
state.selected.canvas,
|
||||||
state.original.x,
|
state.original.x,
|
||||||
state.original.y
|
state.original.y
|
||||||
);
|
);
|
||||||
|
|
||||||
if (state.originalDisplayLayer) {
|
if (state.originalDisplayLayer) {
|
||||||
imageCollection.deleteLayer(state.originalDisplayLayer);
|
state.originalDisplayLayer.clear();
|
||||||
state.originalDisplayLayer = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.dragging) state.dragging = null;
|
if (state.dragging) state.dragging = null;
|
||||||
else state.selected = null;
|
else state.selected = null;
|
||||||
|
|
||||||
|
state.rotation = 0;
|
||||||
|
state.original = null;
|
||||||
|
moving = null;
|
||||||
|
scaling = null;
|
||||||
|
rotating = null;
|
||||||
|
|
||||||
state.redraw();
|
state.redraw();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Selection bounding box object. Has some witchery to deal with handles.
|
// Selection Handlers
|
||||||
const selectionBB = (x1, y1, x2, y2) => {
|
const selection = _tool._draggable_selection(state);
|
||||||
x1 = Math.round(x1);
|
|
||||||
y1 = Math.round(y1);
|
// UI Erasers
|
||||||
x2 = Math.round(x2);
|
let eraseSelectedBox = () => null;
|
||||||
y2 = Math.round(y2);
|
let eraseSelectedImage = () => null;
|
||||||
return {
|
let eraseCursor = () => null;
|
||||||
original: {
|
let eraseSelection = () => null;
|
||||||
x: Math.min(x1, x2),
|
|
||||||
y: Math.min(y1, y2),
|
// Redraw UI
|
||||||
w: Math.abs(x1 - x2),
|
state.redrawui = () => {
|
||||||
h: Math.abs(y1 - y2),
|
// Get cursor positions
|
||||||
},
|
const {x, y, sx, sy} = _tool._process_cursor(
|
||||||
x: Math.min(x1, x2),
|
state.lastMouseMove,
|
||||||
y: Math.min(y1, y2),
|
state.snapToGrid
|
||||||
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;
|
|
||||||
|
|
||||||
let xRatio = (nx - origin.x) / (x - origin.x);
|
eraseSelectedBox();
|
||||||
let yRatio = (ny - origin.y) / (y - origin.y);
|
|
||||||
if (keepAspectRatio)
|
|
||||||
xRatio = yRatio = Math.min(xRatio, yRatio);
|
|
||||||
|
|
||||||
if (Number.isFinite(xRatio)) {
|
|
||||||
let left = this.original.x;
|
|
||||||
let right = this.original.x + this.original.w;
|
|
||||||
|
|
||||||
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([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.selected) {
|
if (state.selected) {
|
||||||
ovCtx.lineWidth = 1;
|
eraseSelectedBox = state.selected.drawBox(
|
||||||
ovCtx.strokeStyle = "#FFF";
|
uiCtx,
|
||||||
|
{x, y},
|
||||||
const bb = {
|
viewport.c2v
|
||||||
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
|
|
||||||
);
|
);
|
||||||
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
|
|
||||||
);
|
|
||||||
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
|
// Mouse Move Handler
|
||||||
if (cursorInHandle || state.selected.contains(evn.x, evn.y))
|
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
|
||||||
|
);
|
||||||
|
|
||||||
|
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";
|
imageCollection.inputElement.style.cursor = "pointer";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw current cursor location
|
eraseSelectedImage = state.selected.drawImage(
|
||||||
state.erasePrevCursor = _tool._cursor_draw(x, y);
|
state.originalDisplayLayer.ctx,
|
||||||
|
ovCtx,
|
||||||
|
{opacity: state.selectionPeekOpacity / 100}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw Selection
|
||||||
|
if (selection.exists) {
|
||||||
|
uiCtx.save();
|
||||||
|
uiCtx.setLineDash([2, 2]);
|
||||||
|
uiCtx.lineWidth = 2;
|
||||||
|
uiCtx.strokeStyle = "#FFF";
|
||||||
|
|
||||||
|
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();
|
uiCtx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw cursor
|
||||||
|
eraseCursor = _tool._cursor_draw(sx, sy);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handles left mouse clicks
|
// Handles left mouse clicks
|
||||||
state.clickcb = (evn) => {
|
state.clickcb = (evn) => {
|
||||||
if (
|
if (
|
||||||
!state.original ||
|
state.selected &&
|
||||||
(state.originalLayer === uil.layer &&
|
!(
|
||||||
state.original.x === state.selected.x &&
|
state.selected.rotation === 0 &&
|
||||||
state.original.y === state.selected.y &&
|
state.selected.scale.x === 1 &&
|
||||||
state.original.w === state.selected.w &&
|
state.selected.scale.y === 1 &&
|
||||||
state.original.h === state.selected.h)
|
state.selected.position.x === state.original.sx &&
|
||||||
|
state.selected.position.y === state.original.sy &&
|
||||||
|
state.original.layer === uil.layer
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
state.reset();
|
// Put original image back
|
||||||
return;
|
state.original.layer.ctx.drawImage(
|
||||||
}
|
state.selected.canvas,
|
||||||
|
|
||||||
// If something is selected, commit changes to the canvas
|
|
||||||
if (state.selected) {
|
|
||||||
state.originalLayer.ctx.drawImage(
|
|
||||||
state.selected.image,
|
|
||||||
state.original.x,
|
state.original.x,
|
||||||
state.original.y
|
state.original.y
|
||||||
);
|
);
|
||||||
commands.runCommand("eraseImage", "Image Transform Erase", {
|
|
||||||
...state.original,
|
// Erase Original Selection Area
|
||||||
ctx: state.originalLayer.ctx,
|
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,
|
// Draw Image
|
||||||
x: Math.round(state.selected.x),
|
const {canvas, bb} = cropCanvas(state.originalDisplayLayer.canvas, {
|
||||||
y: Math.round(state.selected.y),
|
border: 10,
|
||||||
w: Math.round(state.selected.w),
|
|
||||||
h: Math.round(state.selected.h),
|
|
||||||
});
|
});
|
||||||
|
commands.runCommand("drawImage", "Transform Tool Apply", {
|
||||||
|
image: canvas,
|
||||||
|
...bb,
|
||||||
|
});
|
||||||
|
|
||||||
state.reset(true);
|
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
|
// Handles left mouse drag events
|
||||||
state.dragstartcb = (evn) => {
|
state.dragcb = (evn) => {
|
||||||
let ix = evn.ix;
|
const {x, y, sx, sy} = _tool._process_cursor(evn, state.snapToGrid);
|
||||||
let iy = evn.iy;
|
|
||||||
if (state.snapToGrid) {
|
|
||||||
ix += snap(evn.ix, 0, 64);
|
|
||||||
iy += snap(evn.iy, 0, 64);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If is selected, check if drag is in handles/body and act accordingly
|
if (state.selected) transform(evn, x, y, sx, sy);
|
||||||
if (state.selected) {
|
|
||||||
const handles = state.selected.handles();
|
|
||||||
|
|
||||||
const activeHandle = handles.find((v) => {
|
if (selection.exists) selection.dragcb(evn);
|
||||||
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};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handles left mouse drag end events
|
// Handles left mouse drag end events
|
||||||
state.dragendcb = (evn) => {
|
state.dragendcb = (evn) => {
|
||||||
let x = evn.x;
|
const {x, y, sx, sy} = _tool._process_cursor(evn, state.snapToGrid);
|
||||||
let y = evn.y;
|
|
||||||
if (state.snapToGrid) {
|
|
||||||
x += snap(evn.x, 0, 64);
|
|
||||||
y += snap(evn.y, 0, 64);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we are scaling, stop scaling and do some handler magic
|
if (selection.exists) {
|
||||||
if (state.scaling) {
|
selection.dragendcb(evn);
|
||||||
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",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cut out selected portion of the image for manipulation
|
const bb = selection.bb;
|
||||||
const cvs = document.createElement("canvas");
|
|
||||||
cvs.width = state.selected.w;
|
|
||||||
cvs.height = state.selected.h;
|
|
||||||
const ctx = cvs.getContext("2d");
|
|
||||||
|
|
||||||
ctx.drawImage(
|
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,
|
uil.canvas,
|
||||||
state.selected.x,
|
bb.x,
|
||||||
state.selected.y,
|
bb.y,
|
||||||
state.selected.w,
|
bb.w,
|
||||||
state.selected.h,
|
bb.h,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
state.selected.w,
|
bb.w,
|
||||||
state.selected.h
|
bb.h
|
||||||
);
|
);
|
||||||
|
|
||||||
uil.ctx.clearRect(
|
uil.ctx.clearRect(bb.x, bb.y, bb.w, bb.h);
|
||||||
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.original = {
|
||||||
state.selected = null;
|
...bb,
|
||||||
|
sx: selection.bb.center.x,
|
||||||
state.dragging = null;
|
sy: selection.bb.center.y,
|
||||||
|
layer: uil.layer,
|
||||||
|
};
|
||||||
|
state.selected = new _tool.MarqueeSelection(canvas, bb.center);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selection.deselect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.selected) transform(evn, x, y, sx, sy);
|
||||||
|
|
||||||
|
moving = null;
|
||||||
|
scaling = null;
|
||||||
|
rotating = false;
|
||||||
|
|
||||||
state.redraw();
|
state.redraw();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -550,11 +471,11 @@ const selectTransformTool = () =>
|
||||||
|
|
||||||
ctx.clearRect(0, 0, state.selected.w, state.selected.h);
|
ctx.clearRect(0, 0, state.selected.w, state.selected.h);
|
||||||
ctx.drawImage(
|
ctx.drawImage(
|
||||||
state.selected.image,
|
state.selected.canvas,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
state.selected.image.width,
|
state.selected.canvas.width,
|
||||||
state.selected.image.height,
|
state.selected.canvas.height,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
state.selected.w,
|
state.selected.w,
|
||||||
|
@ -684,7 +605,7 @@ const selectTransformTool = () =>
|
||||||
saveSelectionButton.onclick = () => {
|
saveSelectionButton.onclick = () => {
|
||||||
downloadCanvas({
|
downloadCanvas({
|
||||||
cropToContent: false,
|
cropToContent: false,
|
||||||
canvas: state.selected.image,
|
canvas: state.selected.canvas,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -695,7 +616,7 @@ const selectTransformTool = () =>
|
||||||
createResourceButton.title = "Saves Selection as a Resource";
|
createResourceButton.title = "Saves Selection as a Resource";
|
||||||
createResourceButton.onclick = () => {
|
createResourceButton.onclick = () => {
|
||||||
const image = document.createElement("img");
|
const image = document.createElement("img");
|
||||||
image.src = state.selected.image.toDataURL();
|
image.src = state.selected.canvas.toDataURL();
|
||||||
image.onload = () => {
|
image.onload = () => {
|
||||||
tools.stamp.state.addResource("Selection Resource", image);
|
tools.stamp.state.addResource("Selection Resource", image);
|
||||||
tools.stamp.enable();
|
tools.stamp.enable();
|
||||||
|
|
|
@ -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 = () =>
|
const stampTool = () =>
|
||||||
toolbar.registerTool(
|
toolbar.registerTool(
|
||||||
"./res/icons/file-up.svg",
|
"./res/icons/file-up.svg",
|
||||||
|
@ -14,6 +52,12 @@ const stampTool = () =>
|
||||||
mouse.listen.world.btn.left.onclick.on(state.drawcb);
|
mouse.listen.world.btn.left.onclick.on(state.drawcb);
|
||||||
mouse.listen.world.btn.right.onclick.on(state.cancelcb);
|
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
|
// For calls from other tools to paste image
|
||||||
if (opt && opt.image) {
|
if (opt && opt.image) {
|
||||||
state.addResource(
|
state.addResource(
|
||||||
|
@ -41,6 +85,12 @@ const stampTool = () =>
|
||||||
mouse.listen.world.btn.left.onclick.clear(state.drawcb);
|
mouse.listen.world.btn.left.onclick.clear(state.drawcb);
|
||||||
mouse.listen.world.btn.right.onclick.clear(state.cancelcb);
|
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();
|
ovLayer.clear();
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -54,7 +104,15 @@ const stampTool = () =>
|
||||||
state.lastMouseMove = {x: 0, y: 0};
|
state.lastMouseMove = {x: 0, y: 0};
|
||||||
state.block_res_change = true;
|
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) => {
|
state.selectResource = (resource, nolock = true, deselect = true) => {
|
||||||
|
rotation = 0;
|
||||||
|
state.setScale(1);
|
||||||
if (nolock && state.ctxmenu.uploadButton.disabled) return;
|
if (nolock && state.ctxmenu.uploadButton.disabled) return;
|
||||||
|
|
||||||
console.debug(
|
console.debug(
|
||||||
|
@ -290,32 +348,65 @@ const stampTool = () =>
|
||||||
syncResources();
|
syncResources();
|
||||||
};
|
};
|
||||||
|
|
||||||
state.movecb = (evn) => {
|
state.onwheelcb = (evn) => {
|
||||||
let x = evn.x;
|
_stamp_onwheel(evn, state);
|
||||||
let y = evn.y;
|
};
|
||||||
if (state.snapToGrid) {
|
|
||||||
x += snap(evn.x, 0, 64);
|
state.dragstartcb = (evn) => {
|
||||||
y += snap(evn.y, 0, 64);
|
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);
|
state.dragendcb = (evn) => {
|
||||||
uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
|
rotating = null;
|
||||||
state.erasePrevCursor && state.erasePrevCursor();
|
};
|
||||||
|
|
||||||
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;
|
state.lastMouseMove = evn;
|
||||||
|
|
||||||
ovLayer.clear();
|
ovLayer.clear();
|
||||||
|
|
||||||
|
let px = sx;
|
||||||
|
let py = sy;
|
||||||
|
|
||||||
|
if (rotating) {
|
||||||
|
px = rotating.x;
|
||||||
|
py = rotating.y;
|
||||||
|
}
|
||||||
|
|
||||||
// Draw selected image
|
// Draw selected image
|
||||||
if (state.selected) {
|
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
|
// Draw current cursor location
|
||||||
state.erasePrevCursor = _tool._cursor_draw(x, y);
|
erasePrevCursor = _tool._cursor_draw(px, py);
|
||||||
uiCtx.restore();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
state.redraw = () => {
|
state.redraw = () => {
|
||||||
|
@ -323,20 +414,16 @@ const stampTool = () =>
|
||||||
};
|
};
|
||||||
|
|
||||||
state.drawcb = (evn) => {
|
state.drawcb = (evn) => {
|
||||||
let x = evn.x;
|
const {x, y, sx, sy} = _tool._process_cursor(evn, state.snapToGrid);
|
||||||
let y = evn.y;
|
|
||||||
if (state.snapToGrid) {
|
|
||||||
x += snap(evn.x, 0, 64);
|
|
||||||
y += snap(evn.y, 0, 64);
|
|
||||||
}
|
|
||||||
|
|
||||||
const resource = state.selected;
|
const resource = state.selected;
|
||||||
|
|
||||||
if (resource) {
|
if (resource) {
|
||||||
|
const {canvas, bb} = cropCanvas(ovCanvas, {border: 10});
|
||||||
commands.runCommand("drawImage", "Image Stamp", {
|
commands.runCommand("drawImage", "Image Stamp", {
|
||||||
image: resource.image,
|
image: canvas,
|
||||||
x,
|
x: bb.x,
|
||||||
y,
|
y: bb.y,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (resource.temporary) {
|
if (resource.temporary) {
|
||||||
|
@ -380,6 +467,16 @@ const stampTool = () =>
|
||||||
);
|
);
|
||||||
state.ctxmenu.snapToGridLabel = array;
|
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
|
// Create resource list
|
||||||
const uploadButtonId = `upload-btn-${guid()}`;
|
const uploadButtonId = `upload-btn-${guid()}`;
|
||||||
|
|
||||||
|
@ -528,6 +625,7 @@ const stampTool = () =>
|
||||||
},
|
},
|
||||||
populateContextMenu: (menu, state) => {
|
populateContextMenu: (menu, state) => {
|
||||||
menu.appendChild(state.ctxmenu.snapToGridLabel);
|
menu.appendChild(state.ctxmenu.snapToGridLabel);
|
||||||
|
menu.appendChild(state.ctxmenu.scaleSlider);
|
||||||
menu.appendChild(state.ctxmenu.resourceManager);
|
menu.appendChild(state.ctxmenu.resourceManager);
|
||||||
},
|
},
|
||||||
shortcut: "U",
|
shortcut: "U",
|
||||||
|
|
|
@ -5,15 +5,15 @@
|
||||||
<title>openOutpaint 🐠</title>
|
<title>openOutpaint 🐠</title>
|
||||||
<!-- CSS Variables -->
|
<!-- CSS Variables -->
|
||||||
<link href="../css/colors.css?v=3f81e80" rel="stylesheet" />
|
<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=5b8d4d6" rel="stylesheet" />
|
<link href="../css/index.css?v=5b8d4d6" rel="stylesheet" />
|
||||||
<link href="../css/layers.css?v=b4fbf61" 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/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" />
|
<link href="../css/ui/toolbar.css?v=109c78f" rel="stylesheet" />
|
||||||
|
|
||||||
<!-- Tool Specific CSS -->
|
<!-- Tool Specific CSS -->
|
||||||
|
|
|
@ -8,8 +8,8 @@
|
||||||
<iframe
|
<iframe
|
||||||
id="openoutpaint"
|
id="openoutpaint"
|
||||||
style="width: 100%; height: 800px"
|
style="width: 100%; height: 800px"
|
||||||
src="../index.html?v=95a96ad"
|
src="../index.html?v=daf18de"
|
||||||
src="../index.html?v=95a96ad"
|
src="../index.html?v=daf18de"
|
||||||
frameborder="0"></iframe>
|
frameborder="0"></iframe>
|
||||||
<button id="add-res">Add Resource</button>
|
<button id="add-res">Add Resource</button>
|
||||||
<script>
|
<script>
|
||||||
|
|
Loading…
Reference in a new issue