Signed-off-by: Victor Seiji Hariki <victorseijih@gmail.com>
This commit is contained in:
Victor Seiji Hariki 2023-01-08 18:21:08 -03:00
parent 2c9eea4ce6
commit b056f81155
10 changed files with 1139 additions and 556 deletions

View file

@ -45,7 +45,7 @@
right: 0; right: 0;
} }
#layer-overlay { .overlay-canvas {
position: fixed; position: fixed;
top: 0; top: 0;

View file

@ -8,7 +8,7 @@
<link href="css/icons.css?v=caa702e" rel="stylesheet" /> <link href="css/icons.css?v=caa702e" 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=a94b43d" rel="stylesheet" />
<link href="css/ui/generic.css?v=4b9afe2" rel="stylesheet" /> <link href="css/ui/generic.css?v=4b9afe2" rel="stylesheet" />
@ -319,7 +319,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">
@ -339,7 +340,7 @@
<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=c7cdf59" 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>
@ -349,11 +350,11 @@
<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=23c4cf4"
type="text/javascript"></script> type="text/javascript"></script>
<!-- Configuration --> <!-- Configuration -->
<script src="js/config.js?v=f903401" type="text/javascript"></script> <script src="js/config.js?v=664f6ee" 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,7 +369,7 @@
<!-- Load Tools --> <!-- Load Tools -->
<script <script
src="js/ui/tool/generic.js?v=2bcd36d" src="js/ui/tool/generic.js?v=11c9556"
type="text/javascript"></script> type="text/javascript"></script>
<script src="js/ui/tool/dream.js?v=18e3b66" type="text/javascript"></script> <script src="js/ui/tool/dream.js?v=18e3b66" type="text/javascript"></script>
@ -379,7 +380,7 @@
src="js/ui/tool/colorbrush.js?v=8acb4f6" src="js/ui/tool/colorbrush.js?v=8acb4f6"
type="text/javascript"></script> type="text/javascript"></script>
<script <script
src="js/ui/tool/select.js?v=3a96068" src="js/ui/tool/select.js?v=7db1d0c"
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=3c07ac8" type="text/javascript"></script>
<script <script
@ -394,7 +395,7 @@
src="js/initalize/toolbar.populate.js?v=c1ca438" src="js/initalize/toolbar.populate.js?v=c1ca438"
type="text/javascript"></script> type="text/javascript"></script>
<script <script
src="js/initalize/debug.populate.js?v=64ad17f" src="js/initalize/debug.populate.js?v=76f8c89"
type="text/javascript"></script> type="text/javascript"></script>
<script <script
src="js/initalize/ui.populate.js?v=b59b288" src="js/initalize/ui.populate.js?v=b59b288"

View file

@ -5,6 +5,9 @@
*/ */
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,

View file

@ -31,3 +31,5 @@ mouse.listen.world.onmousemove.on((evn) => {
debugCtx.fill(); debugCtx.fill();
} }
}); });
window.addEventListener("DOMContentLoaded", () => (global.debug = true));

View file

