Skip to content
This repository has been archived by the owner on Nov 24, 2023. It is now read-only.

freehand selection integration or demo ? #23

Open
michaeljohnn opened this issue Mar 26, 2019 · 4 comments
Open

freehand selection integration or demo ? #23

michaeljohnn opened this issue Mar 26, 2019 · 4 comments

Comments

@michaeljohnn
Copy link

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.

@danilofuchs
Copy link
Contributor

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.

@dimitrius-ion
Copy link

dimitrius-ion commented Oct 4, 2019

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

@danilofuchs
Copy link
Contributor

@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.

@dimitrius-ion
Copy link

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

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants