From 989aeabce7d23f2db857f66ad9246b18d4c34764 Mon Sep 17 00:00:00 2001 From: Adeyemi Date: Thu, 18 Apr 2024 02:18:19 +0100 Subject: [PATCH 1/4] feat: improved canvas zoom --- frontend/src/canvas/Canvas.js | 455 +++++++++++++++++++--------------- 1 file changed, 253 insertions(+), 202 deletions(-) diff --git a/frontend/src/canvas/Canvas.js b/frontend/src/canvas/Canvas.js index 43302efb..27567e21 100644 --- a/frontend/src/canvas/Canvas.js +++ b/frontend/src/canvas/Canvas.js @@ -1,214 +1,214 @@ -import React, { useCallback, useRef, useEffect, useState } from 'react' -import useWebSocket, { ReadyState } from 'react-use-websocket' -import './Canvas.css'; +// export default Canvas; +import * as d3 from "d3"; +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 = React.memo((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 [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); + const canvasPositionRef = useRef(null); + const canvasScaleRef = 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 - if (e.deltaY < 0) { - newScale = Math.min(maxScale, newScale + 0.2) - } else { - newScale = Math.max(minScale, newScale - 0.2) } - // TODO: Smart positioning of canvas zoom ( zoom to center of mouse pointer ) - //let newCanvasPositionX = canvasPositionX - //let newCanvasPositionY = canvasPositionY - //const canvasOriginX = canvasPositionX + width / 2 - //const canvasOriginY = canvasPositionY + height / 2 - //setCanvasPositionX(newCanvasPositionX) - //setCanvasPositionY(newCanvasPositionY) - - setCanvasScale(newScale) - } - - const handlePointerDown = (e) => { - setIsDragging(true) - setDragStartX(e.clientX) - setDragStartY(e.clientY) - } - - const handlePointerUp = () => { - 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) - } - } + ); + // TODO: Weird positioning behavior when clicking into devtools useEffect(() => { - document.addEventListener('pointerup', handlePointerUp) + const canvas = d3.select(canvasPositionRef.current); + const zoom = d3.zoom().on("zoom", zoomHandler); + + // Set default zoom level and center the canvas + canvas + .call(zoom) + .call(zoom.transform, d3.zoomIdentity.translate(0, 0).scale(4)); // Default transform; return () => { - document.removeEventListener('pointerup', handlePointerUp) - } - }, []) + canvas.on(".zoom", null); // Clean up zoom event listeners + }; + }, []); + + const zoomHandler = (event) => { + const ele = canvasScaleRef.current; + const { + k: newScale, + x: newCanvasPositionX, + y: newCanvasPositionY, + } = event.transform; + const transformValue = `translate(${newCanvasPositionX}px, ${newCanvasPositionY}px) scale(${newScale})`; + ele.style.transform = transformValue; + }; - const [setup, setSetup] = useState(false) + const [setup, setSetup] = useState(false); - const draw = useCallback((ctx, imageData) => { - ctx.canvas.width = width - ctx.canvas.height = height - ctx.putImageData(imageData, 0, 0) - // TODO: Use image-rendering for supported browsers? - }, [width, height]) + const draw = useCallback( + (ctx, imageData) => { + ctx.canvas.width = width; + ctx.canvas.height = height; + ctx.putImageData(imageData, 0, 0); + // TODO: Use image-rendering for supported browsers? + }, + [width, height] + ); useEffect(() => { if (setup) { - return + return; } - const canvas = canvasRef.current - const context = canvas.getContext('2d') + 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 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) - }); + 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") + console.log("Connect to websocket"); if (readyState === ReadyState.OPEN) { sendJsonMessage({ event: "subscribe", data: { channel: "general", }, - }) + }); } // TODO: Return a cleanup function to close the websocket / ... - }, [readyState, sendJsonMessage, setup, colors, width, height, backendUrl, draw]) + }, [ + readyState, + sendJsonMessage, + setup, + colors, + width, + height, + backendUrl, + draw, + ]); 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, colors, width]) + }, [lastJsonMessage, colors, width]); - 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: Cache pixel info & clear cache on update from websocket - // TODO: Dont query if hover select ( until 1s after hover? ) - props.setPixelPlacedBy(data) - }).catch(error => { - console.error(error) - }); + 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); - }, [props, width, height, backendUrl]) + const position = y * width + x; + let getPixelInfoEndpoint = + backendUrl + "/getPixelInfo?position=" + position.toString(); + fetch(getPixelInfoEndpoint, { + mode: "cors", + }) + .then((response) => { + return response.text(); + }) + .then((data) => { + // TODO: Cache pixel info & clear cache on update from websocket + // TODO: Dont query if hover select ( until 1s after hover? ) + props.setPixelPlacedBy(data); + }) + .catch((error) => { + console.error(error); + }); + }, + [props, width, height, backendUrl] + ); 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 * width - const colorIdx = props.selectedColorId - let placePixelEndpoint = backendUrl + "/placePixelDevnet" + + const position = props.selectedPositionX + props.selectedPositionY * width; + const colorIdx = props.selectedColorId; + let placePixelEndpoint = backendUrl + "/placePixelDevnet"; fetch(placePixelEndpoint, { mode: "cors", method: "POST", @@ -216,60 +216,93 @@ 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); @@ -278,6 +311,12 @@ const Canvas = props => { }; }, [props.selectedColorId, pixelSelect]); + // const transform = `scale(${toDomPrecision( + // canvasScale + // )}) translate(${toDomPrecision(canvasPositionX)}px, ${toDomPrecision( + // canvasPositionY + // )}px))`; + // TODO //const templateImage = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 4, 3] //const templateWidth = 4 @@ -287,19 +326,31 @@ const Canvas = props => { // TODO: both place options return ( -
-
-
- { props.pixelSelectedMode && ( -
-
+
+
+
+ {props.pixelSelectedMode && ( +
+
)} - +
); -} +}); -export default Canvas \ No newline at end of file +export default Canvas; From 89e74561cce2b5d43f755c245fd942c9297dc3ae Mon Sep 17 00:00:00 2001 From: Adeyemi Date: Thu, 18 Apr 2024 02:30:33 +0100 Subject: [PATCH 2/4] feat: improved canvas and fix lint format --- frontend/src/canvas/Canvas.js | 385 +++++++++++++--------------------- 1 file changed, 151 insertions(+), 234 deletions(-) diff --git a/frontend/src/canvas/Canvas.js b/frontend/src/canvas/Canvas.js index 27567e21..e58baadc 100644 --- a/frontend/src/canvas/Canvas.js +++ b/frontend/src/canvas/Canvas.js @@ -1,45 +1,43 @@ -// export default Canvas; -import * as d3 from "d3"; -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 { select, zoom, zoomIdentity } from "d3" +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 = React.memo((props) => { - const backendUrl = "http://" + backendConfig.host + ":" + backendConfig.port; - // TODO: Pressing "Canvas" resets the view / positioning +const Canvas = props => { + const backendUrl = "http://" + backendConfig.host + ":" + backendConfig.port + //TODO: Pressing "Canvas" resets the view / positioning //TODO: Way to configure tick rates to give smooth xp for all users - const canvasRef = useRef(null); - const canvasPositionRef = useRef(null); - const canvasScaleRef = useRef(null); + const canvasRef = useRef(null) + const canvasPositionRef = useRef(null) + const canvasScaleRef = 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 useEffect(() => { - const canvas = d3.select(canvasPositionRef.current); - const zoom = d3.zoom().on("zoom", zoomHandler); + const canvas = select(canvasPositionRef.current) + const Dzoom = zoom().on("zoom", zoomHandler) // Set default zoom level and center the canvas canvas - .call(zoom) - .call(zoom.transform, d3.zoomIdentity.translate(0, 0).scale(4)); // Default transform; + .call(Dzoom) + .call(Dzoom.transform, zoomIdentity.translate(0, 0).scale(4)) return () => { canvas.on(".zoom", null); // Clean up zoom event listeners @@ -47,168 +45,138 @@ const Canvas = React.memo((props) => { }, []); const zoomHandler = (event) => { - const ele = canvasScaleRef.current; + const ele = canvasScaleRef.current const { k: newScale, x: newCanvasPositionX, y: newCanvasPositionY, } = event.transform; - const transformValue = `translate(${newCanvasPositionX}px, ${newCanvasPositionY}px) scale(${newScale})`; - ele.style.transform = transformValue; - }; + const transformValue = `translate(${newCanvasPositionX}px, ${newCanvasPositionY}px) scale(${newScale})` + ele.style.transform = transformValue + } - const [setup, setSetup] = useState(false); + const [setup, setSetup] = useState(false) - const draw = useCallback( - (ctx, imageData) => { - ctx.canvas.width = width; - ctx.canvas.height = height; - ctx.putImageData(imageData, 0, 0); - // TODO: Use image-rendering for supported browsers? - }, - [width, height] - ); + const draw = useCallback((ctx, imageData) => { + ctx.canvas.width = width + ctx.canvas.height = height + ctx.putImageData(imageData, 0, 0) + // TODO: Use image-rendering for supported browsers? + }, [width, height]) useEffect(() => { if (setup) { - return; + return } - const canvas = canvasRef.current; - const context = canvas.getContext("2d"); + 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 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); - }); + } + 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"); + console.log("Connect to websocket") if (readyState === ReadyState.OPEN) { sendJsonMessage({ event: "subscribe", data: { channel: "general", }, - }); + }) } // TODO: Return a cleanup function to close the websocket / ... - }, [ - readyState, - sendJsonMessage, - setup, - colors, - width, - height, - backendUrl, - draw, - ]); + }, [readyState, sendJsonMessage, setup, colors, width, height, backendUrl, draw]) 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, colors, width]); + }, [lastJsonMessage, colors, width]) - 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 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: Cache pixel info & clear cache on update from websocket - // TODO: Dont query if hover select ( until 1s after hover? ) - props.setPixelPlacedBy(data); - }) - .catch((error) => { - console.error(error); - }); - }, - [props, width, height, backendUrl] - ); + const position = y * width + x + let getPixelInfoEndpoint = backendUrl + "/getPixelInfo?position=" + position.toString() + fetch(getPixelInfoEndpoint, { + mode: 'cors' + }).then(response => { + return response.text() + }).then(data => { + // TODO: Cache pixel info & clear cache on update from websocket + // TODO: Dont query if hover select ( until 1s after hover? ) + props.setPixelPlacedBy(data) + }).catch(error => { + console.error(error) + }); + + }, [props, width, height, backendUrl]) 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 * width; - const colorIdx = props.selectedColorId; - let placePixelEndpoint = backendUrl + "/placePixelDevnet"; + const position = props.selectedPositionX + props.selectedPositionY * width + const colorIdx = props.selectedColorId + let placePixelEndpoint = backendUrl + "/placePixelDevnet" fetch(placePixelEndpoint, { mode: "cors", method: "POST", @@ -216,93 +184,60 @@ const Canvas = React.memo((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); @@ -311,12 +246,6 @@ const Canvas = React.memo((props) => { }; }, [props.selectedColorId, pixelSelect]); - // const transform = `scale(${toDomPrecision( - // canvasScale - // )}) translate(${toDomPrecision(canvasPositionX)}px, ${toDomPrecision( - // canvasPositionY - // )}px))`; - // TODO //const templateImage = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 4, 3] //const templateWidth = 4 @@ -328,22 +257,10 @@ const Canvas = React.memo((props) => { return (
-
+
{props.pixelSelectedMode && ( -
-
+
+
)} @@ -351,6 +268,6 @@ const Canvas = React.memo((props) => {
); -}); +} -export default Canvas; +export default Canvas From 5f7ac084a68f10b2d110a463d485ff3bd7e1b838 Mon Sep 17 00:00:00 2001 From: Adeyemi Date: Thu, 18 Apr 2024 02:32:12 +0100 Subject: [PATCH 3/4] feat: improved canvas and fix lint format --- frontend/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/package.json b/frontend/package.json index 6b85ccc2..16758294 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,6 +6,7 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "d3": "^7.9.0", "get-starknet": "^3.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", From cbd369b074b411b6ed089e24052e4347e02597f2 Mon Sep 17 00:00:00 2001 From: Adeyemi Date: Thu, 18 Apr 2024 20:00:18 +0100 Subject: [PATCH 4/4] feat: added min and max scale variable --- frontend/src/canvas/Canvas.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/src/canvas/Canvas.js b/frontend/src/canvas/Canvas.js index e58baadc..1e0f88eb 100644 --- a/frontend/src/canvas/Canvas.js +++ b/frontend/src/canvas/Canvas.js @@ -11,6 +11,11 @@ const Canvas = props => { //TODO: Pressing "Canvas" resets the view / positioning //TODO: Way to configure tick rates to give smooth xp for all users + //Todo: Make this dynamic + const minScale = 1; + const maxScale = 40; + + const canvasRef = useRef(null) const canvasPositionRef = useRef(null) const canvasScaleRef = useRef(null) @@ -32,7 +37,7 @@ const Canvas = props => { // TODO: Weird positioning behavior when clicking into devtools useEffect(() => { const canvas = select(canvasPositionRef.current) - const Dzoom = zoom().on("zoom", zoomHandler) + const Dzoom = zoom().scaleExtent([minScale, maxScale]).on("zoom", zoomHandler) // Set default zoom level and center the canvas canvas