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 { 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
|
||||||
|
|||||||
+40
-1
@@ -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;
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user