Add the ability to zoom and position uploaded images, add checkbox to hide all model selections
This commit is contained in:
@@ -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
@@ -4,6 +4,7 @@ import adeptusAstartesIcon from "../assets/adeptusAstartesIcon.png";
|
||||
import { Arrow, wavyLine } from "../assets/icons";
|
||||
import { Weapons, hasDifferentProfiles } from "./Weapons";
|
||||
import { useLocalStorage } from "../helpers/useLocalStorage";
|
||||
import { ImgEditor } from "./ImgEditor";
|
||||
|
||||
export const Roster = ({ roster, onePerPage, colorUserChoice }) => {
|
||||
if (!roster) {
|
||||
@@ -220,6 +221,7 @@ const Unit = ({
|
||||
}}
|
||||
>
|
||||
<input
|
||||
className="hide-model-selection"
|
||||
type="checkbox"
|
||||
onChange={(e) => setHideModelCount(e.target.checked)}
|
||||
/>
|
||||
@@ -319,7 +321,7 @@ const Unit = ({
|
||||
{cost.points}pts
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative z-10 flex gap-4">
|
||||
<div className="relative flex gap-4">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
@@ -341,11 +343,12 @@ const Unit = ({
|
||||
</div>
|
||||
{!hideModelCount && modelStats?.[0] && (
|
||||
<div
|
||||
className={`mt-[17px] ${
|
||||
className={`pointer-events-none mt-[17px] ${
|
||||
modelStats.length > 1 ? "" : "self-center"
|
||||
}`}
|
||||
style={{
|
||||
fontSize: "0.7em",
|
||||
zIndex: 101,
|
||||
}}
|
||||
>
|
||||
{modelList.map((model, index) => (
|
||||
@@ -362,16 +365,12 @@ const Unit = ({
|
||||
top: 0,
|
||||
height: "100%",
|
||||
bottom: 0,
|
||||
width: "45%",
|
||||
width: "50%",
|
||||
zIndex: 100,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{hasImage && (
|
||||
<img
|
||||
src={image}
|
||||
alt=""
|
||||
style={{ width: "100%", height: "100%", objectFit: "contain" }}
|
||||
/>
|
||||
)}
|
||||
{hasImage && <ImgEditor image={image} name={name} />}
|
||||
<div className="absolute right-[1px] top-[2px] flex gap-1">
|
||||
{hasImage && !bgRemoved && (
|
||||
<button
|
||||
|
||||
+41
-2
@@ -19,7 +19,20 @@ function App() {
|
||||
const [onePerPage, setOnePerPage] = useState(false);
|
||||
const [primaryColor, setPrimaryColor] = useState("#536766");
|
||||
const [colorUserChoice, setColorUserChoice] = useState(false);
|
||||
const [hideModelSelections, setHideModelSelections] = useState(false);
|
||||
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) {
|
||||
const files = event?.target?.files;
|
||||
|
||||
@@ -250,7 +263,7 @@ function App() {
|
||||
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">
|
||||
<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"
|
||||
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>
|
||||
{
|
||||
// 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 }}>
|
||||
<label style={{ display: "flex", alignItems: "center", gap: 4 }}>
|
||||
<input
|
||||
|
||||
Reference in New Issue
Block a user