Merge pull request #168 from zero01101/rotate

Rotation, finally
This commit is contained in:
Victor Seiji Hariki 2023-01-14 18:57:06 -03:00 committed by GitHub
commit ec4054529c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 983 additions and 477 deletions

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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>

View file

@ -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/"}),
}, },

View file

@ -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";

View file

@ -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;

View file

@ -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})
);
}
} }
/** /**

View file

@ -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

View file

@ -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");

View file

@ -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);
};
}
},
}; };

View file

@ -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();

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 = () => 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",

View file

@ -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 -->

View file

@ -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>