DNDGamePlayer: Electron редактор сцен, презентация, упаковка electron-builder
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import styles from './RotatedImage.module.css';
|
||||
|
||||
type Mode = 'cover' | 'contain';
|
||||
|
||||
type RotatedImageProps = {
|
||||
url: string;
|
||||
rotationDeg: 0 | 90 | 180 | 270;
|
||||
mode: Mode;
|
||||
alt?: string;
|
||||
/** Высота/ширина полностью контролируются родителем. */
|
||||
style?: React.CSSProperties;
|
||||
/** Прямоугольник видимого контента (contain/cover) внутри контейнера. */
|
||||
onContentRectChange?: ((rect: { x: number; y: number; w: number; h: number }) => void) | undefined;
|
||||
};
|
||||
|
||||
function useElementSize<T extends HTMLElement>() {
|
||||
const ref = useRef<T | null>(null);
|
||||
const [size, setSize] = useState<{ w: number; h: number }>({ w: 0, h: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
const ro = new ResizeObserver(() => {
|
||||
const r = el.getBoundingClientRect();
|
||||
setSize({ w: r.width, h: r.height });
|
||||
});
|
||||
ro.observe(el);
|
||||
const r = el.getBoundingClientRect();
|
||||
setSize({ w: r.width, h: r.height });
|
||||
return () => ro.disconnect();
|
||||
}, []);
|
||||
|
||||
return [ref, size] as const;
|
||||
}
|
||||
|
||||
export function RotatedImage({
|
||||
url,
|
||||
rotationDeg,
|
||||
mode,
|
||||
alt = '',
|
||||
style,
|
||||
onContentRectChange,
|
||||
}: RotatedImageProps) {
|
||||
const [ref, size] = useElementSize<HTMLDivElement>();
|
||||
const [imgSize, setImgSize] = useState<{ w: number; h: number } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
if (cancelled) return;
|
||||
setImgSize({ w: img.naturalWidth || 1, h: img.naturalHeight || 1 });
|
||||
};
|
||||
img.src = url;
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [url]);
|
||||
|
||||
const scale = useMemo(() => {
|
||||
if (!imgSize) return 1;
|
||||
if (size.w <= 1 || size.h <= 1) return 1;
|
||||
const rotated = rotationDeg === 90 || rotationDeg === 270;
|
||||
const iw = rotated ? imgSize.h : imgSize.w;
|
||||
const ih = rotated ? imgSize.w : imgSize.h;
|
||||
const sx = size.w / iw;
|
||||
const sy = size.h / ih;
|
||||
return mode === 'cover' ? Math.max(sx, sy) : Math.min(sx, sy);
|
||||
}, [imgSize, mode, rotationDeg, size.h, size.w]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!onContentRectChange) return;
|
||||
if (!imgSize) return;
|
||||
if (size.w <= 1 || size.h <= 1) return;
|
||||
const rotated = rotationDeg === 90 || rotationDeg === 270;
|
||||
// Bounding-box размеров после rotate(): при 90/270 меняются местами.
|
||||
const bw = (rotated ? imgSize.h : imgSize.w) * scale;
|
||||
const bh = (rotated ? imgSize.w : imgSize.h) * scale;
|
||||
const x = (size.w - bw) / 2;
|
||||
const y = (size.h - bh) / 2;
|
||||
onContentRectChange({ x, y, w: bw, h: bh });
|
||||
}, [imgSize, mode, onContentRectChange, rotationDeg, scale, size.h, size.w]);
|
||||
|
||||
const w = imgSize ? imgSize.w * scale : undefined;
|
||||
const h = imgSize ? imgSize.h * scale : undefined;
|
||||
|
||||
return (
|
||||
<div ref={ref} className={styles.root} style={style}>
|
||||
<img
|
||||
alt={alt}
|
||||
src={url}
|
||||
className={styles.img}
|
||||
style={{
|
||||
width: w ?? '100%',
|
||||
height: h ?? '100%',
|
||||
objectFit: imgSize ? undefined : mode,
|
||||
transform: `translate(-50%, -50%) rotate(${String(rotationDeg)}deg)`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user