291 lines
9.2 KiB
TypeScript
291 lines
9.2 KiB
TypeScript
import {
|
|
IPalmistryFinger,
|
|
IPalmistryLine,
|
|
IPalmistryPoint,
|
|
} from "@/api/resources/Palmistry";
|
|
import "./scanned-photo.css";
|
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
|
type Props = {
|
|
photo: string;
|
|
small: boolean;
|
|
displayLines: boolean;
|
|
lines: Array<IPalmistryLine & { label?: string }>;
|
|
fingers: IPalmistryFinger[];
|
|
drawElementChangeDelay: number;
|
|
startDelay: number;
|
|
drawElements: Array<IPalmistryLine | IPalmistryFinger>;
|
|
className?: string;
|
|
isDecorationShown?: boolean;
|
|
lineLabelsShown?: boolean;
|
|
};
|
|
|
|
export default function StepScanPhoto(props: Props) {
|
|
const { className: classNameProp = "", isDecorationShown = true } = props;
|
|
const className = ["scanned-photo", classNameProp];
|
|
const { lines, drawElementChangeDelay, fingers, drawElements, lineLabelsShown = false } = props;
|
|
const imageRef = useRef<HTMLImageElement>(null);
|
|
const linesRef = useRef<SVGPathElement[]>([]);
|
|
const [isImageLoaded, setIsImageLoaded] = useState(false);
|
|
const [imageWidth, setImageWidth] = useState(0);
|
|
const [imageHeight, setImageHeight] = useState(0);
|
|
const [textPositions, setTextPositions] = useState<Array<{ x: number, y: number }>>([]);
|
|
|
|
if (props.small) {
|
|
className.push("scanned-photo_small");
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (isImageLoaded && imageRef.current) {
|
|
setImageWidth(imageRef.current.width || 0);
|
|
setImageHeight(imageRef.current.height || 0);
|
|
}
|
|
}, [isImageLoaded]);
|
|
|
|
useEffect(() => {
|
|
if (!imageWidth || !imageHeight || !lines.length) return;
|
|
|
|
const textWidth = 90;
|
|
const textHeight = 17;
|
|
const padding = 10;
|
|
const newPositions: Array<{ x: number, y: number }> = [];
|
|
|
|
lines.forEach((line, index) => {
|
|
const points = line.points;
|
|
const positions = [];
|
|
|
|
for (let i = 0; i < points.length - 1; i++) {
|
|
const x = (points[i].x + points[i + 1].x) / 2;
|
|
const y = (points[i].y + points[i + 1].y) / 2;
|
|
positions.push({ x, y });
|
|
}
|
|
|
|
positions.unshift({ x: points[0].x, y: points[0].y });
|
|
positions.push({ x: points[points.length - 1].x, y: points[points.length - 1].y });
|
|
|
|
let positionFound = false;
|
|
for (const pos of positions) {
|
|
let hasOverlap = false;
|
|
|
|
for (const existingPos of newPositions) {
|
|
if (
|
|
pos.x * imageWidth + padding < existingPos.x + textWidth &&
|
|
pos.x * imageWidth + padding + textWidth > existingPos.x &&
|
|
pos.y * imageHeight - padding < existingPos.y + textHeight &&
|
|
pos.y * imageHeight - padding + textHeight > existingPos.y
|
|
) {
|
|
hasOverlap = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!hasOverlap) {
|
|
newPositions.push({
|
|
x: pos.x * imageWidth + 10,
|
|
y: pos.y * imageHeight - 5
|
|
});
|
|
positionFound = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!positionFound) {
|
|
newPositions.push({
|
|
x: points[0].x * imageWidth + textWidth + padding * (index + 1),
|
|
y: points[0].y * imageHeight - textHeight - padding * (index + 1)
|
|
});
|
|
}
|
|
});
|
|
|
|
setTextPositions(newPositions);
|
|
}, [lines, imageWidth, imageHeight]);
|
|
|
|
const getCoordinatesString = useCallback(
|
|
(points: IPalmistryPoint[]) => {
|
|
const coordinatesString = `M ${points[0]?.x * imageWidth} ${points[0]?.y * imageHeight
|
|
}`;
|
|
return points.reduce(
|
|
(acc, point) =>
|
|
`${acc} L ${point?.x * imageWidth} ${point?.y * imageHeight}`,
|
|
coordinatesString
|
|
);
|
|
},
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
[lines, isImageLoaded, imageWidth, imageHeight]
|
|
);
|
|
|
|
const getLineLength = (line: SVGPathElement) => {
|
|
return line?.getTotalLength();
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className={className.join(" ")}
|
|
style={{
|
|
height: imageHeight ? `${imageHeight}px` : "auto",
|
|
}}
|
|
>
|
|
<div className="scanned-photo__container">
|
|
<div
|
|
className={`scanned-photo__stick ${!isDecorationShown ? "scanned-photo__stick_hidden" : ""}`}
|
|
style={{
|
|
animationDelay: `${drawElementChangeDelay * drawElements?.length + 2500
|
|
}ms`,
|
|
maxWidth: `${imageWidth}px`,
|
|
}}
|
|
/>
|
|
|
|
<div className="scanned-photo__image-container">
|
|
<img
|
|
className="scanned-photo__image"
|
|
alt="PalmIcon"
|
|
src={props.photo}
|
|
ref={imageRef}
|
|
onLoad={() => setIsImageLoaded(true)}
|
|
// width={imageWidth}
|
|
// height={imageHeight}
|
|
/>
|
|
</div>
|
|
{!!imageHeight && !!imageWidth && (
|
|
<svg
|
|
viewBox={`0 0 ${imageWidth} ${imageHeight}`}
|
|
className="scanned-photo__svg-objects"
|
|
>
|
|
{!!fingers.length &&
|
|
fingers?.map((finger, index) => {
|
|
return (
|
|
<svg
|
|
x={finger.point.x * imageWidth - 12}
|
|
y={finger.point.y * imageHeight - 12}
|
|
height="24px"
|
|
width="24px"
|
|
key={index}
|
|
>
|
|
<circle
|
|
cx="50%"
|
|
cy="50%"
|
|
r="11"
|
|
fill="white"
|
|
opacity="0.3"
|
|
className="scanned-photo__finger-point"
|
|
style={{
|
|
animationDelay: `${drawElementChangeDelay * (index + 1)
|
|
}ms`,
|
|
}}
|
|
/>
|
|
<circle
|
|
cx="50%"
|
|
cy="50%"
|
|
r="5"
|
|
fill="#066FDE"
|
|
stroke="white"
|
|
strokeWidth="0.3"
|
|
className="scanned-photo__finger-point"
|
|
style={{
|
|
animationDelay: `${drawElementChangeDelay * (index + 1)
|
|
}ms`,
|
|
}}
|
|
/>
|
|
</svg>
|
|
);
|
|
})}
|
|
|
|
{props.displayLines && (
|
|
<>
|
|
{lines.map((line, index) => (
|
|
<g key={`line-${index}`}>
|
|
<path
|
|
className={`scanned-photo__line scanned-photo__line_${line?.name}`}
|
|
d={getCoordinatesString(line?.points)}
|
|
ref={(el) =>
|
|
(linesRef.current[index] = el as SVGPathElement)
|
|
}
|
|
style={{
|
|
strokeDasharray:
|
|
getLineLength(linesRef.current[index]) || 500,
|
|
strokeDashoffset:
|
|
getLineLength(linesRef.current[index]) || 500,
|
|
animationDelay: `${drawElementChangeDelay * (index + 1)
|
|
}ms`,
|
|
}}
|
|
/>
|
|
</g>
|
|
))}
|
|
{lineLabelsShown && lines.map((line, index) => (
|
|
<g key={`line-label-${index}`}>
|
|
<text
|
|
x={textPositions[index]?.x || 0}
|
|
y={textPositions[index]?.y || 0}
|
|
fill="#066FDE"
|
|
className={`scanned-photo__line-text scanned-photo__line-text_${line?.name}`}
|
|
style={{
|
|
animationDelay: `${drawElementChangeDelay * (index + 1) + 300}ms`,
|
|
}}
|
|
>
|
|
{line.label || line.name}
|
|
</text>
|
|
</g>
|
|
))}
|
|
</>
|
|
)}
|
|
</svg>
|
|
)}
|
|
</div>
|
|
|
|
<div
|
|
className={`scanned-photo__decoration ${!isDecorationShown ? "scanned-photo__decoration_hidden" : ""}`}
|
|
style={{
|
|
animationDelay: `${drawElementChangeDelay * drawElements?.length}ms`,
|
|
}}
|
|
>
|
|
<div
|
|
className="scanned-photo__decoration__corners"
|
|
style={{
|
|
animationDelay: `${drawElementChangeDelay * drawElements?.length + 1500
|
|
}ms`,
|
|
}}
|
|
>
|
|
<div className="scanned-photo__decoration__light-blue-circle" />
|
|
|
|
<svg
|
|
version="1.1"
|
|
id="L3"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
x="0px"
|
|
y="0px"
|
|
viewBox="0 0 220 220"
|
|
enableBackground="new 0 0 0 0"
|
|
>
|
|
<circle
|
|
fill="none"
|
|
stroke="#EFF2FD"
|
|
strokeWidth="2"
|
|
cx="110"
|
|
cy="110"
|
|
r="105"
|
|
/>
|
|
<circle
|
|
fill="#066fde"
|
|
stroke="none"
|
|
strokeWidth="3"
|
|
cx="110"
|
|
cy="215"
|
|
r="4"
|
|
>
|
|
<animateTransform
|
|
attributeName="transform"
|
|
dur={`${drawElementChangeDelay * drawElements?.length + 3000
|
|
}ms`}
|
|
type="rotate"
|
|
from="0 110 110"
|
|
to="360 110 110"
|
|
repeatCount="indefinite"
|
|
></animateTransform>
|
|
</circle>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|