+ {rotatable && (
+
+ )}
+
+ {direction.map((d) => {
+ const cursor = `${getCursor(
+ rotateAngle + parentRotateAngle,
+ d
+ )}-resize`;
+ return (
+
this.startResize(e, cursor)}
+ />
+ );
+ })}
+
+ {direction.map((d) => {
+ return
;
+ })}
+
+ );
+ }
+}
diff --git a/packages/annotation-toolkit/src/layers/RRRR/index.js b/packages/annotation-toolkit/src/layers/RRRR/index.js
new file mode 100644
index 000000000..24037e717
--- /dev/null
+++ b/packages/annotation-toolkit/src/layers/RRRR/index.js
@@ -0,0 +1,140 @@
+import React, { Component } from "react";
+import Rect from "./Rect";
+import { centerToTL, tLToCenter, getNewStyle, degToRadian } from "./utils";
+
+export default class ResizableRect extends Component {
+ // static propTypes = {
+ // left: PropTypes.number.isRequired,
+ // top: PropTypes.number.isRequired,
+ // width: PropTypes.number.isRequired,
+ // height: PropTypes.number.isRequired,
+ // rotatable: PropTypes.bool,
+ // rotateAngle: PropTypes.number,
+ // parentRotateAngle: PropTypes.number,
+ // zoomable: PropTypes.string,
+ // minWidth: PropTypes.number,
+ // minHeight: PropTypes.number,
+ // aspectRatio: PropTypes.oneOfType([
+ // PropTypes.number,
+ // PropTypes.bool
+ // ]),
+ // onRotateStart: PropTypes.func,
+ // onRotate: PropTypes.func,
+ // onRotateEnd: PropTypes.func,
+ // onResizeStart: PropTypes.func,
+ // onResize: PropTypes.func,
+ // onResizeEnd: PropTypes.func,
+ // onDragStart: PropTypes.func,
+ // onDrag: PropTypes.func,
+ // onDragEnd: PropTypes.func
+ // }
+
+ static defaultProps = {
+ parentRotateAngle: 0,
+ rotateAngle: 0,
+ rotatable: true,
+ zoomable: "",
+ minWidth: 10,
+ minHeight: 10,
+ };
+
+ handleRotate = (angle, startAngle) => {
+ if (!this.props.onRotate) return;
+ let rotateAngle = Math.round(startAngle + angle);
+ if (rotateAngle >= 360) {
+ rotateAngle -= 360;
+ } else if (rotateAngle < 0) {
+ rotateAngle += 360;
+ }
+ if (rotateAngle > 356 || rotateAngle < 4) {
+ rotateAngle = 0;
+ } else if (rotateAngle > 86 && rotateAngle < 94) {
+ rotateAngle = 90;
+ } else if (rotateAngle > 176 && rotateAngle < 184) {
+ rotateAngle = 180;
+ } else if (rotateAngle > 266 && rotateAngle < 274) {
+ rotateAngle = 270;
+ }
+ this.props.onRotate(rotateAngle);
+ };
+
+ handleResize = (length, alpha, rect, type, isShiftKey) => {
+ if (!this.props.onResize) return;
+ const {
+ rotateAngle,
+ aspectRatio,
+ minWidth,
+ minHeight,
+ parentRotateAngle,
+ } = this.props;
+ const beta = alpha - degToRadian(rotateAngle + parentRotateAngle);
+ const deltaW = length * Math.cos(beta);
+ const deltaH = length * Math.sin(beta);
+ const ratio =
+ isShiftKey && !aspectRatio ? rect.width / rect.height : aspectRatio;
+ const {
+ position: { centerX, centerY },
+ size: { width, height },
+ } = getNewStyle(
+ type,
+ { ...rect, rotateAngle },
+ deltaW,
+ deltaH,
+ ratio,
+ minWidth,
+ minHeight
+ );
+
+ this.props.onResize(
+ centerToTL({ centerX, centerY, width, height, rotateAngle }),
+ isShiftKey,
+ type
+ );
+ };
+
+ handleDrag = (deltaX, deltaY) => {
+ this.props.onDrag && this.props.onDrag(deltaX, deltaY);
+ };
+
+ render() {
+ const {
+ document,
+ top,
+ left,
+ width,
+ height,
+ rotateAngle,
+ parentRotateAngle,
+ zoomable,
+ rotatable,
+ onRotate,
+ onResizeStart,
+ onResizeEnd,
+ onRotateStart,
+ onRotateEnd,
+ onDragStart,
+ onDragEnd,
+ } = this.props;
+
+ const styles = tLToCenter({ top, left, width, height, rotateAngle });
+
+ return (
+
+ );
+ }
+}
diff --git a/packages/annotation-toolkit/src/layers/RRRR/react-rect.css b/packages/annotation-toolkit/src/layers/RRRR/react-rect.css
new file mode 100644
index 000000000..a0cc823e5
--- /dev/null
+++ b/packages/annotation-toolkit/src/layers/RRRR/react-rect.css
@@ -0,0 +1,91 @@
+.movable-rect-frame > div {
+ position: absolute;
+ border: 1px solid #eb5648;
+}
+
+.movable-rect-frame .square {
+ position: absolute;
+ width: 7px;
+ height: 7px;
+ background: white;
+ border: 1px solid #eb5648;
+ border-radius: 1px;
+}
+.movable-rect-frame .resizable-handler {
+ position: absolute;
+ width: 14px;
+ height: 14px;
+ cursor: pointer;
+ z-index: 1;
+}
+.movable-rect-frame .resizable-handler.tl,
+.movable-rect-frame .resizable-handler.t,
+.movable-rect-frame .resizable-handler.tr {
+ top: -7px;
+}
+.movable-rect-frame .resizable-handler.tl,
+.movable-rect-frame .resizable-handler.l,
+.movable-rect-frame .resizable-handler.bl {
+ left: -7px;
+}
+.movable-rect-frame .resizable-handler.bl,
+.movable-rect-frame .resizable-handler.b,
+.movable-rect-frame .resizable-handler.br {
+ bottom: -7px;
+}
+.movable-rect-frame .resizable-handler.br,
+.movable-rect-frame .resizable-handler.r,
+.movable-rect-frame .resizable-handler.tr {
+ right: -7px;
+}
+.movable-rect-frame .resizable-handler.l,
+.movable-rect-frame .resizable-handler.r {
+ margin-top: -7px;
+}
+.movable-rect-frame .resizable-handler.t,
+.movable-rect-frame .resizable-handler.b {
+ margin-left: -7px;
+}
+
+.movable-rect-frame .rotate {
+ position: absolute;
+ left: 50%;
+ top: -26px;
+ width: 18px;
+ height: 18px;
+ margin-left: -9px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ cursor: pointer;
+}
+.movable-rect-frame .t,
+.movable-rect-frame .tl,
+.movable-rect-frame .tr {
+ top: -3px;
+}
+.movable-rect-frame .b,
+.movable-rect-frame .bl,
+.movable-rect-frame .br {
+ bottom: -3px;
+}
+.movable-rect-frame .r,
+.movable-rect-frame .tr,
+.movable-rect-frame .br {
+ right: -3px;
+}
+.movable-rect-frame .tl,
+.movable-rect-frame .l,
+.movable-rect-frame .bl {
+ left: -3px;
+}
+.movable-rect-frame .l,
+.movable-rect-frame .r {
+ top: 50%;
+ margin-top: -3px;
+}
+.movable-rect-frame .t,
+.movable-rect-frame .b {
+ left: 50%;
+ margin-left: -3px;
+}
diff --git a/packages/annotation-toolkit/src/layers/RRRR/utils.js b/packages/annotation-toolkit/src/layers/RRRR/utils.js
new file mode 100644
index 000000000..89ea4468e
--- /dev/null
+++ b/packages/annotation-toolkit/src/layers/RRRR/utils.js
@@ -0,0 +1,264 @@
+export const getLength = (x, y) => Math.sqrt(x * x + y * y);
+
+export const getAngle = ({ x: x1, y: y1 }, { x: x2, y: y2 }) => {
+ const dot = x1 * x2 + y1 * y2;
+ const det = x1 * y2 - y1 * x2;
+ const angle = (Math.atan2(det, dot) / Math.PI) * 180;
+ return (angle + 360) % 360;
+};
+
+export const degToRadian = (deg) => (deg * Math.PI) / 180;
+
+const cos = (deg) => Math.cos(degToRadian(deg));
+const sin = (deg) => Math.sin(degToRadian(deg));
+
+const setWidthAndDeltaW = (width, deltaW, minWidth) => {
+ const expectedWidth = width + deltaW;
+ if (expectedWidth > minWidth) {
+ width = expectedWidth;
+ } else {
+ deltaW = minWidth - width;
+ width = minWidth;
+ }
+ return { width, deltaW };
+};
+
+const setHeightAndDeltaH = (height, deltaH, minHeight) => {
+ const expectedHeight = height + deltaH;
+ if (expectedHeight > minHeight) {
+ height = expectedHeight;
+ } else {
+ deltaH = minHeight - height;
+ height = minHeight;
+ }
+ return { height, deltaH };
+};
+
+export const getNewStyle = (
+ type,
+ rect,
+ deltaW,
+ deltaH,
+ ratio,
+ minWidth,
+ minHeight
+) => {
+ let { width, height, centerX, centerY, rotateAngle } = rect;
+ const widthFlag = width < 0 ? -1 : 1;
+ const heightFlag = height < 0 ? -1 : 1;
+ width = Math.abs(width);
+ height = Math.abs(height);
+ switch (type) {
+ case "r": {
+ const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth);
+ width = widthAndDeltaW.width;
+ deltaW = widthAndDeltaW.deltaW;
+ if (ratio) {
+ deltaH = deltaW / ratio;
+ height = width / ratio;
+ // 左上角固定
+ centerX +=
+ (deltaW / 2) * cos(rotateAngle) - (deltaH / 2) * sin(rotateAngle);
+ centerY +=
+ (deltaW / 2) * sin(rotateAngle) + (deltaH / 2) * cos(rotateAngle);
+ } else {
+ // 左边固定
+ centerX += (deltaW / 2) * cos(rotateAngle);
+ centerY += (deltaW / 2) * sin(rotateAngle);
+ }
+ break;
+ }
+ case "tr": {
+ deltaH = -deltaH;
+ const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth);
+ width = widthAndDeltaW.width;
+ deltaW = widthAndDeltaW.deltaW;
+ const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight);
+ height = heightAndDeltaH.height;
+ deltaH = heightAndDeltaH.deltaH;
+ if (ratio) {
+ deltaW = deltaH * ratio;
+ width = height * ratio;
+ }
+ centerX +=
+ (deltaW / 2) * cos(rotateAngle) + (deltaH / 2) * sin(rotateAngle);
+ centerY +=
+ (deltaW / 2) * sin(rotateAngle) - (deltaH / 2) * cos(rotateAngle);
+ break;
+ }
+ case "br": {
+ const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth);
+ width = widthAndDeltaW.width;
+ deltaW = widthAndDeltaW.deltaW;
+ const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight);
+ height = heightAndDeltaH.height;
+ deltaH = heightAndDeltaH.deltaH;
+ if (ratio) {
+ deltaW = deltaH * ratio;
+ width = height * ratio;
+ }
+ centerX +=
+ (deltaW / 2) * cos(rotateAngle) - (deltaH / 2) * sin(rotateAngle);
+ centerY +=
+ (deltaW / 2) * sin(rotateAngle) + (deltaH / 2) * cos(rotateAngle);
+ break;
+ }
+ case "b": {
+ const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight);
+ height = heightAndDeltaH.height;
+ deltaH = heightAndDeltaH.deltaH;
+ if (ratio) {
+ deltaW = deltaH * ratio;
+ width = height * ratio;
+ // 左上角固定
+ centerX +=
+ (deltaW / 2) * cos(rotateAngle) - (deltaH / 2) * sin(rotateAngle);
+ centerY +=
+ (deltaW / 2) * sin(rotateAngle) + (deltaH / 2) * cos(rotateAngle);
+ } else {
+ // 上边固定
+ centerX -= (deltaH / 2) * sin(rotateAngle);
+ centerY += (deltaH / 2) * cos(rotateAngle);
+ }
+ break;
+ }
+ case "bl": {
+ deltaW = -deltaW;
+ const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth);
+ width = widthAndDeltaW.width;
+ deltaW = widthAndDeltaW.deltaW;
+ const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight);
+ height = heightAndDeltaH.height;
+ deltaH = heightAndDeltaH.deltaH;
+ if (ratio) {
+ height = width / ratio;
+ deltaH = deltaW / ratio;
+ }
+ centerX -=
+ (deltaW / 2) * cos(rotateAngle) + (deltaH / 2) * sin(rotateAngle);
+ centerY -=
+ (deltaW / 2) * sin(rotateAngle) - (deltaH / 2) * cos(rotateAngle);
+ break;
+ }
+ case "l": {
+ deltaW = -deltaW;
+ const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth);
+ width = widthAndDeltaW.width;
+ deltaW = widthAndDeltaW.deltaW;
+ if (ratio) {
+ height = width / ratio;
+ deltaH = deltaW / ratio;
+ // 右上角固定
+ centerX -=
+ (deltaW / 2) * cos(rotateAngle) + (deltaH / 2) * sin(rotateAngle);
+ centerY -=
+ (deltaW / 2) * sin(rotateAngle) - (deltaH / 2) * cos(rotateAngle);
+ } else {
+ // 右边固定
+ centerX -= (deltaW / 2) * cos(rotateAngle);
+ centerY -= (deltaW / 2) * sin(rotateAngle);
+ }
+ break;
+ }
+ case "tl": {
+ deltaW = -deltaW;
+ deltaH = -deltaH;
+ const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth);
+ width = widthAndDeltaW.width;
+ deltaW = widthAndDeltaW.deltaW;
+ const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight);
+ height = heightAndDeltaH.height;
+ deltaH = heightAndDeltaH.deltaH;
+ if (ratio) {
+ width = height * ratio;
+ deltaW = deltaH * ratio;
+ }
+ centerX -=
+ (deltaW / 2) * cos(rotateAngle) - (deltaH / 2) * sin(rotateAngle);
+ centerY -=
+ (deltaW / 2) * sin(rotateAngle) + (deltaH / 2) * cos(rotateAngle);
+ break;
+ }
+ case "t": {
+ deltaH = -deltaH;
+ const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight);
+ height = heightAndDeltaH.height;
+ deltaH = heightAndDeltaH.deltaH;
+ if (ratio) {
+ width = height * ratio;
+ deltaW = deltaH * ratio;
+ // 左下角固定
+ centerX +=
+ (deltaW / 2) * cos(rotateAngle) + (deltaH / 2) * sin(rotateAngle);
+ centerY +=
+ (deltaW / 2) * sin(rotateAngle) - (deltaH / 2) * cos(rotateAngle);
+ } else {
+ centerX += (deltaH / 2) * sin(rotateAngle);
+ centerY -= (deltaH / 2) * cos(rotateAngle);
+ }
+ break;
+ }
+ }
+
+ return {
+ position: {
+ centerX,
+ centerY,
+ },
+ size: {
+ width: width * widthFlag,
+ height: height * heightFlag,
+ },
+ };
+};
+
+const cursorStartMap = { n: 0, ne: 1, e: 2, se: 3, s: 4, sw: 5, w: 6, nw: 7 };
+const cursorDirectionArray = ["n", "ne", "e", "se", "s", "sw", "w", "nw"];
+const cursorMap = {
+ 0: 0,
+ 1: 1,
+ 2: 2,
+ 3: 2,
+ 4: 3,
+ 5: 4,
+ 6: 4,
+ 7: 5,
+ 8: 6,
+ 9: 6,
+ 10: 7,
+ 11: 8,
+};
+export const getCursor = (rotateAngle, d) => {
+ const increment = cursorMap[Math.floor(rotateAngle / 30)];
+ const index = cursorStartMap[d];
+ const newIndex = (index + increment) % 8;
+ return cursorDirectionArray[newIndex];
+};
+
+export const centerToTL = ({
+ centerX,
+ centerY,
+ width,
+ height,
+ rotateAngle,
+}) => ({
+ top: centerY - height / 2,
+ left: centerX - width / 2,
+ width,
+ height,
+ rotateAngle,
+});
+
+export const tLToCenter = ({ top, left, width, height, rotateAngle }) => ({
+ position: {
+ centerX: left + width / 2,
+ centerY: top + height / 2,
+ },
+ size: {
+ width,
+ height,
+ },
+ transform: {
+ rotateAngle,
+ },
+});
diff --git a/packages/annotation-toolkit/src/layers/VideoPlayer.js b/packages/annotation-toolkit/src/layers/VideoPlayer.js
index e61af9ffd..412463e58 100644
--- a/packages/annotation-toolkit/src/layers/VideoPlayer.js
+++ b/packages/annotation-toolkit/src/layers/VideoPlayer.js
@@ -58,10 +58,10 @@ export default function VideoPlayer({
.getContext("2d")
.drawImage(
vidRef.current.getInternalPlayer(),
- x / scale,
- y / scale,
- cropWidth / scale,
- cropHeight / scale,
+ x,
+ y,
+ cropWidth,
+ cropHeight,
0,
0,
cropWidth,
@@ -125,13 +125,21 @@ export default function VideoPlayer({
) : null}