Add the ability to zoom and position uploaded images, add checkbox to hide all model selections

This commit is contained in:
NilsUeter
2025-03-08 15:58:37 +01:00
parent 04558102db
commit cae8eca247
3 changed files with 136 additions and 12 deletions
+86
View File
@@ -0,0 +1,86 @@
import { useState, useRef, useEffect } from "react";
import { useLocalStorage } from "../helpers/useLocalStorage";
export const ImgEditor = ({ image, name }) => {
const [scale, setScale] = useLocalStorage(`scale_${name}`, 1);
const [positionX, setPositionX] = useLocalStorage(`positionX_${name}`, 0);
const [positionY, setPositionY] = useLocalStorage(`positionY_${name}`, 0);
const [dragging, setDragging] = useState(false);
const [startPos, setStartPos] = useState({ x: 0, y: 0 });
const imgRef = useRef(null);
const handleWheel = (e) => {
e.preventDefault();
setScale((prevScale) =>
Math.min(Math.max(parseFloat(prevScale) + e.deltaY * -0.001, 0.8), 5),
);
};
const handleMouseDown = (e) => {
setDragging(true);
setStartPos({
x: e.clientX - parseFloat(positionX),
y: e.clientY - parseFloat(positionY),
});
};
const handleMouseMove = (e) => {
if (dragging) {
setPositionX(e.clientX - startPos.x);
setPositionY(e.clientY - startPos.y);
}
};
const handleMouseUp = () => {
setDragging(false);
};
const handleDragStart = (e) => {
e.preventDefault();
};
useEffect(() => {
const imgElement = imgRef.current;
if (imgElement) {
imgElement.addEventListener("wheel", handleWheel);
imgElement.addEventListener("mousedown", handleMouseDown);
imgElement.addEventListener("dragstart", handleDragStart);
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mouseup", handleMouseUp);
}
return () => {
if (imgElement) {
imgElement.removeEventListener("wheel", handleWheel);
imgElement.removeEventListener("mousedown", handleMouseDown);
imgElement.removeEventListener("dragstart", handleDragStart);
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseup", handleMouseUp);
}
};
}, [dragging]);
return (
<div
ref={imgRef}
className="h-full w-full"
style={{
cursor: dragging ? "grabbing" : "grab",
overflow: "hidden",
}}
>
<img
src={image}
alt=""
style={{
width: "100%",
height: "100%",
transform: `translate(${positionX}px, ${positionY}px) scale(${scale})`,
transformOrigin: "center",
userSelect: "none",
objectFit: "contain",
}}
/>
</div>
);
};
+9 -10
View File
@@ -4,6 +4,7 @@ import adeptusAstartesIcon from "../assets/adeptusAstartesIcon.png";
import { Arrow, wavyLine } from "../assets/icons"; import { Arrow, wavyLine } from "../assets/icons";
import { Weapons, hasDifferentProfiles } from "./Weapons"; import { Weapons, hasDifferentProfiles } from "./Weapons";
import { useLocalStorage } from "../helpers/useLocalStorage"; import { useLocalStorage } from "../helpers/useLocalStorage";
import { ImgEditor } from "./ImgEditor";
export const Roster = ({ roster, onePerPage, colorUserChoice }) => { export const Roster = ({ roster, onePerPage, colorUserChoice }) => {
if (!roster) { if (!roster) {
@@ -220,6 +221,7 @@ const Unit = ({
}} }}
> >
<input <input
className="hide-model-selection"
type="checkbox" type="checkbox"
onChange={(e) => setHideModelCount(e.target.checked)} onChange={(e) => setHideModelCount(e.target.checked)}
/> />
@@ -319,7 +321,7 @@ const Unit = ({
{cost.points}pts {cost.points}pts
</span> </span>
</div> </div>
<div className="relative z-10 flex gap-4"> <div className="relative flex gap-4">
<div <div
style={{ style={{
display: "flex", display: "flex",
@@ -341,11 +343,12 @@ const Unit = ({
</div> </div>
{!hideModelCount && modelStats?.[0] && ( {!hideModelCount && modelStats?.[0] && (
<div <div
className={`mt-[17px] ${ className={`pointer-events-none mt-[17px] ${
modelStats.length > 1 ? "" : "self-center" modelStats.length > 1 ? "" : "self-center"
}`} }`}
style={{ style={{
fontSize: "0.7em", fontSize: "0.7em",
zIndex: 101,
}} }}
> >
{modelList.map((model, index) => ( {modelList.map((model, index) => (
@@ -362,16 +365,12 @@ const Unit = ({
top: 0, top: 0,
height: "100%", height: "100%",
bottom: 0, bottom: 0,
width: "45%", width: "50%",
zIndex: 100,
overflow: "hidden",
}} }}
> >
{hasImage && ( {hasImage && <ImgEditor image={image} name={name} />}
<img
src={image}
alt=""
style={{ width: "100%", height: "100%", objectFit: "contain" }}
/>
)}
<div className="absolute right-[1px] top-[2px] flex gap-1"> <div className="absolute right-[1px] top-[2px] flex gap-1">
{hasImage && !bgRemoved && ( {hasImage && !bgRemoved && (
<button <button
+41 -2
View File
@@ -19,7 +19,20 @@ function App() {
const [onePerPage, setOnePerPage] = useState(false); const [onePerPage, setOnePerPage] = useState(false);
const [primaryColor, setPrimaryColor] = useState("#536766"); const [primaryColor, setPrimaryColor] = useState("#536766");
const [colorUserChoice, setColorUserChoice] = useState(false); const [colorUserChoice, setColorUserChoice] = useState(false);
const [hideModelSelections, setHideModelSelections] = useState(false);
const uploadRef = useRef(); const uploadRef = useRef();
const toggleHideModelSelections = (hide) => {
const checkboxes = document.querySelectorAll(
'input[type="checkbox"].hide-model-selection',
);
checkboxes.forEach((checkbox) => {
if (checkbox.checked !== hide) {
checkbox.click();
}
});
};
async function handleFileSelect(event) { async function handleFileSelect(event) {
const files = event?.target?.files; const files = event?.target?.files;
@@ -250,7 +263,7 @@ function App() {
stringifyJSON(rostersJSON.filter((_, i) => i !== index)), stringifyJSON(rostersJSON.filter((_, i) => i !== index)),
) )
} }
className="print-display-none absolute right-[-4px] top-[-4px] rounded-full border-0 bg-red-600 fill-white p-[2px] text-sm transition duration-150 ease-in-out hover:bg-red-800 focus:outline-none" className="print-display-none absolute right-[-4px] top-[-4px] rounded-full border-0 bg-red-600 fill-white p-[2px] text-sm transition duration-150 ease-in-out hover:bg-red-800 focus:outline-none"
> >
<svg height="16" viewBox="0 0 16 16" width="16"> <svg height="16" viewBox="0 0 16 16" width="16">
<path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 8l3.22 3.22a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 9.06l-3.22 3.22a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z"></path> <path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 8l3.22 3.22a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 9.06l-3.22 3.22a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z"></path>
@@ -325,8 +338,34 @@ function App() {
type="checkbox" type="checkbox"
onChange={(e) => setOnePerPage(e.target.checked)} onChange={(e) => setOnePerPage(e.target.checked)}
/> />
<span>One Datacard per Page when Printing</span> <span className="select-none">
One Datacard per Page when Printing
</span>
</label> </label>
{
// only show when 10th edition
edition === 10 && (
<label
style={{
display: "flex",
alignItems: "center",
gap: 4,
minHeight: 26,
}}
>
<input
type="checkbox"
value={hideModelSelections}
onChange={(e) => {
setHideModelSelections(e.target.checked);
toggleHideModelSelections(e.target.checked);
}}
className="hide-model-selection"
/>
<span className="select-none">Hide all model selections</span>
</label>
)
}
<div style={{ display: "flex", alignItems: "center", gap: 4 }}> <div style={{ display: "flex", alignItems: "center", gap: 4 }}>
<label style={{ display: "flex", alignItems: "center", gap: 4 }}> <label style={{ display: "flex", alignItems: "center", gap: 4 }}>
<input <input