I am trying to add custom controls to the Line object. I found an interesting example that implements the behavior I want.
type ActionHandler = (
eventData: Readonly<MouseEvent>,
transform: Readonly<fabric.Transform>,
x: number,
y: number
) => boolean;
/**
* define a function that will define what the control does
* this function will be called on every mouse move after a control has been
* clicked and is being dragged.
* The function receive as argument the mouse event, the current trasnform object
* and the current position in canvas coordinate
* transform.target is a reference to the current object being transformed,
*/
const actionHandler: ActionHandler = (eventData, transform, x, y) => {
const line = transform.target as fabric.Line;
// @ts-ignore
const currentControl = line.controls[line.__corner];
const cursorPosition = { x, y };
const dimensions = line._getNonTransformedDimensions();
const size = line._getTransformedDimensions(0, 0);
const finalPointPosition = new fabric.Point(
(cursorPosition.x * dimensions.x) / size.x,
(cursorPosition.y * dimensions.y) / size.y
);
// @ts-ignore
if (currentControl.pointIndex === 0) {
line.set({
x1: finalPointPosition.x,
y1: finalPointPosition.y
});
}
// @ts-ignore
if (currentControl.pointIndex === 1) {
line.set({ x2: cursorPosition.x, y2: cursorPosition.y });
}
return true;
};
/**
* define a function that can keep the line in the same position when we change its
* width/height/top/left.
* @param {number} anchorIndex - index of the anchor
* @param {(eventData: fabric.IEvent, transform: { target: fabric.Object }, x: number, y: number) => boolean} fn - action function
* @return {(eventData: fabric.IEvent, transform: { target: fabric.Object }, x: number, y: number) => boolean} if the action was performed
*/
// eslint-disable-next-line max-lines-per-function
function anchorWrapper(anchorIndex: number, func: ActionHandler) {
// eslint-disable-next-line max-params, max-lines-per-function
const callback: ActionHandler = (eventData, transform, x, y) => {
const actionPerformed = func(eventData, transform, x, y);
return actionPerformed;
};
return callback;
}
// define a function that can locate the controls.
// this function will be used both for drawing and for interaction.
function linePositionHandler(
_dim: {
readonly x: number;
readonly y: number;
},
_finalMatrix: readonly number[],
line: fabric.Line
): fabric.Point {
const points = line.calcLinePoints();
const matrix = line.calcTransformMatrix();
const point1 = fabric.util.transformPoint(
{ x: points.x1, y: points.y1 } as fabric.Point,
matrix
);
const point2 = fabric.util.transformPoint(
{ x: points.x2, y: points.y2 } as fabric.Point,
matrix
);
return fabric.util.transformPoint(this.pointIndex === 0 ? point1 : point2, [
...line.getViewportTransform()
]);
}
/**
* Apply the polygon controls to the object
*/
// eslint-disable-next-line max-lines-per-function
export const applyControls = <T extends fabric.Line>(object: T): T => {
object.cornerSize = 44;
object.hasBorders = false;
// object.lockMovementX = true
// object.lockMovementY = true
object.controls = [0, 1].reduce(function (
accumulator: Record<string, fabric.Control>,
_point,
index
) {
// eslint-disable-next-line fp/no-mutation, functional/immutable-data
accumulator["p" + index] = new fabric.Control({
positionHandler: linePositionHandler,
actionHandler: actionHandler,
actionName: "modifyLine",
// Custom property
// @ts-ignore
pointIndex: index
});
return accumulator;
},
{});
return object;
};
fabric.Object.prototype.strokeWidth = 1;
fabric.Object.prototype.borderScaleFactor = 3;
fabric.Object.prototype.transparentCorners = false;
fabric.Object.prototype.cornerSize = 44;
fabric.Object.prototype.cornerColor = "cyan";
// fabric.Object.prototype.touchCornerSize = 44;
fabric.Object.prototype.cornerStyle = "circle";
fabric.Object.prototype.padding = 22;
// Use up to this fraction digits for numbers inside objects
// fabric.Object.NUM_FRACTION_DIGITS = 8;
window.onload = function() {
const canvas = new fabric.Canvas("canvas1", {
width: window.innerWidth,
height: window.innerHeight
});
window.addEventListener("resize", () => {
canvas.setWidth(window.innerWidth);
canvas.setHeight(window.innerHeight);
});
const DEFAULT_LINE_OPTIONS = {
stroke: "cyan",
strokeWidth: 4,
strokeLineCap: "round",
transparentCorners: true,
// objectCaching: false,
perPixelTargetFind: true
// lockMovementX: true,
// lockMovementY: true
};
const createLine = (opts: fabric.IObjectOptions & fabric.ILineOptions) => (
points: [number, number, number, number]
) => new fabric.Line(points, { ...DEFAULT_LINE_OPTIONS, ...opts });
const line = createLine({})([0, 0, 110, 110]);
canvas.absolutePan({ x: -100, y: -100 });
canvas.setZoom(2);
canvas.add(applyControls(line));
canvas.setActiveObject(line);
canvas.add(applyControls(createLine({})([29, 39, 129, 198])));
}
https://codepen.io/rockerboo/pen/KKQKePo
However, there is a problem that I can’t solve. It concerns the situation when I first move the line and then try to drag any of the controles. In that case, the line jumps to the position before the control was dragged.
How can I improve it?