aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorNoma <thu_vu@brown.edu>2025-03-21 15:21:32 -0400
committerNoma <thu_vu@brown.edu>2025-03-21 15:21:32 -0400
commit10c9ef174be25a821900185e762ab1cc3e48df18 (patch)
tree89ab31183419ec1adbff51ecb7e1a4534ef65ca6 /src
parent2571cc0130efd2592c426cdb528e5083a4bc8eab (diff)
feat: mesh transform for images
Diffstat (limited to 'src')
-rw-r--r--src/client/views/nodes/imageEditor/imageMeshTool/ImageMeshTool.ts0
-rw-r--r--src/client/views/nodes/imageEditor/imageMeshTool/imageMesh.scss24
-rw-r--r--src/client/views/nodes/imageEditor/imageMeshTool/imageMesh.tsx109
-rw-r--r--src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.scss21
-rw-r--r--src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx90
5 files changed, 244 insertions, 0 deletions
diff --git a/src/client/views/nodes/imageEditor/imageMeshTool/ImageMeshTool.ts b/src/client/views/nodes/imageEditor/imageMeshTool/ImageMeshTool.ts
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/client/views/nodes/imageEditor/imageMeshTool/ImageMeshTool.ts
diff --git a/src/client/views/nodes/imageEditor/imageMeshTool/imageMesh.scss b/src/client/views/nodes/imageEditor/imageMeshTool/imageMesh.scss
new file mode 100644
index 000000000..253f48f77
--- /dev/null
+++ b/src/client/views/nodes/imageEditor/imageMeshTool/imageMesh.scss
@@ -0,0 +1,24 @@
+/* MeshTransformGrid.scss */
+.meshTransformGrid {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ pointer-events: none; /* Prevents interaction with the grid itself */
+ opacity: 5%;
+}
+
+.grid-line {
+ position: absolute;
+ background-color: rgba(255, 255, 255, 0.6); /* Light grid lines */
+}
+
+.control-point {
+ position: absolute;
+ width: 12px;
+ height: 12px;
+ background-color: rgba(255, 255, 255, 1); /* White control points */
+ border-radius: 50%;
+ cursor: pointer;
+ z-index: 10;
+ pointer-events: auto; /* Allows dragging of control points */
+}
diff --git a/src/client/views/nodes/imageEditor/imageMeshTool/imageMesh.tsx b/src/client/views/nodes/imageEditor/imageMeshTool/imageMesh.tsx
new file mode 100644
index 000000000..ee5c597e9
--- /dev/null
+++ b/src/client/views/nodes/imageEditor/imageMeshTool/imageMesh.tsx
@@ -0,0 +1,109 @@
+import React, { useState, useEffect } from 'react';
+import './MeshTransformGrid.scss';
+
+interface MeshTransformGridProps {
+ imageRef: React.RefObject<HTMLImageElement>; // Reference to the image element
+ gridXSize: number; // Number of X subdivisions
+ gridYSize: number; // Number of Y subdivisions
+ isInteractive: boolean; // Whether control points are interactive (can be dragged)
+}
+
+const MeshTransformGrid: React.FC<MeshTransformGridProps> = ({ imageRef, gridXSize, gridYSize, isInteractive }) => {
+ const [controlPoints, setControlPoints] = useState<any[]>([]);
+
+ // Set up control points based on image size and grid sizes
+ useEffect(() => {
+ if (imageRef.current) {
+ const { width, height, left, top } = imageRef.current.getBoundingClientRect();
+ const newControlPoints = [];
+
+ for (let i = 0; i <= gridYSize; i++) {
+ for (let j = 0; j <= gridXSize; j++) {
+ newControlPoints.push({
+ id: `${i}-${j}`,
+ x: (j * width) / gridXSize + left,
+ y: (i * height) / gridYSize + top,
+ });
+ }
+ }
+
+ setControlPoints(newControlPoints);
+ }
+ }, [imageRef, gridXSize, gridYSize]);
+
+ // Handle dragging of control points
+ const handleDrag = (e: React.MouseEvent, pointId: string) => {
+ if (!isInteractive) return; // Prevent dragging if grid is not interactive
+
+ const { clientX, clientY } = e;
+ const updatedPoints = controlPoints.map((point) => {
+ if (point.id === pointId) {
+ return { ...point, x: clientX, y: clientY };
+ }
+ return point;
+ });
+ setControlPoints(updatedPoints);
+ };
+
+ // Render grid lines between control points
+ const renderGridLines = () => {
+ const lines = [];
+ for (let i = 0; i < controlPoints.length; i++) {
+ const point = controlPoints[i];
+ const nextPoint = controlPoints[i + 1];
+
+ // Horizontal lines
+ if (nextPoint && i % (gridXSize + 1) !== gridXSize) {
+ lines.push({
+ start: { x: point.x, y: point.y },
+ end: { x: nextPoint.x, y: nextPoint.y },
+ });
+ }
+
+ // Vertical lines
+ if (i + gridXSize + 1 < controlPoints.length) {
+ const downPoint = controlPoints[i + gridXSize + 1];
+ lines.push({
+ start: { x: point.x, y: point.y },
+ end: { x: downPoint.x, y: downPoint.y },
+ });
+ }
+ }
+ return lines.map((line, index) => (
+ <div
+ key={index}
+ className="grid-line"
+ style={{
+ position: 'absolute',
+ left: `${line.start.x}px`,
+ top: `${line.start.y}px`,
+ width: `${Math.abs(line.end.x - line.start.x)}px`,
+ height: `${Math.abs(line.end.y - line.start.y)}px`,
+ border: '1px solid rgba(255, 255, 255, 0.6)',
+ }}
+ />
+ ));
+ };
+
+ return (
+ <div className="meshTransformGrid">
+ {renderGridLines()}
+
+ {controlPoints.map((point) => (
+ <div
+ key={point.id}
+ className="control-point"
+ style={{
+ left: `${point.x}px`,
+ top: `${point.y}px`,
+ transform: 'translate(-50%, -50%)',
+ }}
+ draggable={isInteractive} // Only allow dragging if interactive
+ onDrag={(e) => handleDrag(e, point.id)}
+ />
+ ))}
+ </div>
+ );
+};
+
+export default MeshTransformGrid;
diff --git a/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.scss b/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.scss
new file mode 100644
index 000000000..a72b2de6a
--- /dev/null
+++ b/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.scss
@@ -0,0 +1,21 @@
+/* MeshTransformButton.scss */
+.meshTransformBtnContainer {
+ position: relative;
+ width: 100%;
+ height: 100%;
+}
+
+.grid-line {
+ position: absolute;
+ background-color: rgba(255, 255, 255, 0.6);
+}
+
+.control-point {
+ position: absolute;
+ width: 12px;
+ height: 12px;
+ background-color: rgba(255, 255, 255, 1);
+ border-radius: 50%;
+ cursor: pointer;
+ z-index: 10;
+}
diff --git a/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx b/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx
new file mode 100644
index 000000000..eb68410b0
--- /dev/null
+++ b/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx
@@ -0,0 +1,90 @@
+import './MeshTransformButton.scss';
+import * as React from 'react';
+import ReactLoading from 'react-loading';
+import { Button, IconButton, Type } from '@dash/components';
+import { AiOutlineInfo } from 'react-icons/ai';
+import { SettingsManager } from '../../../../util/SettingsManager';
+import MeshTransformGrid from './imageMesh';
+
+interface ButtonContainerProps {
+ onClick: () => Promise<void>;
+ loading: boolean;
+ onReset: () => void;
+ btnText: string;
+ imageWidth: number;
+ imageHeight: number;
+ gridXSize: number; // X subdivisions
+ gridYSize: number; // Y subdivisions
+}
+
+export function MeshTransformButton({
+ loading,
+ onClick: startMeshTransform,
+ onReset,
+ btnText,
+ imageWidth,
+ imageHeight,
+ gridXSize,
+ gridYSize
+}: ButtonContainerProps) {
+ const [showGrid, setShowGrid] = React.useState(false);
+ const [isGridInteractive, setIsGridInteractive] = React.useState(false); // Controls the dragging of control points
+ const imageRef = React.useRef<HTMLImageElement>(null); // Reference to the image element
+
+ const handleGridToggle = () => {
+ if (showGrid) {
+ setShowGrid(false); // Hide the grid
+ setIsGridInteractive(false); // Disable control points manipulation
+ } else {
+ setShowGrid(true); // Show the grid
+ setIsGridInteractive(true); // Enable control points manipulation
+ }
+ };
+
+ return (
+ <div className="meshTransformBtnContainer">
+ <Button text="RESET" type={Type.PRIM} color={SettingsManager.userVariantColor} onClick={onReset} />
+ {loading ? (
+ <Button
+ text={btnText}
+ type={Type.TERT}
+ color={SettingsManager.userVariantColor}
+ icon={<ReactLoading type="spin" color="#ffffff" width={20} height={20} />}
+ iconPlacement="right"
+ onClick={() => {
+ if (!loading) handleGridToggle(); // Toggle the grid visibility and control points manipulation
+ }}
+ />
+ ) : (
+ <Button
+ text={btnText}
+ type={Type.TERT}
+ color={SettingsManager.userVariantColor}
+ onClick={() => {
+ if (!loading) handleGridToggle(); // Toggle the grid visibility and control points manipulation
+ }}
+ />
+ )}
+
+ {/* The IconButton will toggle the grid */}
+ <IconButton
+ type={Type.SEC}
+ color={SettingsManager.userVariantColor}
+ tooltip="Toggle Grid"
+ icon={<AiOutlineInfo size="16px" />}
+ onClick={handleGridToggle} // Toggle the grid when clicked
+ />
+
+ {/* Only show the grid if `showGrid` is true */}
+ {showGrid && (
+ <MeshTransformGrid
+ imageRef={imageRef}
+ gridXSize={gridXSize}
+ gridYSize={gridYSize}
+ isInteractive={isGridInteractive} // Pass the interactive flag to control point manipulation
+ />
+ )}
+ <img ref={imageRef} src="your-image-source.jpg" alt="Mesh" style={{ width: imageWidth, height: imageHeight }} />
+ </div>
+ );
+}