test
Signed-off-by: Victor Seiji Hariki <victorseijih@gmail.com>
This commit is contained in:
parent
2c9eea4ce6
commit
b056f81155
10 changed files with 1139 additions and 556 deletions
|
@ -45,7 +45,7 @@
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#layer-overlay {
|
.overlay-canvas {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
17
index.html
17
index.html
|
@ -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"
|
||||||
|
|
|
@ -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,
|
||||||
|
|
||||||
|
|
|
@ -31,3 +31,5 @@ mouse.listen.world.onmousemove.on((evn) => {
|
||||||
debugCtx.fill();
|
debugCtx.fill();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.addEventListener("DOMContentLoaded", () => (global.debug = true));
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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
908
js/ui/tool/select.bkp.js
Normal 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",
|
||||||
|
}
|
||||||
|
);
|
|
@ -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) => {
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue