back to original transform tool state

But way more flexible implementation

Signed-off-by: Victor Seiji Hariki <victorseijih@gmail.com>
This commit is contained in:
Victor Seiji Hariki 2023-01-12 01:00:06 -03:00
parent d7e20f87dc
commit 35a62ae9fe
6 changed files with 499 additions and 87 deletions

View file

@ -341,17 +341,17 @@
<script src="js/global.js?v=5ee46bd" type="text/javascript"></script>
<!-- Base Libs -->
<script src="js/lib/util.js?v=39564fb" type="text/javascript"></script>
<script src="js/lib/util.js?v=49a78a6" type="text/javascript"></script>
<script src="js/lib/events.js?v=2ab7933" type="text/javascript"></script>
<script src="js/lib/input.js?v=09298ac" type="text/javascript"></script>
<script src="js/lib/layers.js?v=a1f8aea" type="text/javascript"></script>
<script src="js/lib/commands.js?v=bf23c83" type="text/javascript"></script>
<script src="js/lib/toolbar.js?v=ca3fccf" type="text/javascript"></script>
<script src="js/lib/toolbar.js?v=b3ffbff" type="text/javascript"></script>
<script src="js/lib/ui.js?v=76ede2b" type="text/javascript"></script>
<script
src="js/initalize/layers.populate.js?v=938a1b2"
src="js/initalize/layers.populate.js?v=fcca23e"
type="text/javascript"></script>
<!-- Configuration -->
@ -370,7 +370,7 @@
<!-- Load Tools -->
<script
src="js/ui/tool/generic.js?v=d63d268"
src="js/ui/tool/generic.js?v=9f1f84e"
type="text/javascript"></script>
<script src="js/ui/tool/dream.js?v=76bc565" type="text/javascript"></script>
@ -381,7 +381,7 @@
src="js/ui/tool/colorbrush.js?v=3f8c01a"
type="text/javascript"></script>
<script
src="js/ui/tool/select.js?v=44e8ec4"
src="js/ui/tool/select.js?v=080237c"
type="text/javascript"></script>
<script src="js/ui/tool/stamp.js?v=3c07ac8" type="text/javascript"></script>
<script

View file

