- added functionality for brush and eraser size

- added shutter priority option
This commit is contained in:
2026-01-14 12:42:11 +00:00
parent bb4234d336
commit 1c24b726b0
11 changed files with 260 additions and 45 deletions

View File

@@ -20,6 +20,7 @@
"country-flag-icons": "^1.6.4",
"formik": "^2.4.9",
"konva": "^10.0.12",
"rc-slider": "^11.1.9",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-konva": "^19.2.1",

View File

@@ -9,7 +9,6 @@ const CameraSettingsProvider = ({ children }: { children: ReactNode }) => {
useEffect(() => {
if (cameraControllerQuery?.data) {
console.log(cameraControllerQuery?.data);
const currentCameraControlMode = cameraControllerQuery.data.propOperationMode.value.toLowerCase();
const currentAutoMaxGain = cameraControllerQuery.data.propAutoModeMaxGain.value;
const currentAutoMinShutter = cameraControllerQuery.data.propAutoModeMinShutter.value;
@@ -20,11 +19,15 @@ const CameraSettingsProvider = ({ children }: { children: ReactNode }) => {
const currentManualFixGain = cameraControllerQuery.data.propManualModeFixGain.value;
const currentManualFixIris = cameraControllerQuery.data.propManualModeFixIris.value;
const currentShutterPriorityFixShutter = cameraControllerQuery.data.propShutterPriority.value;
const currentShutterPriorityMaxGain = cameraControllerQuery.data.propShutterPriorityMaxGain.value;
const currentShutterPriorityExposureCompensation =
cameraControllerQuery.data.propShutterPriorityExposureCompensation.value;
console.log({
currentCameraControlMode,
currentManualFixShutter,
currentManualFixGain,
currentManualFixIris,
currentShutterPriorityFixShutter,
currentShutterPriorityMaxGain,
currentShutterPriorityExposureCompensation,
});
dispatch({
type: "SET_CAMERA_CONTROLS",
@@ -41,6 +44,11 @@ const CameraSettingsProvider = ({ children }: { children: ReactNode }) => {
fixGain: currentManualFixGain,
fixIris: currentManualFixIris,
},
shutterPriority: {
fixShutter: currentShutterPriorityFixShutter,
maxGain: currentShutterPriorityMaxGain,
exposureCompensation: currentShutterPriorityExposureCompensation,
},
},
});
}

View File

@@ -8,9 +8,11 @@ export const initialState: CameraSettings = {
cameraControlMode: "auto",
auto: { minShutter: "1/100", maxShutter: "1/1000", maxGain: "0dB", exposureCompensation: "EC:off" },
manual: { fixShutter: "1/100", fixGain: "0dB", fixIris: "F2.0" },
shutterPriority: { fixShutter: "1/100", maxGain: "0dB", exposureCompensation: "EC:off" },
},
regionPainter: {
paintmode: "painter",
brushSize: 1,
paintMode: "painter",
paintedCells: new Map(),
regions: [
{ name: "Region 1", brushColour: "#FF0000" },
@@ -43,7 +45,15 @@ export const cameraSettingsReducer = (state: CameraSettings, action: CameraSetti
...state,
regionPainter: {
...state.regionPainter,
paintmode: action.payload,
paintMode: action.payload,
},
};
case "SET_BRUSH_SIZE":
return {
...state,
regionPainter: {
...state.regionPainter,
brushSize: action.payload,
},
};

View File

@@ -0,0 +1,19 @@
import Slider from "rc-slider";
import "rc-slider/assets/index.css";
type SliderComponentProps = {
id: string;
onChange: (value: number | number[]) => void;
value?: number;
min?: number;
max?: number;
step?: number;
};
const SliderComponent = ({ id, onChange, value = 0, min = 0, max = 100, step = 1 }: SliderComponentProps) => {
const handleChange = (val: number | number[]) => onChange(val);
return <Slider id={id} onChange={handleChange} value={value} min={min} max={max} step={step} />;
};
export default SliderComponent;

View File

@@ -17,7 +17,6 @@ const VideoFeed = ({ mostRecentSighting, isLoading, size, modeSetting, isModal =
const contextMode = cameraSettings.mode;
const [localMode, setLocalMode] = useState(0);
const mode = isModal ? localMode : contextMode;
console.log(mode);
const { image, plateRect, plateTrack } = useCreateVideoSnapshot(mostRecentSighting);
const handleModeChange = (newMode: number) => {

View File

@@ -11,7 +11,7 @@ const CameraControls = ({ state, dispatch }: CameraControlProps) => {
const { cameraControllerMutation } = useCameraController();
console.log(state);
const initialValues = {
cameraMode: state.cameraControlMode === "auto" ? "auto" : "manual",
cameraMode: state.cameraControlMode,
auto: {
minShutter: state.auto.minShutter,
maxShutter: state.auto.maxShutter,
@@ -23,24 +23,32 @@ const CameraControls = ({ state, dispatch }: CameraControlProps) => {
fixGain: state.manual.fixGain,
fixIris: state.manual.fixIris,
},
shutterPriority: {
fixShutter: state.shutterPriority.fixShutter,
maxGain: state.shutterPriority.maxGain,
exposureCompensation: state.shutterPriority.exposureCompensation,
},
};
const handleSumbit = (values: {
cameraMode: string;
auto: typeof initialValues.auto;
manual: typeof initialValues.manual;
shutterPriority: typeof initialValues.shutterPriority;
}) => {
cameraControllerMutation.mutate({
cameraControlMode: values.cameraMode as "auto" | "manual",
cameraControlMode: values.cameraMode as "auto" | "manual" | "shutter priority",
auto: values.auto,
manual: values.manual,
shutterPriority: values.shutterPriority,
});
dispatch({
type: "SET_CAMERA_CONTROLS",
payload: {
cameraControlMode: values.cameraMode as "auto" | "manual",
cameraControlMode: values.cameraMode as "auto" | "manual" | "shutter priority",
auto: values.auto,
manual: values.manual,
shutterPriority: values.shutterPriority,
},
});
};
@@ -48,7 +56,7 @@ const CameraControls = ({ state, dispatch }: CameraControlProps) => {
return (
<Formik initialValues={initialValues} onSubmit={handleSumbit} enableReinitialize>
{({ values }) => (
<Form className="flex flex-col gap-5 border border-gray-500 p-4 rounded-md">
<Form className="flex flex-col gap-5 border border-gray-500 p-4 rounded-md mt-[2%]">
<h2 className="text-2xl mb-2">Controls</h2>
<div className="flex flex-row gap-4">
<div>
@@ -73,6 +81,23 @@ const CameraControls = ({ state, dispatch }: CameraControlProps) => {
<Field type="radio" id="manual" name="cameraMode" className="sr-only" value="manual" />
</label>
</div>
<div>
<label
htmlFor="shutter priority"
className={`p-4 border rounded-lg mb-2 text-lg
${values.cameraMode === "shutter priority" ? "border-gray-400 bg-[#202b36]" : "bg-[#253445] border-gray-700"}
hover:bg-[#202b36] hover:cursor-pointer`}
>
Shutter Priority
<Field
type="radio"
id="shutter priority"
name="cameraMode"
className="sr-only"
value="shutter priority"
/>
</label>
</div>
</div>
{values.cameraMode === "auto" && (
<div className="flex flex-col gap-2">
@@ -266,6 +291,92 @@ const CameraControls = ({ state, dispatch }: CameraControlProps) => {
</div>
</div>
)}
{values.cameraMode === "shutter priority" && (
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-2 border border-gray-600 p-4 rounded-md">
<h3 className="text-lg">Shutter Speed</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 ">
<div className="flex flex-col gap-2">
<label htmlFor="shutterPriority.fixShutter" className="text-lg mb-1">
Fix Shutter:
</label>
<Field
as="select"
id="shutterPriority.fixShutter"
name="shutterPriority.fixShutter"
className="p-2 border border-gray-600 rounded-md bg-[#253445]"
>
<option value="1/100">1/100</option>
<option value="1/120">1/120</option>
</Field>
</div>
</div>
</div>
<div className="flex flex-col gap-2 border border-gray-600 p-4 rounded-md">
<h3 className="text-lg">Gain</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 ">
<div className="flex flex-col gap-2">
<label htmlFor="shutterPriority.maxGain" className="text-lg mb-1">
Max Gain:
</label>
<Field
as="select"
id="shutterPriority.maxGain"
name="shutterPriority.maxGain"
className="p-2 border border-gray-600 rounded-md bg-[#253445]"
>
<option value="0dB">0dB</option>
<option value="2dB">2dB</option>
<option value="4dB">4dB</option>
<option value="6dB">6dB</option>
<option value="10dB">10dB</option>
<option value="12dB">12dB</option>
<option value="16dB">16dB</option>
<option value="18dB">18dB</option>
<option value="20dB">20dB</option>
<option value="24dB">24dB</option>
</Field>
</div>
</div>
</div>
<div className="flex flex-col gap-2 border border-gray-600 p-4 rounded-md">
<h3 className="text-lg">Exposure</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 ">
<div className="flex flex-col gap-2">
<label htmlFor="shutterPriority.exposureCompensation" className="text-lg mb-1">
Exposure Compensation:
</label>
<Field
as="select"
id="shutterPriority.exposureCompensation"
name="shutterPriority.exposureCompensation"
className="p-2 border border-gray-600 rounded-md bg-[#253445]"
>
<option value="EC:off">EC:off</option>
<option value="EC:0">EC:0</option>
<option value="EC:1">EC:1</option>
<option value="EC:2">EC:2</option>
<option value="EC:3">EC:3</option>
<option value="EC:4">EC:4</option>
<option value="EC:5">EC:5</option>
<option value="EC:6">EC:6</option>
<option value="EC:7">EC:7</option>
<option value="EC:8">EC:8</option>
<option value="EC:9">EC:9</option>
<option value="EC:10">EC:10</option>
<option value="EC:11">EC:11</option>
<option value="EC:12">EC:12</option>
<option value="EC:13">EC:13</option>
<option value="EC:14">EC:14</option>
</Field>
</div>
</div>
</div>
</div>
)}
<button type="submit" className="p-3 rounded-md bg-green-700 hover:bg-green-900 cursor-pointer">
Save Settings
</button>

View File

@@ -1,4 +1,5 @@
import { useCameraSettingsContext } from "../../../../app/context/CameraSettingsContext";
import SliderComponent from "../../../../components/ui/SliderComponent";
import type { CameraSettings } from "../../../../utils/types";
type RegionProps = {
@@ -7,8 +8,9 @@ type RegionProps = {
const Region = ({ state }: RegionProps) => {
const { dispatch } = useCameraSettingsContext();
const paintMode = state.regionPainter.paintmode;
const paintMode = state.regionPainter.paintMode;
const regions = state.regionPainter.regions;
const brushSize = state.regionPainter.brushSize;
const handleChangePaintMode = (event: React.ChangeEvent<HTMLInputElement>) => {
const mode = event.target.value as "painter" | "eraser";
@@ -25,7 +27,7 @@ const Region = ({ state }: RegionProps) => {
console.log(state);
};
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-4 mt-[2%]">
<div className="flex flex-col gap-4 md:flex-row">
<div className="border border-gray-600 p-2 rounded-lg w-full">
<h2 className="text-2xl mb-2">Tools</h2>
@@ -64,6 +66,20 @@ const Region = ({ state }: RegionProps) => {
/>
<span className="text-xl">Eraser</span>
</label>
<div className="flex flex-col mt-4 border border-gray-600 rounded-lg p-4">
<span className="text-lg mb-2">
{paintMode === "painter" ? "Brush" : "Eraser"} Size: {brushSize}
</span>
<SliderComponent
id="brushSize"
onChange={(value) => dispatch({ type: "SET_BRUSH_SIZE", payload: value as number })}
value={brushSize}
min={1}
max={5}
step={1}
/>
</div>
</div>
</div>
<div className="border border-gray-600 p-2 rounded-lg w-full">

View File

@@ -18,7 +18,8 @@ const VideoFeedSetup = () => {
const { state, dispatch } = useCameraSettingsContext();
const cameraMode = state.cameraMode;
const paintedCells = state.regionPainter.paintedCells;
const paintMode = state.regionPainter.paintmode;
const paintMode = state.regionPainter.paintMode;
const brushSize = state.regionPainter.brushSize;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const paintLayerRef = useRef<any>(null);
const size = state.imageSize;
@@ -37,43 +38,47 @@ const VideoFeedSetup = () => {
};
const image = draw(latestBitmapRef);
const paintCell = (x: number, y: number) => {
const paintCell = (x: number, y: number, brushSize: number) => {
const col = Math.floor(x / (cellSize + gap));
const row = Math.floor(y / (cellSize + gap));
const raduis = Math.floor(brushSize / 2);
if (row < 0 || row >= rows || col < 0 || col >= cols) return;
const activeRegion = region;
if (!activeRegion) return;
const cellKey = `${row}-${col}`;
const currentColour = region.brushColour;
for (let r = row - raduis; r <= row + raduis; r++) {
for (let c = col - raduis; c <= col + raduis; c++) {
if (r < 0 || r >= rows || c < 0 || c >= cols) continue;
const key = `${r}-${c}`;
const map = paintedCells;
const existing = map.get(cellKey);
const existing = map.get(key);
if (paintMode === "eraser") {
if (map.has(cellKey)) {
map.delete(cellKey);
if (map.has(key)) {
map.delete(key);
paintLayerRef.current?.batchDraw();
}
return;
continue;
}
if (existing && existing.colour === currentColour) continue;
map.set(key, { colour: currentColour, region: activeRegion });
}
}
if (existing && existing.colour === currentColour) return;
map.set(cellKey, { colour: currentColour, region: activeRegion });
paintLayerRef.current?.batchDraw();
};
const handleStageMouseDown = (e: KonvaEventObject<MouseEvent>) => {
if (!(cameraMode === 1)) return;
const pos = e.target.getStage()?.getPointerPosition();
if (pos) paintCell(pos.x, pos.y);
if (pos) paintCell(pos.x, pos.y, brushSize);
};
const handleMouseMove = (e: KonvaEventObject<MouseEvent>) => {
if (!(cameraMode === 1)) return;
const pos = e.target.getStage()?.getPointerPosition();
if (pos && e.evt.buttons === 1) paintCell(pos.x, pos.y);
if (pos && e.evt.buttons === 1) paintCell(pos.x, pos.y, brushSize);
};
useEffect(() => {

View File

@@ -10,25 +10,30 @@ const fetchCameraControllerConfig = async () => {
const updateCameraControllerConfig = async (config: CameraSettings["cameraControls"]) => {
if (!config) return;
const fields = [];
if (config.cameraControlMode === "auto") {
fields.push(
{ name: "propOperationMode", value: "Auto" },
{ name: "propAutoModeMaxGain", value: config.auto.maxGain },
{ name: "propAutoModeMinShutter", value: config.auto.minShutter },
{ name: "propAutoModeMaxShutter", value: config.auto.maxShutter },
{ name: "propAutoModeExposureCompensation", value: config.auto.exposureCompensation },
{ property: "propOperationMode", value: "Auto" },
{ property: "propAutoModeMaxGain", value: config.auto.maxGain },
{ property: "propAutoModeMinShutter", value: config.auto.minShutter },
{ property: "propAutoModeMaxShutter", value: config.auto.maxShutter },
{ property: "propAutoModeExposureCompensation", value: config.auto.exposureCompensation },
);
} else if (config.cameraControlMode === "manual") {
fields.push(
{ name: "propOperationMode", value: "Manual" },
{ name: "propManualModeShutter", value: config.manual.fixShutter },
{ name: "propManualModeFixGain", value: config.manual.fixGain },
{ name: "propManualModeFixIris", value: config.manual.fixIris },
{ property: "propOperationMode", value: "Manual" },
{ property: "propManualModeShutter", value: config.manual.fixShutter },
{ property: "propManualModeFixGain", value: config.manual.fixGain },
{ property: "propManualModeFixIris", value: config.manual.fixIris },
);
} else if (config.cameraControlMode === "shutter priority") {
fields.push(
{ property: "propOperationMode", value: "Shutter Priority" },
{ property: "propShutterPriority", value: config.shutterPriority.fixShutter },
{ property: "propShutterPriorityMaxGain", value: config.shutterPriority.maxGain },
{ property: "propShutterPriorityExposureCompensation", value: config.shutterPriority.exposureCompensation },
);
}
const data = {
id: "Colour--camera-control-widget-config",
fields: fields,
@@ -41,7 +46,6 @@ const updateCameraControllerConfig = async (config: CameraSettings["cameraContro
body: JSON.stringify(data),
});
if (!response.ok) throw new Error("Cannot reach camera controller endpoint");
console.log(response);
return response.json();
};

View File

@@ -67,7 +67,7 @@ export type CameraSettings = {
mode: number;
imageSize: { width: number; height: number };
cameraControls: {
cameraControlMode: "auto" | "manual";
cameraControlMode: "auto" | "manual" | "shutter priority";
auto: {
minShutter: string;
maxShutter: string;
@@ -79,9 +79,15 @@ export type CameraSettings = {
fixGain: string;
fixIris: string;
};
shutterPriority: {
fixShutter: string;
maxGain: string;
exposureCompensation: string;
};
};
regionPainter: {
paintmode: "painter" | "eraser";
brushSize: number;
paintMode: "painter" | "eraser";
paintedCells: Map<string, PaintedCell>;
regions: Region[];
selectedRegionIndex: number;
@@ -108,6 +114,10 @@ export type CameraSettingsAction =
| {
type: "SET_CAMERA_CONTROLS";
payload: CameraSettings["cameraControls"];
}
| {
type: "SET_BRUSH_SIZE";
payload: number;
};
export type CameraStatus = {

View File

@@ -226,6 +226,11 @@
"@babel/plugin-transform-modules-commonjs" "^7.27.1"
"@babel/plugin-transform-typescript" "^7.28.5"
"@babel/runtime@^7.10.1", "@babel/runtime@^7.18.3":
version "7.28.6"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.6.tgz#d267a43cb1836dc4d182cce93ae75ba954ef6d2b"
integrity sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==
"@babel/template@^7.27.2":
version "7.27.2"
resolved "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz"
@@ -1247,6 +1252,11 @@ chokidar@^3.6.0:
optionalDependencies:
fsevents "~2.3.2"
classnames@^2.2.5:
version "2.5.1"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b"
integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
clsx@^2.0.0, clsx@^2.1.1:
version "2.1.1"
resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz"
@@ -2019,6 +2029,23 @@ punycode@^2.1.0:
resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz"
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
rc-slider@^11.1.9:
version "11.1.9"
resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-11.1.9.tgz#d872130fbf4ec51f28543d62e90451091d6f5208"
integrity sha512-h8IknhzSh3FEM9u8ivkskh+Ef4Yo4JRIY2nj7MrH6GQmrwV6mcpJf5/4KgH5JaVI1H3E52yCdpOlVyGZIeph5A==
dependencies:
"@babel/runtime" "^7.10.1"
classnames "^2.2.5"
rc-util "^5.36.0"
rc-util@^5.36.0:
version "5.44.4"
resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.44.4.tgz#89ee9037683cca01cd60f1a6bbda761457dd6ba5"
integrity sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==
dependencies:
"@babel/runtime" "^7.18.3"
react-is "^18.2.0"
react-dom@^19.2.0:
version "19.2.3"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz"
@@ -2036,6 +2063,11 @@ react-is@^16.13.1, react-is@^16.7.0:
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-is@^18.2.0:
version "18.3.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
react-konva@^19.2.1:
version "19.2.1"
resolved "https://registry.npmjs.org/react-konva/-/react-konva-19.2.1.tgz"