@ -65,6 +65,12 @@ uiCanvas.width = uiCanvas.clientWidth;
uiCanvas.height = uiCanvas.clientHeight; uiCanvas.height = uiCanvas.clientHeight;
const uiCtx = uiCanvas.getContext("2d", {desynchronized: true}); const uiCtx = uiCanvas.getContext("2d", {desynchronized: true});
/** @type {HTMLCanvasElement} */
const uiDebugCanvas = document.getElementById("layer-debug-overlay"); // where mouse cursor renders
uiDebugCanvas.width = uiDebugCanvas.clientWidth;
uiDebugCanvas.height = uiDebugCanvas.clientHeight;
const uiDebugCtx = uiDebugCanvas.getContext("2d", {desynchronized: true});
/** /**
* Here we setup canvas dynamic scaling * Here we setup canvas dynamic scaling
*/ */
@ -160,54 +166,68 @@ 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 = { const viewport = {
get cx() { cx: 0,
return this._x * this.zoom; cy: 0,
},
set cx(v) {
return (this._x = v / this.zoom);
},
_x: 0,
get cy() {
return this._y * this.zoom;
},
set cy(v) {
return (this._y = v / this.zoom);
},
_y: 0,
zoom: 1, zoom: 1,
rotation: 0, rotation: 0,
get w() { get w() {
return (window.innerWidth * 1) / this.zoom; return window.innerWidth * 1;
}, },
get h() { get h() {
return (window.innerHeight * 1) / this.zoom; return window.innerHeight * 1;
}, },
viewToCanvas(x, y) { viewToCanvas(x, y) {
return this.matrix.transformPoint({x, y});
return { return {
x: this.cx + this.w * (x / window.innerWidth - 0.5), x: this.cx + this.w * (x / window.innerWidth - 0.5),
y: this.cy + this.h * (y / window.innerHeight - 0.5), y: this.cy + this.h * (y / window.innerHeight - 0.5),
}; };
}, },
canvasToView(x, y) { canvasToView(x, y) {
return this.imatrix.transformPoint({x, y});
return { return {
x: window.innerWidth * ((x - this.cx) / this.w) + window.innerWidth / 2, x: window.innerWidth * ((x - this.cx) / this.w) + window.innerWidth / 2,
y: window.innerHeight * ((y - this.cy) / this.h) + window.innerHeight / 2, y: window.innerHeight * ((y - this.cy) / this.h) + window.innerHeight / 2,
}; };
}, },
/**
* The transformation matrix (vp to world)
*
* @type {DOMMatrix}
*/
get matrix() {
const matrix = new DOMMatrix();
matrix.scaleSelf(1 / this.zoom);
matrix.translateSelf(this.cx - this.w / 2, this.cy - this.h / 2);
return matrix;
},
/**
* The transformation matrix (world to vp)
*
* @type {DOMMatrix}
*/
get imatrix() {
return this.matrix.invertSelf();
},
/** /**
* 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.transform = this.imatrix;
return;
el.style.transformOrigin = `${this.cx}px ${this.cy}px`; el.style.transformOrigin = `${this.cx}px ${this.cy}px`;
el.style.transform = `scale(${this.zoom}) translate(${-( el.style.transform = `scale(${this.zoom}) translate(${-(
this._x - this._x -
@ -216,8 +236,8 @@ const viewport = {
}, },
}; };
viewport.cx = imageCollection.size.w / 2; //viewport.cx = imageCollection.size.w / 2;
viewport.cy = imageCollection.size.h / 2; //viewport.cy = imageCollection.size.h / 2;
let worldInit = null; let worldInit = null;
@ -320,8 +340,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.cy = worldInit.y + (evn.iy - evn.y) / viewport.zoom; viewport.cy = worldInit.y + (evn.iy - evn.y);
// Limits // Limits
viewport.cx = Math.max( viewport.cx = Math.max(
@ -337,13 +357,6 @@ const cameraPaint = (evn) => {
} }
viewport.transform(imageCollection.element); viewport.transform(imageCollection.element);
if (global.debug) {
debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height);
debugCtx.fillStyle = "#F0F";
debugCtx.beginPath();
debugCtx.arc(viewport.cx, viewport.cy, 5, 0, Math.PI * 2);
debugCtx.fill();
}
}; };
const cameraPaintEnd = (evn) => { const cameraPaintEnd = (evn) => {

View file

@ -64,6 +64,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

@ -174,6 +174,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 +275,137 @@ 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;
/**
* @type {Point}
*/
position = {x: 0, y: 0};
scale = 1;
rotation = 0;
/**
* @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;
}
/**
* Draws the marquee selector box
*
* @param {CanvasRenderingContext2D} context A context for rendering the box to
* @param {DOMMatrix} transform A transformation matrix to transform the position by
*/
drawBox(context, transform = new DOMMatrix()) {
context.save();
context.setTransform(transform);
context.scale(this.scale, this.scale);
context.rotate((this.rotation * 180) / Math.PI);
context.translate(this.position.x, this.position.y);
// Line Color
context.strokeStyle = "#FFF";
// Draw the box itself
context.save();
context.lineWidth = 2;
context.setLineDash([4, 2]);
context.beginPath();
context.strokeRect(
-this.canvas.width / 2,
-this.canvas.height / 2,
this.canvas.width,
this.canvas.height
);
context.restore();
context.restore();
}
/**
* Draws the selected images
*
* @param {CanvasRenderingContext2D} context A context for rendering the box to
* @param {DOMMatrix} transform A transformation matrix to transform the position by
*/
drawImage(context, transform = new DOMMatrix()) {
context.save();
context.setTransform(transform);
context.scale(this.scale, this.scale);
context.rotate((this.rotation * 180) / Math.PI);
context.translate(this.position.x, this.position.y);
context.restore();
}
},
/**
* Marquee Selection with an image
*/
_marquee_selection(state) {
return {
// Location of the origin of the selection
position: {x: 0, y: 0},
// Scale of the selection
scale: 1,
// Angle of the selection (radians)
rotation: 0,
/**
* Draws the selection
*/
draw() {},
};
},
}; };
name;

908
js/ui/tool/select.bkp.js Normal file
View file

