w-aura/src/components/palmistry/scanned-photo/scanned-photo.tsx
Daniil Chemerkin 61a63a91ee develop
2025-02-25 11:05:56 +00:00

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>
);
}