react-image-annotation icon indicating copy to clipboard operation
react-image-annotation copied to clipboard

freehand selection integration or demo ?

Open korbinzhao opened this issue 5 years ago • 4 comments

Hi, in my opinion, freehand selection is a common requirement in image annotation. And i can see, react-image-annotation provide a method for freehand selection realization in the doc. Then, further than that, is it possible to integrate the freehand selection to react-image-annotation immediately or provide a demo showing how to achieve it immediately ? Looking forward to your help, thx a lot.

korbinzhao avatar Mar 26 '19 14:03 korbinzhao

The way I accomplished through a custom Selector was to create a svg element on top of Annotation and then create paths that follow the datapoints.

danilofuchs avatar Mar 30 '19 23:03 danilofuchs

@danilofuchs hi, do you mind sharing your selector code? i couldn't find it in your fork

dimitrius-ion avatar Oct 04 '19 16:10 dimitrius-ion

@dimitrius1986 Unfortunately, the code in which I implemented this functionality is private. I can, however, paste this snippet of how it was implemented:

import { IAnnotation, IGeometry } from "react-image-annotation";
interface IPoint {
  x: number;
  y: number;
}
export interface IGeometryDraw extends IGeometry {
  coordinates: IPoint[];
  x: number;
  y: number;
  boxX: number;
  boxY: number;
  boxHeight: number;
  boxWidth: number;
  type: string;
}
interface IAnotacaoDraw extends IAnnotation {
  geometry: IGeometryDraw;
}
const MARGIN = 12;

const marginToPercentage = (container: { width: number; height: number }) => ({
  marginX: (MARGIN / container.width) * 100,
  marginY: (MARGIN / container.height) * 100
});

export const TYPE = "DRAWING";

export function intersects(
  { x, y }: { x: number; y: number },
  geometry: IGeometryDraw,
  container: { width: number; height: number }
): boolean {
  const { marginX, marginY } = marginToPercentage(container);
  if (x < geometry.boxX - marginX) {
    return false;
  }
  if (y < geometry.boxY - marginY) {
    return false;
  }
  if (x > geometry.boxX + geometry.boxWidth + marginX) {
    return false;
  }

  if (y > geometry.boxY + geometry.boxHeight + marginY) {
    return false;
  }

  return true;
}

export function area(
  geometry: IGeometryDraw,
  container: { width: number; height: number }
): number {
  return geometry.boxHeight * geometry.boxWidth;
}

export const methods = {
  onMouseDown: (annotation: IAnotacaoDraw, e: any) => {
    return onPointerDown(annotation, e);
  },
  onMouseMove: (annotation: IAnotacaoDraw, e: any) => {
    return onPointerMove(annotation, e);
  },
  onMouseUp: (annotation: IAnotacaoDraw, e: any) => {
    return onPointerUp(annotation, e);
  },
  onTouchStart: (annotation: IAnotacaoDraw, e: any) => {
    return onPointerDown(annotation, e);
  },
  onTouchMove: (annotation: IAnotacaoDraw, e: any) => {
    return onPointerMove(annotation, e);
  },
  onTouchEnd: (annotation: IAnotacaoDraw, e: any) => {
    return onPointerUp(annotation, e);
  }
};

function onPointerDown(annotation: IAnotacaoDraw, e: any) {
  if (!annotation.geometry) {
    const newPoint = relativeCoordinatesForEvent(e);
    return {
      ...annotation,
      selection: {
        ...annotation.selection,
        showEditor: false,
        mode: "SELECTING"
      },
      geometry: {
        coordinates: [],
        x: newPoint.x,
        y: newPoint.y,
        boxX: newPoint.x,
        boxY: newPoint.y,
        boxHeight: 0,
        boxWidth: 0,
        type: TYPE
      },
      data: {
        id: Math.random()
      }
    };
  } else {
    return {};
  }
}

function onPointerMove(annotation: IAnotacaoDraw, e: any) {
  if (annotation.selection && annotation.selection.mode === "SELECTING") {
    let { y, boxX, boxY, boxHeight, boxWidth } = annotation.geometry;
    const newPoint = relativeCoordinatesForEvent(e);
    if (newPoint.y < y || !y) {
      y = newPoint.y;
    }
    if (newPoint.y < boxY || !boxY) {
      boxHeight += boxY - newPoint.y;
      boxY = newPoint.y;
    } else if (newPoint.y > boxY + boxHeight || !boxHeight) {
      boxHeight = newPoint.y - boxY;
    }
    if (newPoint.x < boxX || !boxX) {
      boxWidth += boxX - newPoint.x;
      boxX = newPoint.x;
    } else if (newPoint.x > boxX + boxWidth || !boxWidth) {
      boxWidth = newPoint.x - boxX;
    }

    const middle = annotation.geometry.coordinates.reduce(
      (prev, curr) => ({
        x: prev.x + curr.x,
        y: prev.y + curr.y
      }),
      { x: 0, y: 0 }
    );
    middle.x /= annotation.geometry.coordinates.length;
    middle.y /= annotation.geometry.coordinates.length;

    return {
      ...annotation,
      selection: {
        ...annotation.selection,
        showEditor: false,
        mode: "SELECTING"
      },
      geometry: {
        coordinates: [
          ...annotation.geometry.coordinates,
          relativeCoordinatesForEvent(e)
        ],
        x: middle.x,
        y,
        boxX,
        boxY,
        boxHeight,
        boxWidth,
        // ...getCoordPercentage(e),
        type: TYPE
      }
    };
  } else {
    return annotation;
  }
}
function onPointerUp(annotation: IAnotacaoDraw, e: any) {
  if (annotation.selection) {
    const { geometry } = annotation;

    if (!geometry) {
      return {};
    }

    switch (annotation.selection.mode) {
      case "SELECTING":
        return {
          ...annotation,
          selection: {
            ...annotation.selection,
            showEditor: true,
            mode: "EDITING"
          }
        };
      default:
        break;
    }
  }

  return annotation;
}

function relativeCoordinatesForEvent(e: any): IPoint {
  if (isTouchEvent(e)) {
    if (isValidTouchEvent(e)) {
      e.preventDefault();
      return getTouchRelativeCoordinates(e);
    } else {
      return {
        x: 0,
        y: 0
      };
    }
  } else {
    return getMouseRelativeCoordinates(e);
  }
}

const isTouchEvent = (e: any) => e.targetTouches !== undefined;
const isValidTouchEvent = (e: any) => e.targetTouches.length === 1;
const getTouchRelativeCoordinates = (e: any) => {
  const touch = e.targetTouches[0];
  const boundingRect = e.currentTarget.getBoundingClientRect();
  // https://idiallo.com/javascript/element-postion
  // https://stackoverflow.com/questions/25630035/javascript-getboundingclientrect-changes-while-scrolling
  const offsetX = touch.pageX - boundingRect.left;
  const offsetY = touch.pageY - (boundingRect.top + window.scrollY);

  return {
    x: (offsetX / boundingRect.width) * 100,
    y: (offsetY / boundingRect.height) * 100
  };
};
function getMouseRelativeCoordinates(e: any) {
  return {
    x: (e.nativeEvent.offsetX / e.currentTarget.offsetWidth) * 100,
    y: (e.nativeEvent.offsetY / e.currentTarget.offsetHeight) * 100
  };
}

export default {
  TYPE,
  intersects,
  area,
  methods
};

Please desconsider any Typescript types if you intend to use this library with plain js.

danilofuchs avatar Oct 04 '19 16:10 danilofuchs

oh wow thank you so much :) you are the best!!!!

dimitrius-ion avatar Oct 04 '19 16:10 dimitrius-ion