@ -322,7 +322,8 @@ mouse.listen.camera.onwheel.on((evn) => {
//viewport.transform(imageCollection.element);
toolbar.currentTool.redraw();
toolbar._current_tool.state.redrawui &&
toolbar._current_tool.state.redrawui();
});
const cameraPaintStart = (evn) => {
@ -348,6 +349,9 @@ const cameraPaint = (evn) => {
}
viewport.transform(imageCollection.element);
toolbar._current_tool.state.redrawui &&
toolbar._current_tool.state.redrawui();
if (global.debug) {
debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height);
debugCtx.fillStyle = "#F0F";

View file

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

View file

@ -37,6 +37,16 @@ class BoundingBox {
return {x: this.x, y: this.y};
}
/** @type {Point} */
get tr() {
return {x: this.x + this.w, y: this.y};
}
/** @type {Point} */
get bl() {
return {x: this.x, y: this.y + this.h};
}
/** @type {Point} */
get br() {
return {x: this.x + this.w, y: this.y + this.h};

View file

@ -315,12 +315,40 @@ const _tool = {
/** @type {HTMLCanvasElement} */
canvas;
_dirty = false;
_position = {x: 0, y: 0};
/**
* @type {Point}
*/
position = {x: 0, y: 0};
scale = {x: 1, y: 1};
rotation = 0;
get position() {
return this._position;
}
set position(v) {
this._dirty = true;
this._position = v;
}
_scale = {x: 1, y: 1};
/**
* @type {Point}
*/
get scale() {
return this._scale;
}
set scale(v) {
if (v.x === 0 || v.y === 0) return;
this._dirty = true;
this._scale = v;
}
_rotation = 0;
get rotation() {
return this._rotation;
}
set rotation(v) {
this._dirty = true;
this._rotation = v;
}
/**
* @param {HTMLCanvasElement} canvas Selected image canvas
@ -331,14 +359,28 @@ const _tool = {
this.position = position;
}
get matrix() {
/** @type {DOMMatrix} */
_rtmatrix = null;
get rtmatrix() {
if (!this._rtmatrix || this._dirty) {
const m = new DOMMatrix();
m.scaleSelf(this.scale.x, this.scale.y);
m.rotateSelf((this.rotation * 180) / Math.PI);
m.translateSelf(this.position.x, this.position.y);
m.rotateSelf((this.rotation * 180) / Math.PI);
return m;
this._rtmatrix = m;
}
return this._rtmatrix;
}
/** @type {DOMMatrix} */
_matrix = null;
get matrix() {
if (!this._matrix || this._dirty) {
this._matrix = this.rtmatrix.scaleSelf(this.scale.x, this.scale.y);
}
return this._matrix;
}
/**
@ -356,44 +398,201 @@ const _tool = {
);
}
hoveringHandle(x, y) {
const localbb = new BoundingBox({
x: -this.canvas.width / 2,
y: -this.canvas.height / 2,
w: this.canvas.width,
h: this.canvas.height,
});
const localc = this.matrix.inverse().transformPoint({x, y});
const ontl =
Math.max(
Math.abs(localc.x - localbb.tl.x),
Math.abs(localc.y - localbb.tl.y)
) <
config.handleDetectSize / 2;
const ontr =
Math.max(
Math.abs(localc.x - localbb.tr.x),
Math.abs(localc.y - localbb.tr.y)
) <
config.handleDetectSize / 2;
const onbl =
Math.max(
Math.abs(localc.x - localbb.bl.x),
Math.abs(localc.y - localbb.bl.y)
) <
config.handleDetectSize / 2;
const onbr =
Math.max(
Math.abs(localc.x - localbb.br.x),
Math.abs(localc.y - localbb.br.y)
) <
config.handleDetectSize / 2;
return {onHandle: ontl || ontr || onbl || onbr, ontl, ontr, onbl, onbr};
}
hoveringBox(x, y) {
const localbb = new BoundingBox({
x: -this.canvas.width / 2,
y: -this.canvas.height / 2,
w: this.canvas.width,
h: this.canvas.height,
});
const localc = this.matrix.inverse().transformPoint({x, y});
return (
!this.hoveringHandle(x, y).onHandle &&
localbb.contains(localc.x, localc.y)
);
}
/**
* Draws the marquee selector box
*
* @param {CanvasRenderingContext2D} context A context for rendering the box to
* @param {Point} cursor Cursor position
* @param {DOMMatrix} transform A transformation matrix to transform the position by
*/
drawBox(context, transform = new DOMMatrix()) {
context.save();
drawBox(context, cursor, transform = new DOMMatrix()) {
const m = transform.multiply(this.matrix);
context.setTransform(m);
// Draw the box itself
context.save();
const localbb = new BoundingBox({
x: -this.canvas.width / 2,
y: -this.canvas.height / 2,
w: this.canvas.width,
h: this.canvas.height,
});
// Line Style
context.strokeStyle = "#FFF";
context.lineWidth = 2;
context.setLineDash([4, 2]);
const tl = m.transformPoint(localbb.tl);
const tr = m.transformPoint(localbb.tr);
const bl = m.transformPoint(localbb.bl);
const br = m.transformPoint(localbb.br);
const bbc = m.transformPoint({x: 0, y: 0});
context.beginPath();
context.strokeRect(
context.arc(bbc.x, bbc.y, 5, 0, Math.PI * 2);
context.stroke();
context.setLineDash([4, 2]);
// Draw main rectangle
context.beginPath();
context.moveTo(tl.x, tl.y);
context.lineTo(tr.x, tr.y);
context.lineTo(br.x, br.y);
context.lineTo(bl.x, bl.y);
context.lineTo(tl.x, tl.y);
context.stroke();
// Draw handles
const drawHandle = (pt, hover) => {
let hsz = config.handleDrawSize / 2;
if (hover) hsz *= config.handleDrawHoverScale;
const hm = new DOMMatrix().rotateSelf(this.rotation);
const htl = hm.transformPoint({x: -hsz, y: -hsz});
const htr = hm.transformPoint({x: hsz, y: -hsz});
const hbr = hm.transformPoint({x: hsz, y: hsz});
const hbl = hm.transformPoint({x: -hsz, y: hsz});
context.beginPath();
context.moveTo(htl.x + pt.x, htl.y + pt.y);
context.lineTo(htr.x + pt.x, htr.y + pt.y);
context.lineTo(hbr.x + pt.x, hbr.y + pt.y);
context.lineTo(hbl.x + pt.x, hbl.y + pt.y);
context.lineTo(htl.x + pt.x, htl.y + pt.y);
context.stroke();
};
context.strokeStyle = "#FFF";
context.lineWidth = 2;
context.setLineDash([]);
const {ontl, ontr, onbl, onbr} = this.hoveringHandle(cursor.x, cursor.y);
drawHandle(tl, ontl);
drawHandle(tr, ontr);
drawHandle(bl, onbl);
drawHandle(br, onbr);
context.restore();
return () => {
const border = config.handleDrawSize * config.handleDrawHoverScale;
const minx = Math.min(tl.x, tr.x, bl.x, br.x) - border;
const maxx = Math.max(tl.x, tr.x, bl.x, br.x) + border;
const miny = Math.min(tl.y, tr.y, bl.y, br.y) - border;
const maxy = Math.max(tl.y, tr.y, bl.y, br.y) + border;
context.clearRect(minx, miny, maxx - minx, maxy - miny);
};
}
/**
* Draws the selected image
*
* @param {CanvasRenderingContext2D} context A context for rendering the image to
* @param {CanvasRenderingContext2D} peekctx A context for rendering the layer peeking to
* @param {object} options
* @param {DOMMatrix} options.transform A transformation matrix to transform the position by
* @param {number} options.opacity Opacity of the peek display
*/
drawImage(context, peekctx, options = {}) {
defaultOpt(options, {
transform: new DOMMatrix(),
opacity: 0.4,
});
context.save();
peekctx.save();
const m = options.transform.multiply(this.matrix);
// Draw image
context.setTransform(m);
context.drawImage(
this.canvas,
-this.canvas.width / 2,
-this.canvas.height / 2,
this.canvas.width,
this.canvas.height
);
context.stroke();
context.restore();
// Draw peek
peekctx.filter = `opacity(${options.opacity * 100}%)`;
peekctx.setTransform(m);
peekctx.drawImage(
this.canvas,
-this.canvas.width / 2,
-this.canvas.height / 2,
this.canvas.width,
this.canvas.height
);
peekctx.restore();
context.restore();
return () => {
context.save();
// Here we only save transform for performance
const pt = context.getTransform();
const ppt = context.getTransform();
context.setTransform(m);
peekctx.setTransform(m);
context.clearRect(
-this.canvas.width / 2 - 10,
@ -402,46 +601,16 @@ const _tool = {
this.canvas.height + 20
);
context.restore();
peekctx.clearRect(
-this.canvas.width / 2 - 10,
-this.canvas.height / 2 - 10,
this.canvas.width + 20,
this.canvas.height + 20
);
context.setTransform(pt);
peekctx.setTransform(ppt);
};
}
/**
* 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;

View file

@ -34,6 +34,12 @@ const selectTransformTool = () =>
keyboard.onShortcut({ctrl: true, key: "KeyX"}, state.ctrlxcb);
state.selected = null;
// Register Layer
state.originalDisplayLayer = imageCollection.registerLayer(null, {
after: uil.layer,
category: "select-display",
});
},
(state, opt) => {
// Clear all those listeners and shortcuts we set up
@ -61,6 +67,10 @@ const selectTransformTool = () =>
// Clears overlay
imageCollection.inputElement.style.cursor = "auto";
// Delete Layer
imageCollection.deleteLayer(state.originalDisplayLayer);
state.originalDisplayLayer = null;
},
{
init: (state) => {
@ -104,15 +114,14 @@ const selectTransformTool = () =>
// Clears selection and make things right
state.reset = (erase = false) => {
if (state.selected && !erase)
state.originalLayer.ctx.drawImage(
state.original.image,
state.original.layer.ctx.drawImage(
state.selected.canvas,
state.original.x,
state.original.y
);
if (state.originalDisplayLayer) {
imageCollection.deleteLayer(state.originalDisplayLayer);
state.originalDisplayLayer = null;
state.originalDisplayLayer.clear();
}
if (state.dragging) state.dragging = null;
@ -120,6 +129,7 @@ const selectTransformTool = () =>
state.rotation = 0;
state.original = null;
state.moving = false;
state.redraw();
};
@ -127,23 +137,70 @@ const selectTransformTool = () =>
// Selection Handlers
const selection = _tool._draggable_selection(state);
// Mouse Move Handler
/** @type {{selected: Point, offset: Point} | null} */
let moving = null;
/** @type {{handle: Point} | null} */
let scaling = null;
// UI Erasers
let eraseSelectedBox = () => null;
let eraseSelectedImage = () => null;
let eraseCursor = () => null;
let eraseSelection = () => null;
// Redraw UI
state.redrawui = () => {
// Get cursor positions
const {x, y, sx, sy} = _tool._process_cursor(
state.lastMouseMove,
state.snapToGrid
);
eraseSelectedBox();
if (state.selected) {
eraseSelectedBox = state.selected.drawBox(
uiCtx,
{x, y},
viewport.c2v
);
}
};
// Mouse Move Handler
state.movecb = (evn) => {
state.lastMouseMove = evn;
// Get cursor positions
const {x, y, sx, sy} = _tool._process_cursor(evn, state.snapToGrid);
// Erase Cursor
eraseSelectedBox();
eraseSelectedImage();
eraseSelection();
eraseCursor();
imageCollection.inputElement.style.cursor = "default";
// Draw Box and Selected Image
if (state.selected) {
eraseSelectedBox = state.selected.drawBox(uiCtx, viewport.c2v);
eraseSelectedBox = state.selected.drawBox(
uiCtx,
{x, y},
viewport.c2v
);
if (
state.selected.hoveringBox(x, y) ||
state.selected.hoveringHandle(x, y).onHandle
) {
imageCollection.inputElement.style.cursor = "pointer";
}
eraseSelectedImage = state.selected.drawImage(
state.originalDisplayLayer.ctx,
ovCtx,
{opacity: state.selectionPeekOpacity / 100}
);
}
// Draw Selection
@ -171,42 +228,208 @@ const selectTransformTool = () =>
// Draw cursor
eraseCursor = _tool._cursor_draw(sx, sy);
// Pointer
if (state.selected && state.selected.contains(x, y)) {
imageCollection.inputElement.style.cursor = "pointer";
}
};
// Handles left mouse clicks
state.clickcb = (evn) => {};
state.clickcb = (evn) => {
if (
state.selected &&
!(
state.selected.rotation === 0 &&
state.selected.scale.x === 1 &&
state.selected.scale.y === 1 &&
state.selected.position.x === state.original.sx &&
state.selected.position.y === state.original.sy &&
state.original.layer === uil.layer
)
) {
// Put original image back
state.original.layer.ctx.drawImage(
state.selected.canvas,
state.original.x,
state.original.y
);
// Erase Original Selection Area
commands.runCommand("eraseImage", "Transform Tool Erase", {
ctx: state.original.layer.ctx,
x: state.original.x,
y: state.original.y,
w: state.selected.canvas.width,
h: state.selected.canvas.height,
});
// Draw Image
const {canvas, bb} = cropCanvas(state.originalDisplayLayer.canvas, {
border: 10,
});
commands.runCommand("drawImage", "Transform Tool Apply", {
image: canvas,
...bb,
});
}
state.reset(true);
};
// Handles left mouse drag start events
state.dragstartcb = (evn) => {
const {
x: ix,
y: iy,
sx: six,
sy: siy,
} = _tool._process_cursor({x: evn.ix, y: evn.iy}, state.snapToGrid);
const {x, y, sx, sy} = _tool._process_cursor(evn, state.snapToGrid);
if (state.selected) {
const hoveringBox = state.selected.hoveringBox(ix, iy);
const hoveringHandle = state.selected.hoveringHandle(ix, iy);
const localc = state.selected.matrix
.inverse()
.transformPoint({x: ix, y: iy});
if (hoveringBox) {
// Start dragging
moving = {
selected: state.selected.position,
offset: {
x: six - state.selected.position.x,
y: siy - state.selected.position.y,
},
};
return;
} else if (hoveringHandle.onHandle) {
// Start scaling
let handle = {x: 0, y: 0};
const lbb = new BoundingBox({
x: -state.selected.canvas.width / 2,
y: -state.selected.canvas.height / 2,
w: state.selected.canvas.width,
h: state.selected.canvas.height,
});
if (hoveringHandle.ontl) {
handle = lbb.tl;
} else if (hoveringHandle.ontr) {
handle = lbb.tr;
} else if (hoveringHandle.onbl) {
handle = lbb.bl;
} else {
handle = lbb.br;
}
scaling = {
handle,
};
return;
}
}
selection.dragstartcb(evn);
};
// Handles left mouse drag events
state.dragcb = (evn) => {
selection.dragcb(evn);
const {x, y, sx, sy} = _tool._process_cursor(evn, state.snapToGrid);
if (state.selected) {
if (moving) {
state.selected.position = {
x: sx - moving.offset.x,
y: sy - moving.offset.y,
};
}
if (scaling) {
/** @type {DOMMatrix} */
const m = state.selected.rtmatrix.invertSelf();
const lscursor = m.transformPoint({x: sx, y: sy});
const xs = lscursor.x / scaling.handle.x;
const xy = lscursor.y / scaling.handle.y;
if (!state.keepAspectRatio) state.selected.scale = {x: xs, y: xy};
else {
const scale = Math.max(xs, xy);
state.selected.scale = {x: scale, y: scale};
}
}
}
if (selection.exists) selection.dragcb(evn);
};
// Handles left mouse drag end events
state.dragendcb = (evn) => {
selection.dragendcb(evn);
const {x, y, sx, sy} = _tool._process_cursor(evn, state.snapToGrid);
if (selection.exists) {
if (selection.bb.w !== 0 && selection.bb.h !== 0) {
// TODO: CHANGE THIS
state.selected = new _tool.MarqueeSelection(
uil.getVisible(selection.bb),
selection.bb.center
selection.dragendcb(evn);
const bb = selection.bb;
state.reset();
if (selection.exists && bb.w !== 0 && bb.h !== 0) {
const canvas = document.createElement("canvas");
canvas.width = bb.w;
canvas.height = bb.h;
canvas
.getContext("2d")
.drawImage(
uil.canvas,
bb.x,
bb.y,
bb.w,
bb.h,
0,
0,
bb.w,
bb.h
);
uil.ctx.clearRect(bb.x, bb.y, bb.w, bb.h);
state.original = {
...bb,
sx: selection.bb.center.x,
sy: selection.bb.center.y,
layer: uil.layer,
};
state.selected = new _tool.MarqueeSelection(canvas, bb.center);
}
selection.deselect();
}
if (state.selected) {
if (moving) {
state.selected.position = {
x: sx - moving.offset.x,
y: sy - moving.offset.y,
};
}
if (scaling) {
/** @type {DOMMatrix} */
const m = state.selected.rtmatrix.invertSelf();
const lscursor = m.transformPoint({x: sx, y: sy});
const xs = lscursor.x / scaling.handle.x;
const xy = lscursor.y / scaling.handle.y;
if (!state.keepAspectRatio) state.selected.scale = {x: xs, y: xy};
else {
const scale = Math.max(xs, xy);
state.selected.scale = {x: scale, y: scale};
}
}
}
moving = null;
scaling = null;
state.redraw();
};