@ -0,0 +1,908 @@
/**
* TODO: REFACTOR THIS WHOLE THING
*/
const selectTransformTool = () =>
toolbar.registerTool(
"./res/icons/box-select.svg",
"Select Image",
(state, opt) => {
// Draw new cursor immediately
ovLayer.clear();
state.movecb(mouse.coords.world.pos);
// Canvas left mouse handlers
mouse.listen.world.onmousemove.on(state.movecb);
mouse.listen.world.btn.left.onclick.on(state.clickcb);
mouse.listen.world.btn.left.ondragstart.on(state.dragstartcb);
mouse.listen.world.btn.left.ondragend.on(state.dragendcb);
// Canvas right mouse handler
mouse.listen.world.btn.right.onclick.on(state.cancelcb);
// Keyboard click handlers
keyboard.listen.onkeyclick.on(state.keyclickcb);
keyboard.listen.onkeydown.on(state.keydowncb);
// Layer system handlers
uil.onactive.on(state.uilayeractivecb);
// Registers keyboard shortcuts
keyboard.onShortcut({ctrl: true, key: "KeyC"}, state.ctrlccb);
keyboard.onShortcut({ctrl: true, key: "KeyV"}, state.ctrlvcb);
keyboard.onShortcut({ctrl: true, key: "KeyX"}, state.ctrlxcb);
state.selected = null;
},
(state, opt) => {
// Clear all those listeners and shortcuts we set up
mouse.listen.world.onmousemove.clear(state.movecb);
mouse.listen.world.btn.left.onclick.clear(state.clickcb);
mouse.listen.world.btn.left.ondragstart.clear(state.dragstartcb);
mouse.listen.world.btn.left.ondragend.clear(state.dragendcb);
mouse.listen.world.btn.right.onclick.clear(state.cancelcb);
keyboard.listen.onkeyclick.clear(state.keyclickcb);
keyboard.listen.onkeydown.clear(state.keydowncb);
keyboard.deleteShortcut(state.ctrlccb, "KeyC");
keyboard.deleteShortcut(state.ctrlvcb, "KeyV");
keyboard.deleteShortcut(state.ctrlxcb, "KeyX");
uil.onactive.clear(state.uilayeractivecb);
// Clear any selections
state.reset();
// Resets cursor
ovLayer.clear();
// Clears overlay
imageCollection.inputElement.style.cursor = "auto";
},
{
init: (state) => {
state.clipboard = {};
state.snapToGrid = true;
state.keepAspectRatio = true;
state.block_res_change = true;
state.useClipboard = !!(
navigator.clipboard && navigator.clipboard.write
); // Use it by default if supported
state.selectionPeekOpacity = 40;
state.original = null;
state.dragging = null;
state.rotation = 0;
state._selected = null;
Object.defineProperty(state, "selected", {
get: () => state._selected,
set: (v) => {
if (v) state.ctxmenu.enableButtons();
else state.ctxmenu.disableButtons();
return (state._selected = v);
},
});
state.moving = null;
// Some things to easy request for a redraw
state.lastMouseTarget = null;
state.lastMouseMove = {x: 0, y: 0};
state.redraw = () => {
ovLayer.clear();
state.movecb(state.lastMouseMove);
};
state.uilayeractivecb = ({uilayer}) => {
if (state.originalDisplayLayer) {
state.originalDisplayLayer.moveAfter(uilayer.layer);
}
};
// Clears selection and make things right
state.reset = (erase = false) => {
if (state.selected && !erase)
state.originalLayer.ctx.drawImage(
state.original.image,
state.original.x,
state.original.y
);
if (state.originalDisplayLayer) {
imageCollection.deleteLayer(state.originalDisplayLayer);
state.originalDisplayLayer = null;
}
if (state.dragging) state.dragging = null;
else state.selected = null;
state.rotation = 0;
state.original = null;
state.redraw();
};
// Selection bounding box object. Has some witchery to deal with handles.
const selectionBB = (x1, y1, x2, y2) => {
x1 = Math.round(x1);
y1 = Math.round(y1);
x2 = Math.round(x2);
y2 = Math.round(y2);
return {
original: {
x: Math.min(x1, x2),
y: Math.min(y1, y2),
w: Math.abs(x1 - x2),
h: Math.abs(y1 - y2),
},
x: Math.min(x1, x2),
y: Math.min(y1, y2),
w: Math.abs(x1 - x2),
h: Math.abs(y1 - y2),
updateOriginal() {
this.original.x = this.x;
this.original.y = this.y;
this.original.w = this.w;
this.original.h = this.h;
},
contains(x, y) {
return (
this.x <= x &&
x <= this.x + this.w &&
this.y <= y &&
y <= this.y + this.h
);
},
center() {
return {x: this.x + this.w / 2, y: this.y + this.h / 2};
},
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);
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;
}
},
};
},
};
};
// 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);
}
uiCtx.save();
// Update scale
if (state.scaling) {
state.scaling.scaleTo(x, y, state.keepAspectRatio);
}
// Update rotation
if (state.rotating) {
const center = state.selected.center();
state.rotation = Math.atan2(evn.x - center.x, center.y - evn.y);
}
// 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([]);
}
// Draw selected box
if (state.selected) {
state.selected.drawBox(uiCtx);
}
if (state.selected && false) {
ovCtx.lineWidth = 1;
ovCtx.strokeStyle = "#FFF";
const bb = new BoundingBox({
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,
};
const scenter = state.selected.center();
// Draw Image
ovCtx.save();
ovCtx.filter = `opacity(${state.selectionPeekOpacity}%)`;
ovCtx.translate(scenter.x, scenter.y);
ovCtx.rotate(state.rotation);
const matrix = ovCtx.getTransform();
const imatrix = matrix.invertSelf();
const cursor = imatrix.transformPoint({
x: evn.x,
y: evn.y,
});
ovCtx.drawImage(
state.selected.image,
0,
0,
state.selected.image.width,
state.selected.image.height,
-state.selected.w / 2,
-state.selected.h / 2,
state.selected.w,
state.selected.h
);
ovCtx.restore();
state.originalDisplayLayer.clear();
state.originalDisplayLayer.ctx.save();
state.originalDisplayLayer.ctx.translate(scenter.x, scenter.y);
state.originalDisplayLayer.ctx.rotate(state.rotation);
state.originalDisplayLayer.ctx.drawImage(
state.selected.image,
0,
0,
state.selected.image.width,
state.selected.image.height,
-state.selected.w / 2,
-state.selected.h / 2,
state.selected.w,
state.selected.h
);
state.originalDisplayLayer.ctx.restore();
const centerx = bbvp.x + bbvp.w / 2;
const centery = bbvp.y + bbvp.h / 2;
uiCtx.save();
uiCtx.translate(centerx, centery);
uiCtx.rotate(state.rotation);
const matrixvp = uiCtx.getTransform();
const imatrixvp = matrixvp.invertSelf();
const cursorvp = imatrixvp.transformPoint({
x: evn.evn.clientX,
y: evn.evn.clientY,
});
// Draw selection box
uiCtx.strokeStyle = "#FFF";
uiCtx.setLineDash([4, 2]);
uiCtx.strokeRect(-bbvp.w / 2, -bbvp.h / 2, bbvp.w, bbvp.h);
uiCtx.setLineDash([]);
// Draw Scaling/Rotation Origin
uiCtx.beginPath();
uiCtx.arc(0, 0, 5, 0, 2 * Math.PI);
uiCtx.stroke();
// Draw Rotation Handle
let cursorInAnyHandle = false;
{
let radius = config.handleDrawSize / 2;
const dx = cursorvp.x;
const dy =
cursorvp.y - (-bbvp.h / 2 - config.rotateHandleDistance);
const dmax = config.handleDetectSize / 2;
if (dx * dx + dy * dy < dmax * dmax) {
cursorInAnyHandle ||= true;
radius *= config.handleDrawHoverScale;
}
uiCtx.beginPath();
uiCtx.moveTo(0, -bbvp.h / 2);
uiCtx.lineTo(
0,
-bbvp.h / 2 - config.rotateHandleDistance + radius
);
uiCtx.stroke();
uiCtx.beginPath();
uiCtx.arc(
0,
-bbvp.h / 2 - config.rotateHandleDistance,
radius,
0,
Math.PI * 2
);
uiCtx.stroke();
}
// Draw Scaling Handles
const drawHandle = (hx, hy) => {
// Handle Draw Range
let hs = config.handleDrawSize;
// Handle Detection Range
let dhs = config.handleDetectSize;
const handleBB = new BoundingBox({
x: hx - dhs / 2,
y: hy - dhs / 2,
w: dhs,
h: dhs,
});
const cursorInHandle = handleBB.contains(cursorvp.x, cursorvp.y);
cursorInAnyHandle ||= cursorInHandle;
if (cursorInHandle) hs *= config.handleDrawHoverScale;
uiCtx.strokeRect(hx - hs / 2, hy - hs / 2, hs, hs);
};
drawHandle(-bbvp.w / 2, -bbvp.h / 2);
drawHandle(bbvp.w / 2, -bbvp.h / 2);
drawHandle(-bbvp.w / 2, bbvp.h / 2);
drawHandle(bbvp.w / 2, bbvp.h / 2);
uiCtx.restore();
// Change cursor
if (
cursorInAnyHandle ||
(-bb.w / 2 < cursor.x &&
bb.w / 2 > cursor.x &&
-bb.h / 2 < cursor.y &&
bb.h / 2 > cursor.y)
)
imageCollection.inputElement.style.cursor = "pointer";
}
// Draw current cursor location
state.erasePrevCursor = _tool._cursor_draw(x, y);
uiCtx.restore();
};
// Handles left mouse clicks
state.clickcb = (evn) => {
if (
!state.original ||
(state.originalLayer === uil.layer &&
state.original.x === state.selected.x &&
state.original.y === state.selected.y &&
state.original.w === state.selected.w &&
state.original.h === state.selected.h &&
state.rotation === 0)
) {
state.reset();
return;
}
// If something is selected, commit changes to the canvas
if (state.selected) {
state.originalLayer.ctx.drawImage(
state.selected.image,
state.original.x,
state.original.y
);
commands.runCommand("eraseImage", "Image Transform Erase", {
...state.original,
ctx: state.originalLayer.ctx,
});
// Use display layer as source
const {canvas, bb} = cropCanvas(state.originalDisplayLayer.canvas);
commands.runCommand("drawImage", "Image Transform Draw", {
image: canvas,
...bb,
});
state.reset(true);
}
};
// Handles left mouse drag events
state.dragstartcb = (evn) => {
let ix = evn.ix;
let iy = evn.iy;
if (state.snapToGrid) {
ix += snap(evn.ix, 0, 64);
iy += snap(evn.iy, 0, 64);
}
// If is selected, check if drag is in handles/body and act accordingly
if (state.selected) {
// Get transformation matrices
const bb = {
x: state.selected.x,
y: state.selected.y,
w: state.selected.w,
h: state.selected.h,
};
const bbvp = {
...viewport.canvasToView(bb.x, bb.y),
w: viewport.zoom * bb.w,
h: viewport.zoom * bb.h,
};
const ivp = viewport.canvasToView(evn.ix, evn.iy);
// Viewport Coordinates
const centerx = bbvp.x + bbvp.w / 2;
const centery = bbvp.y + bbvp.h / 2;
const matrixvp = new DOMMatrix();
matrixvp.translateSelf(centerx, centery);
matrixvp.rotateSelf((state.rotation * 180) / Math.PI);
const imatrixvp = matrixvp.inverse();
const cursorvp = imatrixvp.transformPoint(ivp);
// Check rotation handle
let rotationHandle = false;
const dx = cursorvp.x;
const dy = cursorvp.y - (-bbvp.h / 2 - config.rotateHandleDistance);
const dmax = config.handleDetectSize / 2;
if (dx * dx + dy * dy < dmax * dmax) rotationHandle = true;
// Check handles
let activeHandle = null;
const testHandle = (hx, hy, hwx, hwy) => {
// Handle Detection Range
let dhs = config.handleDetectSize;
const handleBB = new BoundingBox({
x: hx - dhs / 2,
y: hy - dhs / 2,
w: dhs,
h: dhs,
});
if (handleBB.contains(cursorvp.x, cursorvp.y)) {
activeHandle = state.selected.createHandle(hx, hy);
}
};
testHandle(-bbvp.w / 2, -bbvp.h / 2);
testHandle(bbvp.w / 2, -bbvp.h / 2);
testHandle(-bbvp.w / 2, bbvp.h / 2);
testHandle(bbvp.w / 2, bbvp.h / 2);
if (activeHandle) {
state.scaling = activeHandle;
} else if (rotationHandle) {
state.rotating = true;
} else if (state.selected.contains(evn.ix, evn.iy)) {
state.moving = {
snapOffset: {x: evn.ix - ix, y: evn.iy - iy},
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
state.dragendcb = (evn) => {
let x = evn.x;
let y = evn.y;
if (state.snapToGrid) {
x += snap(evn.x, 0, 64);
y += snap(evn.y, 0, 64);
}
// If we are scaling, stop scaling and do some handler magic
if (state.scaling) {
state.selected.updateOriginal();
state.scaling = null;
// If we are moving the selection, just... stop
} else if (state.rotating) {
state.rotating = false;
} 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",
});
state.rotation = 0;
// Cut out selected portion of the image for manipulation
const cvs = document.createElement("canvas");
cvs.width = state.selected.w;
cvs.height = state.selected.h;
const ctx = cvs.getContext("2d");
ctx.drawImage(
uil.canvas,
state.selected.x,
state.selected.y,
state.selected.w,
state.selected.h,
0,
0,
state.selected.w,
state.selected.h
);
uil.ctx.clearRect(
state.selected.x,
state.selected.y,
state.selected.w,
state.selected.h
);
state.selected.image = cvs;
state.original.image = cvs;
if (state.selected.w === 0 || state.selected.h === 0)
state.selected = null;
state.dragging = null;
state.selected = new _tool.MarqueeSelection(cvs);
}
state.redraw();
};
// Handler for right clicks. Basically resets everything
state.cancelcb = (evn) => {
state.reset();
};
// Keyboard callbacks (For now, they just handle the "delete" key)
state.keydowncb = (evn) => {};
state.keyclickcb = (evn) => {
switch (evn.code) {
case "Delete":
// Deletes selected area
state.selected &&
commands.runCommand("eraseImage", "Erase Area", state.selected);
state.selected = null;
state.redraw();
}
};
// Register Ctrl-C/V Shortcut
// Handles copying
state.ctrlccb = (evn, cut = false) => {
// We create a new canvas to store the data
state.clipboard.copy = document.createElement("canvas");
state.clipboard.copy.width = state.selected.w;
state.clipboard.copy.height = state.selected.h;
const ctx = state.clipboard.copy.getContext("2d");
ctx.clearRect(0, 0, state.selected.w, state.selected.h);
ctx.drawImage(
state.selected.image,
0,
0,
state.selected.image.width,
state.selected.image.height,
0,
0,
state.selected.w,
state.selected.h
);
// If cutting, we reverse the selection and erase the selection area
if (cut) {
const aux = state.original;
state.reset();
commands.runCommand("eraseImage", "Cut Image", aux);
}
// Because firefox needs manual activation of the feature
if (state.useClipboard) {
// Send to clipboard
state.clipboard.copy.toBlob((blob) => {
const item = new ClipboardItem({"image/png": blob});
navigator.clipboard &&
navigator.clipboard.write([item]).catch((e) => {
console.warn("Error sending to clipboard");
console.warn(e);
});
});
}
};
// Handles pasting
state.ctrlvcb = (evn) => {
if (state.useClipboard) {
// If we use the clipboard, do some proccessing of clipboard data (ugly but kind of minimum required)
navigator.clipboard &&
navigator.clipboard.read().then((items) => {
for (const item of items) {
for (const type of item.types) {
if (type.startsWith("image/")) {
item.getType(type).then(async (blob) => {
// Converts blob to image
const url = window.URL || window.webkitURL;
const image = document.createElement("img");
image.src = url.createObjectURL(blob);
await image.decode();
tools.stamp.enable({
image,
back: tools.selecttransform.enable,
});
});
}
}
}
});
} else if (state.clipboard.copy) {
// Use internal clipboard
const image = document.createElement("img");
image.src = state.clipboard.copy.toDataURL();
// Send to stamp, as clipboard temporary data
tools.stamp.enable({
image,
back: tools.selecttransform.enable,
});
}
};
// Cut shortcut. Basically, send to copy handler
state.ctrlxcb = (evn) => {
state.ctrlccb(evn, true);
};
},
populateContextMenu: (menu, state) => {
if (!state.ctxmenu) {
state.ctxmenu = {};
// Snap To Grid Checkbox
state.ctxmenu.snapToGridLabel = _toolbar_input.checkbox(
state,
"snapToGrid",
"Snap To Grid",
"icon-grid"
).checkbox;
// Keep Aspect Ratio
state.ctxmenu.keepAspectRatioLabel = _toolbar_input.checkbox(
state,
"keepAspectRatio",
"Keep Aspect Ratio",
"icon-maximize"
).checkbox;
// Use Clipboard
const clipboardCheckbox = _toolbar_input.checkbox(
state,
"useClipboard",
"Use clipboard",
"icon-clipboard-list"
);
state.ctxmenu.useClipboardLabel = clipboardCheckbox.checkbox;
if (!(navigator.clipboard && navigator.clipboard.write))
clipboardCheckbox.checkbox.disabled = true; // Disable if not available
// Selection Peek Opacity
state.ctxmenu.selectionPeekOpacitySlider = _toolbar_input.slider(
state,
"selectionPeekOpacity",
"Peek Opacity",
{
min: 0,
max: 100,
step: 10,
textStep: 1,
cb: () => {
state.redraw();
},
}
).slider;
// Some useful actions to do with selection
const actionArray = document.createElement("div");
actionArray.classList.add("button-array");
// Save button
const saveSelectionButton = document.createElement("button");
saveSelectionButton.classList.add("button", "tool");
saveSelectionButton.textContent = "Save Sel.";
saveSelectionButton.title = "Saves Selection";
saveSelectionButton.onclick = () => {
downloadCanvas({
cropToContent: false,
canvas: state.selected.image,
});
};
// Save as Resource Button
const createResourceButton = document.createElement("button");
createResourceButton.classList.add("button", "tool");
createResourceButton.textContent = "Resource";
createResourceButton.title = "Saves Selection as a Resource";
createResourceButton.onclick = () => {
const image = document.createElement("img");
image.src = state.selected.image.toDataURL();
image.onload = () => {
tools.stamp.state.addResource("Selection Resource", image);
tools.stamp.enable();
};
};
actionArray.appendChild(saveSelectionButton);
actionArray.appendChild(createResourceButton);
// Some useful actions to do with selection
const visibleActionArray = document.createElement("div");
visibleActionArray.classList.add("button-array");
// Save Visible button
const saveVisibleSelectionButton = document.createElement("button");
saveVisibleSelectionButton.classList.add("button", "tool");
saveVisibleSelectionButton.textContent = "Save Vis.";
saveVisibleSelectionButton.title = "Saves Visible Selection";
saveVisibleSelectionButton.onclick = () => {
const canvas = uil.getVisible(state.selected, {
categories: ["image", "user", "select-display"],
});
downloadCanvas({
cropToContent: false,
canvas,
});
};
// Save Visible as Resource Button
const createVisibleResourceButton = document.createElement("button");
createVisibleResourceButton.classList.add("button", "tool");
createVisibleResourceButton.textContent = "Vis. to Res.";
createVisibleResourceButton.title =
"Saves Visible Selection as a Resource";
createVisibleResourceButton.onclick = () => {
const canvas = uil.getVisible(state.selected, {
categories: ["image", "user", "select-display"],
});
const image = document.createElement("img");
image.src = canvas.toDataURL();
image.onload = () => {
tools.stamp.state.addResource("Selection Resource", image);
tools.stamp.enable();
};
};
visibleActionArray.appendChild(saveVisibleSelectionButton);
visibleActionArray.appendChild(createVisibleResourceButton);
// Disable buttons (if nothing is selected)
state.ctxmenu.disableButtons = () => {
saveSelectionButton.disabled = true;
createResourceButton.disabled = true;
saveVisibleSelectionButton.disabled = true;
createVisibleResourceButton.disabled = true;
};
// Disable buttons (if something is selected)
state.ctxmenu.enableButtons = () => {
saveSelectionButton.disabled = "";
createResourceButton.disabled = "";
saveVisibleSelectionButton.disabled = "";
createVisibleResourceButton.disabled = "";
};
state.ctxmenu.actionArray = actionArray;
state.ctxmenu.visibleActionArray = visibleActionArray;
}
const array = document.createElement("div");
array.classList.add("checkbox-array");
array.appendChild(state.ctxmenu.snapToGridLabel);
array.appendChild(state.ctxmenu.keepAspectRatioLabel);
array.appendChild(state.ctxmenu.useClipboardLabel);
menu.appendChild(array);
menu.appendChild(state.ctxmenu.selectionPeekOpacitySlider);
menu.appendChild(state.ctxmenu.actionArray);
menu.appendChild(state.ctxmenu.visibleActionArray);
},
shortcut: "S",
}
);

View file

@ -73,8 +73,6 @@ const selectTransformTool = () =>
state.selectionPeekOpacity = 40; state.selectionPeekOpacity = 40;
state.original = null; state.original = null;
state.dragging = null;
state.rotation = 0;
state._selected = null; state._selected = null;
Object.defineProperty(state, "selected", { Object.defineProperty(state, "selected", {
get: () => state._selected, get: () => state._selected,
@ -85,7 +83,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;
@ -125,538 +122,50 @@ const selectTransformTool = () =>
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); state.dragstartcb = (evn) => selection.dragstartcb(evn);
y1 = Math.round(y1); state.dragcb = (evn) => selection.dragcb(evn);
x2 = Math.round(x2); state.dragendcb = (evn) => selection.dragendcb(evn);
y2 = Math.round(y2);
return {
original: {
x: Math.min(x1, x2),
y: Math.min(y1, y2),
w: Math.abs(x1 - x2),
h: Math.abs(y1 - y2),
},
x: Math.min(x1, x2),
y: Math.min(y1, y2),
w: Math.abs(x1 - x2),
h: Math.abs(y1 - y2),
updateOriginal() {
this.original.x = this.x;
this.original.y = this.y;
this.original.w = this.w;
this.original.h = this.h;
},
contains(x, y) {
return (
this.x <= x &&
x <= this.x + this.w &&
this.y <= y &&
y <= this.y + this.h
);
},
center() {
return {x: this.x + this.w / 2, y: this.y + this.h / 2};
},
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); // Mouse Move Handler
let yRatio = (ny - origin.y) / (y - origin.y); let eraseCursor = () => null;
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;
}
},
};
},
};
};
// Mouse move handler. As always, also renders cursor
state.movecb = (evn) => { state.movecb = (evn) => {
ovLayer.clear(); // Get cursor positions
uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); const {x, y, sx, sy} = _tool._process_cursor(evn, state.snapToGrid);
state.erasePrevCursor && state.erasePrevCursor();
imageCollection.inputElement.style.cursor = "auto"; // Draw cursor
state.lastMouseTarget = evn.target; eraseCursor();
state.lastMouseMove = evn; eraseCursor = _tool._cursor_draw(sx, sy);
let x = evn.x;
let y = evn.y; // Draw Box and Selected Image
if (state.snapToGrid) { if (state.selected) {
x += snap(evn.x, 0, 64); state.selected.drawBox(uiCtx);
y += snap(evn.y, 0, 64);
} }
uiCtx.save(); // Draw Selection
if (selection.exists) {
// Update scale
if (state.scaling) {
state.scaling.scaleTo(x, y, state.keepAspectRatio);
}
// Update rotation
if (state.rotating) {
const center = state.selected.center();
state.rotation = Math.atan2(evn.x - center.x, center.y - evn.y);
}
// 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.setLineDash([2, 2]);
uiCtx.lineWidth = 1; uiCtx.lineWidth = 1;
uiCtx.strokeStyle = "#FFF"; uiCtx.strokeStyle = "#FFF";
const ix = state.dragging.ix; const vpbb = selection.bb.transform(viewport.matrix);
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([]);
} }
// Draw selected box
if (state.selected) {
ovCtx.lineWidth = 1;
ovCtx.strokeStyle = "#FFF";
const bb = new BoundingBox({
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,
};
const scenter = state.selected.center();
// Draw Image
ovCtx.save();
ovCtx.filter = `opacity(${state.selectionPeekOpacity}%)`;
ovCtx.translate(scenter.x, scenter.y);
ovCtx.rotate(state.rotation);
const matrix = ovCtx.getTransform();
const imatrix = matrix.invertSelf();
const cursor = imatrix.transformPoint({
x: evn.x,
y: evn.y,
});
ovCtx.drawImage(
state.selected.image,
0,
0,
state.selected.image.width,
state.selected.image.height,
-state.selected.w / 2,
-state.selected.h / 2,
state.selected.w,
state.selected.h
);
ovCtx.restore();
state.originalDisplayLayer.clear();
state.originalDisplayLayer.ctx.save();
state.originalDisplayLayer.ctx.translate(scenter.x, scenter.y);
state.originalDisplayLayer.ctx.rotate(state.rotation);
state.originalDisplayLayer.ctx.drawImage(
state.selected.image,
0,
0,
state.selected.image.width,
state.selected.image.height,
-state.selected.w / 2,
-state.selected.h / 2,
state.selected.w,
state.selected.h
);
state.originalDisplayLayer.ctx.restore();
uiCtx.save();
const centerx = bbvp.x + bbvp.w / 2;
const centery = bbvp.y + bbvp.h / 2;
uiCtx.translate(centerx, centery);
uiCtx.rotate(state.rotation);
const matrixvp = uiCtx.getTransform();
const imatrixvp = matrixvp.invertSelf();
const cursorvp = imatrixvp.transformPoint({
x: evn.evn.clientX,
y: evn.evn.clientY,
});
// Draw selection box
uiCtx.strokeStyle = "#FFF";
uiCtx.setLineDash([4, 2]);
uiCtx.strokeRect(-bbvp.w / 2, -bbvp.h / 2, bbvp.w, bbvp.h);
uiCtx.setLineDash([]);
// Draw Scaling/Rotation Origin
uiCtx.beginPath();
uiCtx.arc(0, 0, 5, 0, 2 * Math.PI);
uiCtx.stroke();
// Draw Rotation Handle
let cursorInAnyHandle = false;
{
let radius = config.handleDrawSize / 2;
const dx = cursorvp.x;
const dy =
cursorvp.y - (-bbvp.h / 2 - config.rotateHandleDistance);
const dmax = config.handleDetectSize / 2;
if (dx * dx + dy * dy < dmax * dmax) {
cursorInAnyHandle ||= true;
radius *= config.handleDrawHoverScale;
}
uiCtx.beginPath();
uiCtx.moveTo(0, -bbvp.h / 2);
uiCtx.lineTo(
0,
-bbvp.h / 2 - config.rotateHandleDistance + radius
);
uiCtx.stroke();
uiCtx.beginPath();
uiCtx.arc(
0,
-bbvp.h / 2 - config.rotateHandleDistance,
radius,
0,
Math.PI * 2
);
uiCtx.stroke();
}
// Draw Scaling Handles
const drawHandle = (hx, hy) => {
// Handle Draw Range
let hs = config.handleDrawSize;
// Handle Detection Range
let dhs = config.handleDetectSize;
const handleBB = new BoundingBox({
x: hx - dhs / 2,
y: hy - dhs / 2,
w: dhs,
h: dhs,
});
const cursorInHandle = handleBB.contains(cursorvp.x, cursorvp.y);
cursorInAnyHandle ||= cursorInHandle;
if (cursorInHandle) hs *= config.handleDrawHoverScale;
uiCtx.strokeRect(hx - hs / 2, hy - hs / 2, hs, hs);
};
drawHandle(-bbvp.w / 2, -bbvp.h / 2);
drawHandle(bbvp.w / 2, -bbvp.h / 2);
drawHandle(-bbvp.w / 2, bbvp.h / 2);
drawHandle(bbvp.w / 2, bbvp.h / 2);
uiCtx.restore();
// Change cursor
if (
cursorInAnyHandle ||
(-bb.w / 2 < cursor.x &&
bb.w / 2 > cursor.x &&
-bb.h / 2 < cursor.y &&
bb.h / 2 > cursor.y)
)
imageCollection.inputElement.style.cursor = "pointer";
}
// Draw current cursor location
state.erasePrevCursor = _tool._cursor_draw(x, y);
uiCtx.restore();
}; };
// Handles left mouse clicks // Handles left mouse clicks
state.clickcb = (evn) => { state.clickcb = (evn) => {};
if (
!state.original ||
(state.originalLayer === uil.layer &&
state.original.x === state.selected.x &&
state.original.y === state.selected.y &&
state.original.w === state.selected.w &&
state.original.h === state.selected.h &&
state.rotation === 0)
) {
state.reset();
return;
}
// If something is selected, commit changes to the canvas
if (state.selected) {
state.originalLayer.ctx.drawImage(
state.selected.image,
state.original.x,
state.original.y
);
commands.runCommand("eraseImage", "Image Transform Erase", {
...state.original,
ctx: state.originalLayer.ctx,
});
// Use display layer as source
const {canvas, bb} = cropCanvas(state.originalDisplayLayer.canvas);
commands.runCommand("drawImage", "Image Transform Draw", {
image: canvas,
...bb,
});
state.reset(true);
}
};
// Handles left mouse drag events // Handles left mouse drag events
state.dragstartcb = (evn) => { state.dragstartcb = (evn) => {
let ix = evn.ix; if (state.selected && state.selected.hasCursor()) {
let iy = evn.iy; } else {
if (state.snapToGrid) { state.selection;
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) {
// Get transformation matrices
const bb = {
x: state.selected.x,
y: state.selected.y,
w: state.selected.w,
h: state.selected.h,
};
const bbvp = {
...viewport.canvasToView(bb.x, bb.y),
w: viewport.zoom * bb.w,
h: viewport.zoom * bb.h,
};
const ivp = viewport.canvasToView(evn.ix, evn.iy);
// Viewport Coordinates
uiCtx.save();
const centerx = bbvp.x + bbvp.w / 2;
const centery = bbvp.y + bbvp.h / 2;
uiCtx.translate(centerx, centery);
uiCtx.rotate(state.rotation);
const matrixvp = uiCtx.getTransform();
const imatrixvp = matrixvp.invertSelf();
const cursorvp = imatrixvp.transformPoint(ivp);
uiCtx.restore();
// World Coordinates
const scenter = state.selected.center();
ovCtx.save();
ovCtx.translate(scenter.x, scenter.y);
ovCtx.rotate(state.rotation);
const matrix = ovCtx.getTransform();
const imatrix = matrix.invertSelf();
ovCtx.restore();
// Check rotation handle
let rotationHandle = false;
const dx = cursorvp.x;
const dy = cursorvp.y - (-bbvp.h / 2 - config.rotateHandleDistance);
const dmax = config.handleDetectSize / 2;
if (dx * dx + dy * dy < dmax * dmax) rotationHandle = true;
// Check handles
let activeHandle = null;
const testHandle = (hx, hy, hwx, hwy) => {
// Handle Detection Range
let dhs = config.handleDetectSize;
const handleBB = new BoundingBox({
x: hx - dhs / 2,
y: hy - dhs / 2,
w: dhs,
h: dhs,
});
if (handleBB.contains(cursorvp.x, cursorvp.y)) {
activeHandle = state.selected.createHandle(hx, hy);
}
};
testHandle(-bbvp.w / 2, -bbvp.h / 2);
testHandle(bbvp.w / 2, -bbvp.h / 2);
testHandle(-bbvp.w / 2, bbvp.h / 2);
testHandle(bbvp.w / 2, bbvp.h / 2);
if (activeHandle) {
state.scaling = activeHandle;
} else if (rotationHandle) {
state.rotating = true;
} else if (state.selected.contains(evn.ix, evn.iy)) {
state.moving = {
snapOffset: {x: evn.ix - ix, y: evn.iy - iy},
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;
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 (state.scaling) {
state.selected.updateOriginal();
state.scaling = null;
// If we are moving the selection, just... stop
} else if (state.rotating) {
state.rotating = false;
} 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",
});
state.rotation = 0;
// Cut out selected portion of the image for manipulation
const cvs = document.createElement("canvas");
cvs.width = state.selected.w;
cvs.height = state.selected.h;
const ctx = cvs.getContext("2d");
ctx.drawImage(
uil.canvas,
state.selected.x,
state.selected.y,
state.selected.w,
state.selected.h,
0,
0,
state.selected.w,
state.selected.h
);
uil.ctx.clearRect(
state.selected.x,
state.selected.y,
state.selected.w,
state.selected.h
);
state.selected.image = cvs;
state.original.image = cvs;
if (state.selected.w === 0 || state.selected.h === 0)
state.selected = null;
state.dragging = null;
}
state.redraw();
};
// Handler for right clicks. Basically resets everything // Handler for right clicks. Basically resets everything
state.cancelcb = (evn) => { state.cancelcb = (evn) => {

View file

@ -8,7 +8,7 @@
<link href="../css/icons.css?v=caa702e" rel="stylesheet" /> <link href="../css/icons.css?v=caa702e" 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=a94b43d" rel="stylesheet" />
<link href="../css/ui/generic.css?v=4b9afe2" rel="stylesheet" /> <link href="../css/ui/generic.css?v=4b9afe2" rel="stylesheet" />