From a265107bfcfbdaa50199936c7490ea3b19d5b8a4 Mon Sep 17 00:00:00 2001 From: Adeyemi Date: Wed, 10 Apr 2024 12:27:03 +0100 Subject: [PATCH 1/4] feat/#4-Pass pixel position instead of (x, y) --- backend/routes/pixel.go | 11 +- frontend/src/canvas/Canvas.js | 463 ++++++++++++++++++++-------------- 2 files changed, 281 insertions(+), 193 deletions(-) diff --git a/backend/routes/pixel.go b/backend/routes/pixel.go index b9fc76d2..48bd89dd 100644 --- a/backend/routes/pixel.go +++ b/backend/routes/pixel.go @@ -65,18 +65,15 @@ func placePixelDevnet(w http.ResponseWriter, r *http.Request) { panic(err) } - x, err := strconv.Atoi(jsonBody["x"]) - if err != nil { - panic(err) - } - y, err := strconv.Atoi(jsonBody["y"]) + position, err := strconv.Atoi(jsonBody["position"]) if err != nil { panic(err) } + shellCmd := backend.ArtPeaceBackend.BackendConfig.Scripts.PlacePixelDevnet - position := x + y * int(backend.ArtPeaceBackend.CanvasConfig.Canvas.Width) + computedPosition := x + y * int(backend.ArtPeaceBackend.CanvasConfig.Canvas.Width) contract := os.Getenv("ART_PEACE_CONTRACT_ADDRESS") - cmd := exec.Command(shellCmd, contract, "place_pixel", strconv.Itoa(position), jsonBody["color"]) + cmd := exec.Command(shellCmd, contract, "place_pixel", strconv.Itoa(computedPosition), jsonBody["color"]) _, err = cmd.Output() if err != nil { fmt.Println("Error executing shell command: ", err) diff --git a/frontend/src/canvas/Canvas.js b/frontend/src/canvas/Canvas.js index c2d88e49..07d2807a 100644 --- a/frontend/src/canvas/Canvas.js +++ b/frontend/src/canvas/Canvas.js @@ -1,50 +1,51 @@ -import React, { useCallback, useRef, useEffect, useState } from 'react' -import useWebSocket, { ReadyState } from 'react-use-websocket' -import './Canvas.css'; +import React, { useCallback, useRef, useEffect, useState } from "react"; +import useWebSocket, { ReadyState } from "react-use-websocket"; +import "./Canvas.css"; // import TemplateOverlay from './TemplateOverlay.js'; -import canvasConfig from "../configs/canvas.config.json" -import backendConfig from "../configs/backend.config.json" +import canvasConfig from "../configs/canvas.config.json"; +import backendConfig from "../configs/backend.config.json"; -const Canvas = props => { - const backendUrl = "http://" + backendConfig.host + ":" + backendConfig.port +const Canvas = (props) => { + const backendUrl = "http://" + backendConfig.host + ":" + backendConfig.port; // TODO: Pressing "Canvas" resets the view / positioning - const [canvasPositionX, setCanvasPositionX] = useState(0) - const [canvasPositionY, setCanvasPositionY] = useState(0) - const [isDragging, setIsDragging] = useState(false) - const [dragStartX, setDragStartX] = useState(0) - const [dragStartY, setDragStartY] = useState(0) + const [canvasPositionX, setCanvasPositionX] = useState(0); + const [canvasPositionY, setCanvasPositionY] = useState(0); + const [isDragging, setIsDragging] = useState(false); + const [dragStartX, setDragStartX] = useState(0); + const [dragStartY, setDragStartY] = useState(0); - const [canvasScale, setCanvasScale] = useState(6) - const minScale = 1 // TODO: To config - const maxScale = 40 + const [canvasScale, setCanvasScale] = useState(6); + const minScale = 1; // TODO: To config + const maxScale = 40; //TODO: Way to configure tick rates to give smooth xp for all users - - const canvasRef = useRef(null) + + const canvasRef = useRef(null); // Read canvas config from environment variable file json - const width = canvasConfig.canvas.width - const height = canvasConfig.canvas.height - const colors = canvasConfig.colors + const width = canvasConfig.canvas.width; + const height = canvasConfig.canvas.height; + const colors = canvasConfig.colors; - const WS_URL = "ws://" + backendConfig.host + ":" + backendConfig.port + "/ws" + const WS_URL = + "ws://" + backendConfig.host + ":" + backendConfig.port + "/ws"; const { sendJsonMessage, lastJsonMessage, readyState } = useWebSocket( WS_URL, { share: false, shouldReconnect: () => true, - }, - ) - + } + ); + // TODO: Weird positioning behavior when clicking into devtools // Handle wheel event for zooming const handleWheel = (e) => { - let newScale = canvasScale + let newScale = canvasScale; if (e.deltaY < 0) { - newScale = Math.min(maxScale, newScale + 0.2) + newScale = Math.min(maxScale, newScale + 0.2); } else { - newScale = Math.max(minScale, newScale - 0.2) + newScale = Math.max(minScale, newScale - 0.2); } // TODO: Smart positioning of canvas zoom ( zoom to center of mouse pointer ) //let newCanvasPositionX = canvasPositionX @@ -54,223 +55,288 @@ const Canvas = props => { //setCanvasPositionX(newCanvasPositionX) //setCanvasPositionY(newCanvasPositionY) - setCanvasScale(newScale) - } + setCanvasScale(newScale); + }; const handlePointerDown = (e) => { - setIsDragging(true) - setDragStartX(e.clientX) - setDragStartY(e.clientY) - } + setIsDragging(true); + setDragStartX(e.clientX); + setDragStartY(e.clientY); + }; const handlePointerUp = (e) => { - setIsDragging(false) - setDragStartX(0) - setDragStartY(0) - } + setIsDragging(false); + setDragStartX(0); + setDragStartY(0); + }; const handlePointerMove = (e) => { if (isDragging) { // TODO: Prevent dragging outside of canvas container - setCanvasPositionX(canvasPositionX + e.clientX - dragStartX) - setCanvasPositionY(canvasPositionY + e.clientY - dragStartY) - setDragStartX(e.clientX) - setDragStartY(e.clientY) + setCanvasPositionX(canvasPositionX + e.clientX - dragStartX); + setCanvasPositionY(canvasPositionY + e.clientY - dragStartY); + setDragStartX(e.clientX); + setDragStartY(e.clientY); } - } + }; useEffect(() => { - document.addEventListener('pointerup', handlePointerUp) + document.addEventListener("pointerup", handlePointerUp); return () => { - document.removeEventListener('pointerup', handlePointerUp) - } - }, []) + document.removeEventListener("pointerup", handlePointerUp); + }; + }, []); - const [setup, setSetup] = useState(false) - const [pixelPlacedBy, setPixelPlacedBy] = useState("") + const [setup, setSetup] = useState(false); + const [pixelPlacedBy, setPixelPlacedBy] = useState(""); const draw = (ctx, imageData) => { - ctx.canvas.width = width - ctx.canvas.height = height - ctx.putImageData(imageData, 0, 0) + ctx.canvas.width = width; + ctx.canvas.height = height; + ctx.putImageData(imageData, 0, 0); // TODO: Use image-rendering for supported browsers? - } + }; useEffect(() => { if (setup) { - return + return; } - const canvas = canvasRef.current - const context = canvas.getContext('2d') - - let getCanvasEndpoint = backendUrl + "/getCanvas" - fetch(getCanvasEndpoint, {mode: 'cors'}).then(response => { - return response.arrayBuffer() - }).then(data => { - let colorData = new Uint8Array(data, 0, data.byteLength) - let dataArray = [] - // TODO: Think about edge cases - let bitwidth = canvasConfig.colors_bitwidth - let oneByteBitOffset = 8 - bitwidth - let twoByteBitOffset = 16 - bitwidth - for (let bitPos = 0; bitPos < data.byteLength * 8; bitPos += bitwidth) { - let bytePos = Math.floor(bitPos / 8) - let bitOffset = bitPos % 8 - if (bitOffset <= oneByteBitOffset) { - let byte = colorData[bytePos] - let value = (byte >> (oneByteBitOffset - bitOffset)) & 0b11111 - dataArray.push(value) - } else { - let byte = colorData[bytePos] << 8 | colorData[bytePos + 1] - let value = (byte >> (twoByteBitOffset - bitOffset)) & 0b11111 - dataArray.push(value) + const canvas = canvasRef.current; + const context = canvas.getContext("2d"); + + let getCanvasEndpoint = backendUrl + "/getCanvas"; + fetch(getCanvasEndpoint, { mode: "cors" }) + .then((response) => { + return response.arrayBuffer(); + }) + .then((data) => { + let colorData = new Uint8Array(data, 0, data.byteLength); + let dataArray = []; + // TODO: Think about edge cases + let bitwidth = canvasConfig.colors_bitwidth; + let oneByteBitOffset = 8 - bitwidth; + let twoByteBitOffset = 16 - bitwidth; + for (let bitPos = 0; bitPos < data.byteLength * 8; bitPos += bitwidth) { + let bytePos = Math.floor(bitPos / 8); + let bitOffset = bitPos % 8; + if (bitOffset <= oneByteBitOffset) { + let byte = colorData[bytePos]; + let value = (byte >> (oneByteBitOffset - bitOffset)) & 0b11111; + dataArray.push(value); + } else { + let byte = (colorData[bytePos] << 8) | colorData[bytePos + 1]; + let value = (byte >> (twoByteBitOffset - bitOffset)) & 0b11111; + dataArray.push(value); + } } - } - let imageDataArray = [] - for (let i = 0; i < dataArray.length; i++) { - const color = "#" + colors[dataArray[i]] + "FF" - const [r, g, b, a] = color.match(/\w\w/g).map(x => parseInt(x, 16)) - imageDataArray.push(r, g, b, a) - } - const uint8ClampedArray = new Uint8ClampedArray(imageDataArray) - const imageData = new ImageData(uint8ClampedArray, width, height) - draw(context, imageData) - setSetup(true) - }).catch(error => { - //TODO: Notifiy user of error - console.error(error) - }); - - console.log("Connect to websocket") + let imageDataArray = []; + for (let i = 0; i < dataArray.length; i++) { + const color = "#" + colors[dataArray[i]] + "FF"; + const [r, g, b, a] = color.match(/\w\w/g).map((x) => parseInt(x, 16)); + imageDataArray.push(r, g, b, a); + } + const uint8ClampedArray = new Uint8ClampedArray(imageDataArray); + const imageData = new ImageData(uint8ClampedArray, width, height); + draw(context, imageData); + setSetup(true); + }) + .catch((error) => { + //TODO: Notifiy user of error + console.error(error); + }); + + console.log("Connect to websocket"); if (readyState === ReadyState.OPEN) { sendJsonMessage({ event: "subscribe", data: { channel: "general", }, - }) + }); } // TODO: Return a cleanup function to close the websocket / ... - }, [draw, readyState]) + }, [draw, readyState]); useEffect(() => { if (lastJsonMessage) { - const canvas = canvasRef.current - const context = canvas.getContext('2d') - const x = lastJsonMessage.position % width - const y = Math.floor(lastJsonMessage.position / width) - const colorIdx = lastJsonMessage.color - const color = "#" + colors[colorIdx] + "FF" + const canvas = canvasRef.current; + const context = canvas.getContext("2d"); + const x = lastJsonMessage.position % width; + const y = Math.floor(lastJsonMessage.position / width); + const colorIdx = lastJsonMessage.color; + const color = "#" + colors[colorIdx] + "FF"; //const [r, g, b, a] = color.match(/\w\w/g).map(x => parseInt(x, 16)) - context.fillStyle = color - context.fillRect(x, y, 1, 1) - } - }, [lastJsonMessage]) - - const pixelSelect = useCallback((clientX, clientY) => { - const canvas = canvasRef.current - const rect = canvas.getBoundingClientRect() - const x = Math.floor((clientX - rect.left) / (rect.right - rect.left) * width) - const y = Math.floor((clientY - rect.top) / (rect.bottom - rect.top) * height) - if (props.selectedColorId === -1 && props.pixelSelectedMode && props.selectedPositionX === x && props.selectedPositionY === y) { - props.clearPixelSelection() - return + context.fillStyle = color; + context.fillRect(x, y, 1, 1); } - if (x < 0 || x >= width || y < 0 || y >= height) { - return - } - props.setPixelSelection(x, y) - - const position = y * width + x - let getPixelInfoEndpoint = backendUrl + "/getPixelInfo?position=" + position.toString() - fetch(getPixelInfoEndpoint, { - mode: 'cors' - }).then(response => { - return response.text() - }).then(data => { - // TODO: not working - // TODO: Cache pixel info & clear cache on update from websocket - // TODO: Dont query if hover select ( until 1s after hover? ) - setPixelPlacedBy(data) - }).catch(error => { - console.error(error) - //TODO: Handle error - }); - - // TODO: Create a border around the selected pixel - }, [props.setSelectedPositionX, props.setSelectedPositionY, props.setPixelSelectedMode, setPixelPlacedBy, width, height, props.selectedColorId, props.pixelSelectedMode, props.selectedPositionX, props.selectedPositionY]) + }, [lastJsonMessage]); + + const pixelSelect = useCallback( + (clientX, clientY) => { + const canvas = canvasRef.current; + const rect = canvas.getBoundingClientRect(); + const x = Math.floor( + ((clientX - rect.left) / (rect.right - rect.left)) * width + ); + const y = Math.floor( + ((clientY - rect.top) / (rect.bottom - rect.top)) * height + ); + if ( + props.selectedColorId === -1 && + props.pixelSelectedMode && + props.selectedPositionX === x && + props.selectedPositionY === y + ) { + props.clearPixelSelection(); + return; + } + if (x < 0 || x >= width || y < 0 || y >= height) { + return; + } + props.setPixelSelection(x, y); + + const position = y * width + x; + let getPixelInfoEndpoint = + backendUrl + "/getPixelInfo?position=" + position.toString(); + fetch(getPixelInfoEndpoint, { + mode: "cors", + }) + .then((response) => { + return response.text(); + }) + .then((data) => { + // TODO: not working + // TODO: Cache pixel info & clear cache on update from websocket + // TODO: Dont query if hover select ( until 1s after hover? ) + setPixelPlacedBy(data); + }) + .catch((error) => { + console.error(error); + //TODO: Handle error + }); + + // TODO: Create a border around the selected pixel + }, + [ + props.setSelectedPositionX, + props.setSelectedPositionY, + props.setPixelSelectedMode, + setPixelPlacedBy, + width, + height, + props.selectedColorId, + props.pixelSelectedMode, + props.selectedPositionX, + props.selectedPositionY, + ] + ); const pixelClicked = (e) => { - pixelSelect(e.clientX, e.clientY) + pixelSelect(e.clientX, e.clientY); if (props.selectedColorId === -1) { - return + return; } if (props.selectedPositionX === null || props.selectedPositionY === null) { - return + return; } - const x = props.selectedPositionX - const y = props.selectedPositionY - const colorIdx = props.selectedColorId - let placePixelEndpoint = backendUrl + "/placePixelDevnet" + const position = props.selectedPositionX + props.selectedPositionY; + const colorIdx = props.selectedColorId; + let placePixelEndpoint = backendUrl + "/placePixelDevnet"; fetch(placePixelEndpoint, { - mode: 'cors', - method: 'POST', - body: JSON.stringify({x: x.toString(), y: y.toString(), color: colorIdx.toString()}) - }).then(response => { - return response.text() - }).then(data => { - console.log(data) - }).catch(error => { - console.error("Error placing pixel") - console.error(error) - }); - props.clearPixelSelection() - props.setSelectedColorId(-1) + mode: "cors", + method: "POST", + body: JSON.stringify({ + position: position.toString(), + color: colorIdx.toString(), + }), + }) + .then((response) => { + return response.text(); + }) + .then((data) => { + console.log(data); + }) + .catch((error) => { + console.error("Error placing pixel"); + console.error(error); + }); + props.clearPixelSelection(); + props.setSelectedColorId(-1); // TODO: Optimistic update - } - + }; + // TODO: Deselect pixel when clicking outside of color palette or pixel // TODO: Show small position vec in bottom right corner of canvas const getSelectedColor = () => { - console.log(props.selectedColorId, props.selectedPositionX, props.selectedPositionY) + console.log( + props.selectedColorId, + props.selectedPositionX, + props.selectedPositionY + ); if (props.selectedPositionX === null || props.selectedPositionY === null) { - return null + return null; } if (props.selectedColorId === -1) { - return null + return null; } - return "#" + colors[props.selectedColorId] + "FF" - } + return "#" + colors[props.selectedColorId] + "FF"; + }; const getSelectorsColor = () => { if (props.selectedPositionX === null || props.selectedPositionY === null) { - return null + return null; } if (props.selectedColorId === -1) { - let color = canvasRef.current.getContext('2d').getImageData(props.selectedPositionX, props.selectedPositionY, 1, 1).data - return "#" + color[0].toString(16).padStart(2, '0') + color[1].toString(16).padStart(2, '0') + color[2].toString(16).padStart(2, '0') + color[3].toString(16).padStart(2, '0') + let color = canvasRef.current + .getContext("2d") + .getImageData( + props.selectedPositionX, + props.selectedPositionY, + 1, + 1 + ).data; + return ( + "#" + + color[0].toString(16).padStart(2, "0") + + color[1].toString(16).padStart(2, "0") + + color[2].toString(16).padStart(2, "0") + + color[3].toString(16).padStart(2, "0") + ); } - return "#" + colors[props.selectedColorId] + "FF" - } + return "#" + colors[props.selectedColorId] + "FF"; + }; const getSelectorsColorInverse = () => { if (props.selectedPositionX === null || props.selectedPositionY === null) { - return null + return null; } if (props.selectedColorId === -1) { - let color = canvasRef.current.getContext('2d').getImageData(props.selectedPositionX, props.selectedPositionY, 1, 1).data - return "#" + (255 - color[0]).toString(16).padStart(2, '0') + (255 - color[1]).toString(16).padStart(2, '0') + (255 - color[2]).toString(16).padStart(2, '0') + color[3].toString(16).padStart(2, '0') + let color = canvasRef.current + .getContext("2d") + .getImageData( + props.selectedPositionX, + props.selectedPositionY, + 1, + 1 + ).data; + return ( + "#" + + (255 - color[0]).toString(16).padStart(2, "0") + + (255 - color[1]).toString(16).padStart(2, "0") + + (255 - color[2]).toString(16).padStart(2, "0") + + color[3].toString(16).padStart(2, "0") + ); } - return "#" + colors[props.selectedColorId] + "FF" - } + return "#" + colors[props.selectedColorId] + "FF"; + }; useEffect(() => { const setFromEvent = (e) => { if (props.selectedColorId === -1) { - return + return; } - pixelSelect(e.clientX, e.clientY) + pixelSelect(e.clientX, e.clientY); }; window.addEventListener("mousemove", setFromEvent); @@ -288,19 +354,44 @@ const Canvas = props => { // TODO: both place options return ( -
-
-
- { props.pixelSelectedMode && ( -
-
+
+
+
+ {props.pixelSelectedMode && ( +
+
)} - +
); -} +}; -export default Canvas +export default Canvas; From 93fc085c45d7cad2199898616dfd122d022aea71 Mon Sep 17 00:00:00 2001 From: Adeyemi Date: Wed, 10 Apr 2024 12:45:54 +0100 Subject: [PATCH 2/4] feat/#4-Pass pixel position instead of (x, y) --- frontend/src/canvas/Canvas.js | 450 ++++++++++++++-------------------- 1 file changed, 181 insertions(+), 269 deletions(-) diff --git a/frontend/src/canvas/Canvas.js b/frontend/src/canvas/Canvas.js index 07d2807a..5eafb4ce 100644 --- a/frontend/src/canvas/Canvas.js +++ b/frontend/src/canvas/Canvas.js @@ -1,51 +1,50 @@ -import React, { useCallback, useRef, useEffect, useState } from "react"; -import useWebSocket, { ReadyState } from "react-use-websocket"; -import "./Canvas.css"; +import React, { useCallback, useRef, useEffect, useState } from 'react' +import useWebSocket, { ReadyState } from 'react-use-websocket' +import './Canvas.css'; // import TemplateOverlay from './TemplateOverlay.js'; -import canvasConfig from "../configs/canvas.config.json"; -import backendConfig from "../configs/backend.config.json"; +import canvasConfig from "../configs/canvas.config.json" +import backendConfig from "../configs/backend.config.json" -const Canvas = (props) => { - const backendUrl = "http://" + backendConfig.host + ":" + backendConfig.port; +const Canvas = props => { + const backendUrl = "http://" + backendConfig.host + ":" + backendConfig.port // TODO: Pressing "Canvas" resets the view / positioning - const [canvasPositionX, setCanvasPositionX] = useState(0); - const [canvasPositionY, setCanvasPositionY] = useState(0); - const [isDragging, setIsDragging] = useState(false); - const [dragStartX, setDragStartX] = useState(0); - const [dragStartY, setDragStartY] = useState(0); + const [canvasPositionX, setCanvasPositionX] = useState(0) + const [canvasPositionY, setCanvasPositionY] = useState(0) + const [isDragging, setIsDragging] = useState(false) + const [dragStartX, setDragStartX] = useState(0) + const [dragStartY, setDragStartY] = useState(0) - const [canvasScale, setCanvasScale] = useState(6); - const minScale = 1; // TODO: To config - const maxScale = 40; + const [canvasScale, setCanvasScale] = useState(6) + const minScale = 1 // TODO: To config + const maxScale = 40 //TODO: Way to configure tick rates to give smooth xp for all users - - const canvasRef = useRef(null); + + const canvasRef = useRef(null) // Read canvas config from environment variable file json - const width = canvasConfig.canvas.width; - const height = canvasConfig.canvas.height; - const colors = canvasConfig.colors; + const width = canvasConfig.canvas.width + const height = canvasConfig.canvas.height + const colors = canvasConfig.colors - const WS_URL = - "ws://" + backendConfig.host + ":" + backendConfig.port + "/ws"; + const WS_URL = "ws://" + backendConfig.host + ":" + backendConfig.port + "/ws" const { sendJsonMessage, lastJsonMessage, readyState } = useWebSocket( WS_URL, { share: false, shouldReconnect: () => true, - } - ); - + }, + ) + // TODO: Weird positioning behavior when clicking into devtools // Handle wheel event for zooming const handleWheel = (e) => { - let newScale = canvasScale; + let newScale = canvasScale if (e.deltaY < 0) { - newScale = Math.min(maxScale, newScale + 0.2); + newScale = Math.min(maxScale, newScale + 0.2) } else { - newScale = Math.max(minScale, newScale - 0.2); + newScale = Math.max(minScale, newScale - 0.2) } // TODO: Smart positioning of canvas zoom ( zoom to center of mouse pointer ) //let newCanvasPositionX = canvasPositionX @@ -55,192 +54,163 @@ const Canvas = (props) => { //setCanvasPositionX(newCanvasPositionX) //setCanvasPositionY(newCanvasPositionY) - setCanvasScale(newScale); - }; + setCanvasScale(newScale) + } const handlePointerDown = (e) => { - setIsDragging(true); - setDragStartX(e.clientX); - setDragStartY(e.clientY); - }; + setIsDragging(true) + setDragStartX(e.clientX) + setDragStartY(e.clientY) + } const handlePointerUp = (e) => { - setIsDragging(false); - setDragStartX(0); - setDragStartY(0); - }; + setIsDragging(false) + setDragStartX(0) + setDragStartY(0) + } const handlePointerMove = (e) => { if (isDragging) { // TODO: Prevent dragging outside of canvas container - setCanvasPositionX(canvasPositionX + e.clientX - dragStartX); - setCanvasPositionY(canvasPositionY + e.clientY - dragStartY); - setDragStartX(e.clientX); - setDragStartY(e.clientY); + setCanvasPositionX(canvasPositionX + e.clientX - dragStartX) + setCanvasPositionY(canvasPositionY + e.clientY - dragStartY) + setDragStartX(e.clientX) + setDragStartY(e.clientY) } - }; + } useEffect(() => { - document.addEventListener("pointerup", handlePointerUp); + document.addEventListener('pointerup', handlePointerUp) return () => { - document.removeEventListener("pointerup", handlePointerUp); - }; - }, []); + document.removeEventListener('pointerup', handlePointerUp) + } + }, []) - const [setup, setSetup] = useState(false); - const [pixelPlacedBy, setPixelPlacedBy] = useState(""); + const [setup, setSetup] = useState(false) + const [pixelPlacedBy, setPixelPlacedBy] = useState("") const draw = (ctx, imageData) => { - ctx.canvas.width = width; - ctx.canvas.height = height; - ctx.putImageData(imageData, 0, 0); + ctx.canvas.width = width + ctx.canvas.height = height + ctx.putImageData(imageData, 0, 0) // TODO: Use image-rendering for supported browsers? - }; + } useEffect(() => { if (setup) { - return; + return } - const canvas = canvasRef.current; - const context = canvas.getContext("2d"); - - let getCanvasEndpoint = backendUrl + "/getCanvas"; - fetch(getCanvasEndpoint, { mode: "cors" }) - .then((response) => { - return response.arrayBuffer(); - }) - .then((data) => { - let colorData = new Uint8Array(data, 0, data.byteLength); - let dataArray = []; - // TODO: Think about edge cases - let bitwidth = canvasConfig.colors_bitwidth; - let oneByteBitOffset = 8 - bitwidth; - let twoByteBitOffset = 16 - bitwidth; - for (let bitPos = 0; bitPos < data.byteLength * 8; bitPos += bitwidth) { - let bytePos = Math.floor(bitPos / 8); - let bitOffset = bitPos % 8; - if (bitOffset <= oneByteBitOffset) { - let byte = colorData[bytePos]; - let value = (byte >> (oneByteBitOffset - bitOffset)) & 0b11111; - dataArray.push(value); - } else { - let byte = (colorData[bytePos] << 8) | colorData[bytePos + 1]; - let value = (byte >> (twoByteBitOffset - bitOffset)) & 0b11111; - dataArray.push(value); - } + const canvas = canvasRef.current + const context = canvas.getContext('2d') + + let getCanvasEndpoint = backendUrl + "/getCanvas" + fetch(getCanvasEndpoint, {mode: 'cors'}).then(response => { + return response.arrayBuffer() + }).then(data => { + let colorData = new Uint8Array(data, 0, data.byteLength) + let dataArray = [] + // TODO: Think about edge cases + let bitwidth = canvasConfig.colors_bitwidth + let oneByteBitOffset = 8 - bitwidth + let twoByteBitOffset = 16 - bitwidth + for (let bitPos = 0; bitPos < data.byteLength * 8; bitPos += bitwidth) { + let bytePos = Math.floor(bitPos / 8) + let bitOffset = bitPos % 8 + if (bitOffset <= oneByteBitOffset) { + let byte = colorData[bytePos] + let value = (byte >> (oneByteBitOffset - bitOffset)) & 0b11111 + dataArray.push(value) + } else { + let byte = colorData[bytePos] << 8 | colorData[bytePos + 1] + let value = (byte >> (twoByteBitOffset - bitOffset)) & 0b11111 + dataArray.push(value) } - let imageDataArray = []; - for (let i = 0; i < dataArray.length; i++) { - const color = "#" + colors[dataArray[i]] + "FF"; - const [r, g, b, a] = color.match(/\w\w/g).map((x) => parseInt(x, 16)); - imageDataArray.push(r, g, b, a); - } - const uint8ClampedArray = new Uint8ClampedArray(imageDataArray); - const imageData = new ImageData(uint8ClampedArray, width, height); - draw(context, imageData); - setSetup(true); - }) - .catch((error) => { - //TODO: Notifiy user of error - console.error(error); - }); - - console.log("Connect to websocket"); + } + let imageDataArray = [] + for (let i = 0; i < dataArray.length; i++) { + const color = "#" + colors[dataArray[i]] + "FF" + const [r, g, b, a] = color.match(/\w\w/g).map(x => parseInt(x, 16)) + imageDataArray.push(r, g, b, a) + } + const uint8ClampedArray = new Uint8ClampedArray(imageDataArray) + const imageData = new ImageData(uint8ClampedArray, width, height) + draw(context, imageData) + setSetup(true) + }).catch(error => { + //TODO: Notifiy user of error + console.error(error) + }); + + console.log("Connect to websocket") if (readyState === ReadyState.OPEN) { sendJsonMessage({ event: "subscribe", data: { channel: "general", }, - }); + }) } // TODO: Return a cleanup function to close the websocket / ... - }, [draw, readyState]); + }, [draw, readyState]) useEffect(() => { if (lastJsonMessage) { - const canvas = canvasRef.current; - const context = canvas.getContext("2d"); - const x = lastJsonMessage.position % width; - const y = Math.floor(lastJsonMessage.position / width); - const colorIdx = lastJsonMessage.color; - const color = "#" + colors[colorIdx] + "FF"; + const canvas = canvasRef.current + const context = canvas.getContext('2d') + const x = lastJsonMessage.position % width + const y = Math.floor(lastJsonMessage.position / width) + const colorIdx = lastJsonMessage.color + const color = "#" + colors[colorIdx] + "FF" //const [r, g, b, a] = color.match(/\w\w/g).map(x => parseInt(x, 16)) - context.fillStyle = color; - context.fillRect(x, y, 1, 1); + context.fillStyle = color + context.fillRect(x, y, 1, 1) } - }, [lastJsonMessage]); - - const pixelSelect = useCallback( - (clientX, clientY) => { - const canvas = canvasRef.current; - const rect = canvas.getBoundingClientRect(); - const x = Math.floor( - ((clientX - rect.left) / (rect.right - rect.left)) * width - ); - const y = Math.floor( - ((clientY - rect.top) / (rect.bottom - rect.top)) * height - ); - if ( - props.selectedColorId === -1 && - props.pixelSelectedMode && - props.selectedPositionX === x && - props.selectedPositionY === y - ) { - props.clearPixelSelection(); - return; - } - if (x < 0 || x >= width || y < 0 || y >= height) { - return; - } - props.setPixelSelection(x, y); - - const position = y * width + x; - let getPixelInfoEndpoint = - backendUrl + "/getPixelInfo?position=" + position.toString(); - fetch(getPixelInfoEndpoint, { - mode: "cors", - }) - .then((response) => { - return response.text(); - }) - .then((data) => { - // TODO: not working - // TODO: Cache pixel info & clear cache on update from websocket - // TODO: Dont query if hover select ( until 1s after hover? ) - setPixelPlacedBy(data); - }) - .catch((error) => { - console.error(error); - //TODO: Handle error - }); - - // TODO: Create a border around the selected pixel - }, - [ - props.setSelectedPositionX, - props.setSelectedPositionY, - props.setPixelSelectedMode, - setPixelPlacedBy, - width, - height, - props.selectedColorId, - props.pixelSelectedMode, - props.selectedPositionX, - props.selectedPositionY, - ] - ); + }, [lastJsonMessage]) + + const pixelSelect = useCallback((clientX, clientY) => { + const canvas = canvasRef.current + const rect = canvas.getBoundingClientRect() + const x = Math.floor((clientX - rect.left) / (rect.right - rect.left) * width) + const y = Math.floor((clientY - rect.top) / (rect.bottom - rect.top) * height) + if (props.selectedColorId === -1 && props.pixelSelectedMode && props.selectedPositionX === x && props.selectedPositionY === y) { + props.clearPixelSelection() + return + } + if (x < 0 || x >= width || y < 0 || y >= height) { + return + } + props.setPixelSelection(x, y) + + const position = y * width + x + let getPixelInfoEndpoint = backendUrl + "/getPixelInfo?position=" + position.toString() + fetch(getPixelInfoEndpoint, { + mode: 'cors' + }).then(response => { + return response.text() + }).then(data => { + // TODO: not working + // TODO: Cache pixel info & clear cache on update from websocket + // TODO: Dont query if hover select ( until 1s after hover? ) + setPixelPlacedBy(data) + }).catch(error => { + console.error(error) + //TODO: Handle error + }); + + // TODO: Create a border around the selected pixel + }, [props.setSelectedPositionX, props.setSelectedPositionY, props.setPixelSelectedMode, setPixelPlacedBy, width, height, props.selectedColorId, props.pixelSelectedMode, props.selectedPositionX, props.selectedPositionY]) const pixelClicked = (e) => { - pixelSelect(e.clientX, e.clientY); + pixelSelect(e.clientX, e.clientY) if (props.selectedColorId === -1) { - return; + return } if (props.selectedPositionX === null || props.selectedPositionY === null) { - return; + return } - const position = props.selectedPositionX + props.selectedPositionY; + + const position =props.selectedPositionX + props.selectedPositionY; const colorIdx = props.selectedColorId; let placePixelEndpoint = backendUrl + "/placePixelDevnet"; fetch(placePixelEndpoint, { @@ -250,93 +220,60 @@ const Canvas = (props) => { position: position.toString(), color: colorIdx.toString(), }), - }) - .then((response) => { - return response.text(); - }) - .then((data) => { - console.log(data); - }) - .catch((error) => { - console.error("Error placing pixel"); - console.error(error); - }); - props.clearPixelSelection(); - props.setSelectedColorId(-1); + }).then(response => { + return response.text() + }).then(data => { + console.log(data) + }).catch(error => { + console.error("Error placing pixel") + console.error(error) + }); + props.clearPixelSelection() + props.setSelectedColorId(-1) // TODO: Optimistic update - }; - + } + // TODO: Deselect pixel when clicking outside of color palette or pixel // TODO: Show small position vec in bottom right corner of canvas const getSelectedColor = () => { - console.log( - props.selectedColorId, - props.selectedPositionX, - props.selectedPositionY - ); + console.log(props.selectedColorId, props.selectedPositionX, props.selectedPositionY) if (props.selectedPositionX === null || props.selectedPositionY === null) { - return null; + return null } if (props.selectedColorId === -1) { - return null; + return null } - return "#" + colors[props.selectedColorId] + "FF"; - }; + return "#" + colors[props.selectedColorId] + "FF" + } const getSelectorsColor = () => { if (props.selectedPositionX === null || props.selectedPositionY === null) { - return null; + return null } if (props.selectedColorId === -1) { - let color = canvasRef.current - .getContext("2d") - .getImageData( - props.selectedPositionX, - props.selectedPositionY, - 1, - 1 - ).data; - return ( - "#" + - color[0].toString(16).padStart(2, "0") + - color[1].toString(16).padStart(2, "0") + - color[2].toString(16).padStart(2, "0") + - color[3].toString(16).padStart(2, "0") - ); + let color = canvasRef.current.getContext('2d').getImageData(props.selectedPositionX, props.selectedPositionY, 1, 1).data + return "#" + color[0].toString(16).padStart(2, '0') + color[1].toString(16).padStart(2, '0') + color[2].toString(16).padStart(2, '0') + color[3].toString(16).padStart(2, '0') } - return "#" + colors[props.selectedColorId] + "FF"; - }; + return "#" + colors[props.selectedColorId] + "FF" + } const getSelectorsColorInverse = () => { if (props.selectedPositionX === null || props.selectedPositionY === null) { - return null; + return null } if (props.selectedColorId === -1) { - let color = canvasRef.current - .getContext("2d") - .getImageData( - props.selectedPositionX, - props.selectedPositionY, - 1, - 1 - ).data; - return ( - "#" + - (255 - color[0]).toString(16).padStart(2, "0") + - (255 - color[1]).toString(16).padStart(2, "0") + - (255 - color[2]).toString(16).padStart(2, "0") + - color[3].toString(16).padStart(2, "0") - ); + let color = canvasRef.current.getContext('2d').getImageData(props.selectedPositionX, props.selectedPositionY, 1, 1).data + return "#" + (255 - color[0]).toString(16).padStart(2, '0') + (255 - color[1]).toString(16).padStart(2, '0') + (255 - color[2]).toString(16).padStart(2, '0') + color[3].toString(16).padStart(2, '0') } - return "#" + colors[props.selectedColorId] + "FF"; - }; + return "#" + colors[props.selectedColorId] + "FF" + } useEffect(() => { const setFromEvent = (e) => { if (props.selectedColorId === -1) { - return; + return } - pixelSelect(e.clientX, e.clientY); + pixelSelect(e.clientX, e.clientY) }; window.addEventListener("mousemove", setFromEvent); @@ -354,44 +291,19 @@ const Canvas = (props) => { // TODO: both place options return ( -
-
-
- {props.pixelSelectedMode && ( -
-
+
+
+
+ { props.pixelSelectedMode && ( +
+
)} - +
); -}; +} -export default Canvas; +export default Canvas \ No newline at end of file From 2c7740e7f6f0572c22a9c5282428ab0c88d03f95 Mon Sep 17 00:00:00 2001 From: Adeyemi Date: Wed, 10 Apr 2024 12:52:53 +0100 Subject: [PATCH 3/4] fix: removed linter formatting --- frontend/src/canvas/Canvas.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/canvas/Canvas.js b/frontend/src/canvas/Canvas.js index 5eafb4ce..8d9d00f0 100644 --- a/frontend/src/canvas/Canvas.js +++ b/frontend/src/canvas/Canvas.js @@ -210,9 +210,9 @@ const Canvas = props => { return } - const position =props.selectedPositionX + props.selectedPositionY; - const colorIdx = props.selectedColorId; - let placePixelEndpoint = backendUrl + "/placePixelDevnet"; + const position = props.selectedPositionX + props.selectedPositionY + const colorIdx = props.selectedColorId + let placePixelEndpoint = backendUrl + "/placePixelDevnet" fetch(placePixelEndpoint, { mode: "cors", method: "POST", From d65b95ac584252dc8828216873bb3c95bcf327a1 Mon Sep 17 00:00:00 2001 From: Adeyemi Date: Wed, 10 Apr 2024 17:05:37 +0100 Subject: [PATCH 4/4] fix: passed the full position from the frontend --- backend/routes/pixel.go | 4 ++-- frontend/src/canvas/Canvas.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/routes/pixel.go b/backend/routes/pixel.go index 48bd89dd..99560601 100644 --- a/backend/routes/pixel.go +++ b/backend/routes/pixel.go @@ -71,9 +71,9 @@ func placePixelDevnet(w http.ResponseWriter, r *http.Request) { } shellCmd := backend.ArtPeaceBackend.BackendConfig.Scripts.PlacePixelDevnet - computedPosition := x + y * int(backend.ArtPeaceBackend.CanvasConfig.Canvas.Width) contract := os.Getenv("ART_PEACE_CONTRACT_ADDRESS") - cmd := exec.Command(shellCmd, contract, "place_pixel", strconv.Itoa(computedPosition), jsonBody["color"]) + + cmd := exec.Command(shellCmd, contract, "place_pixel", strconv.Itoa(position), jsonBody["color"]) _, err = cmd.Output() if err != nil { fmt.Println("Error executing shell command: ", err) diff --git a/frontend/src/canvas/Canvas.js b/frontend/src/canvas/Canvas.js index 8d9d00f0..cdbac741 100644 --- a/frontend/src/canvas/Canvas.js +++ b/frontend/src/canvas/Canvas.js @@ -210,7 +210,7 @@ const Canvas = props => { return } - const position = props.selectedPositionX + props.selectedPositionY + const position = props.selectedPositionX + props.selectedPositionY * width const colorIdx = props.selectedColorId let placePixelEndpoint = backendUrl + "/placePixelDevnet" fetch(placePixelEndpoint, {