diff --git a/backend/routes/nft.go b/backend/routes/nft.go index e27aaefa..de6f7762 100644 --- a/backend/routes/nft.go +++ b/backend/routes/nft.go @@ -1,6 +1,8 @@ package routes import ( + "encoding/json" + "fmt" "io" "net/http" "os" @@ -19,6 +21,7 @@ func InitNFTRoutes() { http.HandleFunc("/get-new-nfts", getNewNFTs) http.HandleFunc("/get-my-nfts", getMyNFTs) http.HandleFunc("/get-nft-likes", getNftLikeCount) + http.HandleFunc("/get-nft-pixel-data", getNftPixelData) // http.HandleFunc("/like-nft", LikeNFT) // http.HandleFunc("/unlike-nft", UnLikeNFT) http.HandleFunc("/get-top-nfts", getTopNFTs) @@ -220,6 +223,65 @@ func getNewNFTs(w http.ResponseWriter, r *http.Request) { routeutils.WriteDataJson(w, string(nfts)) } +func getNftPixelData(w http.ResponseWriter, r *http.Request) { + tokenId := r.URL.Query().Get("tokenId") + if tokenId == "" { + routeutils.WriteErrorJson(w, http.StatusBadRequest, "TokenId parameter is required") + return + } + + // First get the NFT data to access the imageHash + nft, err := core.PostgresQueryOneJson[NFTData]("SELECT * FROM nfts WHERE token_id = $1", tokenId) + if err != nil { + routeutils.WriteErrorJson(w, http.StatusNotFound, "NFT not found") + return + } + + var nftData NFTData + if err := json.Unmarshal([]byte(nft), &nftData); err != nil { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to parse NFT data") + return + } + + // Try to read from file first + roundNumber := os.Getenv("ROUND_NUMBER") + if roundNumber == "" { + roundNumber = "1" // Default to round 1 if not set + } + + filename := fmt.Sprintf("nfts/round-%s/images/nft-%s.png", roundNumber, tokenId) + fileBytes, err := os.ReadFile(filename) + if err != nil { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to read image file") + return + } + + // If we have the file, process it using imageToPixelData + pixelData, err := imageToPixelData(fileBytes, 10) + if err != nil { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to process image") + return + } + + response := struct { + Width int `json:"width"` + Height int `json:"height"` + PixelData []int `json:"pixelData"` + }{ + Width: nftData.Width, + Height: nftData.Height, + PixelData: pixelData, + } + + jsonResponse, err := json.Marshal(response) + if err != nil { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to create response") + return + } + + routeutils.WriteDataJson(w, string(jsonResponse)) +} + func mintNFTDevnet(w http.ResponseWriter, r *http.Request) { // Disable this in production if routeutils.NonProductionMiddleware(w, r) { diff --git a/backend/routes/templates.go b/backend/routes/templates.go index a07aa3c3..d66f3a07 100644 --- a/backend/routes/templates.go +++ b/backend/routes/templates.go @@ -76,7 +76,7 @@ func hexToRGBA(colorBytes string) color.RGBA { return color.RGBA{uint8(r), uint8(g), uint8(b), 255} } -func imageToPixelData(imageData []byte) ([]int, error) { +func imageToPixelData(imageData []byte, scaleFactor int) ([]int, error) { img, _, err := image.Decode(bytes.NewReader(imageData)) if err != nil { return nil, err @@ -96,16 +96,20 @@ func imageToPixelData(imageData []byte) ([]int, error) { bounds := img.Bounds() width, height := bounds.Max.X, bounds.Max.Y - pixelData := make([]int, width*height) - - for y := 0; y < height; y++ { - for x := 0; x < width; x++ { + scaledWidth := width / scaleFactor + scaledHeight := height / scaleFactor + pixelData := make([]int, scaledWidth*scaledHeight) + + for y := 0; y < height; y += scaleFactor { + for x := 0; x < width; x += scaleFactor { + newX := x / scaleFactor + newY := y / scaleFactor rgba := color.RGBAModel.Convert(img.At(x, y)).(color.RGBA) if rgba.A < 128 { // Consider pixels with less than 50% opacity as transparent - pixelData[y*width+x] = 0xFF + pixelData[newY*scaledWidth+newX] = 0xFF } else { closestIndex := findClosestColor(rgba, palette) - pixelData[y*width+x] = closestIndex + pixelData[newY*scaledWidth+newX] = closestIndex } } } @@ -229,7 +233,7 @@ func buildTemplateImg(w http.ResponseWriter, r *http.Request) { return } - imageData, err := imageToPixelData(fileBytes) + imageData, err := imageToPixelData(fileBytes, 1) if err != nil { routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to convert image to pixel data") return @@ -298,7 +302,7 @@ func addTemplateImg(w http.ResponseWriter, r *http.Request) { } bounds := img.Bounds() width, height := bounds.Max.X-bounds.Min.X, bounds.Max.Y-bounds.Min.Y - if width < 5 || width > 64 || height < 5 || height > 64 { + if width < 5 || width > 256 || height < 5 || height > 256 { routeutils.WriteErrorJson(w, http.StatusBadRequest, "Invalid image dimensions") return } @@ -314,7 +318,7 @@ func addTemplateImg(w http.ResponseWriter, r *http.Request) { r.Body.Close() - imageData, err := imageToPixelData(fileBytes) + imageData, err := imageToPixelData(fileBytes, 1) if err != nil { routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to convert image to pixel data") return @@ -372,7 +376,7 @@ func addTemplateData(w http.ResponseWriter, r *http.Request) { return } - if width < 5 || width > 64 || height < 5 || height > 64 { + if width < 5 || width > 256 || height < 5 || height > 256 { routeutils.WriteErrorJson(w, http.StatusBadRequest, "Invalid image dimensions") return } @@ -473,7 +477,7 @@ func getTemplatePixelData(w http.ResponseWriter, r *http.Request) { } // Convert image to pixel data using existing function - pixelData, err := imageToPixelData(fileBytes) + pixelData, err := imageToPixelData(fileBytes, 1) if err != nil { routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to process image") return diff --git a/docker-compose.yml b/docker-compose.yml index 55e12a46..b9bb3ca3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,6 +36,8 @@ services: - ART_PEACE_END_TIME=3000000000 - ART_PEACE_HOST=0328ced46664355fc4b885ae7011af202313056a7e3d44827fb24c9d3206aaa0 - ROUND_NUMBER=1 + volumes: + - nfts:/app/nfts consumer: build: dockerfile: backend/Dockerfile.consumer diff --git a/frontend/public/index.html b/frontend/public/index.html index 7022acd6..ed2e976b 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -1,4 +1,4 @@ - + diff --git a/frontend/src/App.js b/frontend/src/App.js index 44bb6aac..1f67a758 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1009,18 +1009,49 @@ function App() { return []; }; - // Only call getTemplatePixelData if overlayTemplate exists and has a hash - if (overlayTemplate && overlayTemplate.hash) { - // Need to handle the Promise properly - getTemplatePixelData(overlayTemplate.hash) - .then((data) => setTemplatePixels(data)) - .catch((error) => { - console.error('Error fetching template pixels:', error); + const getNftPixelData = async (tokenId) => { + if (tokenId !== null) { + const response = await fetchWrapper( + `get-nft-pixel-data?tokenId=${tokenId}` + ); + if (!response.data) { + console.error('NFT pixel data not found'); + return []; + } + return response.data; + } + return []; + }; + + const fetchPixelData = async () => { + try { + if (!overlayTemplate) { setTemplatePixels([]); - }); - } else { - setTemplatePixels([]); - } + return; + } + + // Handle NFT overlay case + if (overlayTemplate.isNft && overlayTemplate.tokenId !== undefined) { + const data = await getNftPixelData(overlayTemplate.tokenId); + setTemplatePixels(data); + return; + } + + // Handle template overlay case + if (overlayTemplate.hash) { + const data = await getTemplatePixelData(overlayTemplate.hash); + setTemplatePixels(data); + return; + } + + setTemplatePixels([]); + } catch (error) { + console.error('Error fetching pixel data:', error); + setTemplatePixels([]); + } + }; + + fetchPixelData(); }, [overlayTemplate]); return ( diff --git a/frontend/src/canvas/NFTSelector.js b/frontend/src/canvas/NFTSelector.js index 646b3cba..e3974cbd 100644 --- a/frontend/src/canvas/NFTSelector.js +++ b/frontend/src/canvas/NFTSelector.js @@ -76,14 +76,14 @@ const NFTSelector = (props) => { let width = endX - startX; let height = endY - startY; // Max NFT sizes - if (width > 64) { - width = 64; + if (width > 256) { + width = 256; if (x < initX) { startX = endX - width; } } - if (height > 64) { - height = 64; + if (height > 256) { + height = 256; if (y < initY) { startY = endY - height; } @@ -190,14 +190,14 @@ const NFTSelector = (props) => { let width = endX - startX; let height = endY - startY; // Max NFT sizes - if (width > 64) { - width = 64; + if (width > 256) { + width = 256; if (x < initX) { startX = endX - width; } } - if (height > 64) { - height = 64; + if (height > 256) { + height = 256; if (y < initY) { startY = endY - height; } diff --git a/frontend/src/configs/backend.config.json b/frontend/src/configs/backend.config.json index e9f57c37..9636245b 100644 --- a/frontend/src/configs/backend.config.json +++ b/frontend/src/configs/backend.config.json @@ -1,6 +1,7 @@ { "host": "api.art-peace.net", "port": 8080, + "consumer_port": 8081, "scripts": { "place_pixel_devnet": "../tests/integration/local/place_pixel.sh", "place_extra_pixels_devnet": "../tests/integration/local/place_extra_pixels.sh", diff --git a/frontend/src/index.css b/frontend/src/index.css index f08406a5..7a3e9cc3 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,6 +1,7 @@ @font-face { font-family: 'Public-Pixel'; - src: local('Public-Pixel'), + src: + local('Public-Pixel'), url('./fonts/PublicPixel-z84yD.ttf') format('truetype'); } diff --git a/frontend/src/tabs/TabPanel.js b/frontend/src/tabs/TabPanel.js index e602218d..9f084dd2 100644 --- a/frontend/src/tabs/TabPanel.js +++ b/frontend/src/tabs/TabPanel.js @@ -246,6 +246,8 @@ const TabPanel = (props) => { setTemplateImage={props.setTemplateImage} setTemplateColorIds={props.setTemplateColorIds} setActiveTab={props.setActiveTab} + setTemplateOverlayMode={props.setTemplateOverlayMode} + setOverlayTemplate={props.setOverlayTemplate} /> )} @@ -265,6 +267,8 @@ const TabPanel = (props) => { queryAddress={props.queryAddress} isMobile={props.isMobile} gameEnded={props.gameEnded} + setTemplateOverlayMode={props.setTemplateOverlayMode} + setOverlayTemplate={props.setOverlayTemplate} /> )} diff --git a/frontend/src/tabs/factions/FactionItem.js b/frontend/src/tabs/factions/FactionItem.js index 30a1c79a..d105ec05 100644 --- a/frontend/src/tabs/factions/FactionItem.js +++ b/frontend/src/tabs/factions/FactionItem.js @@ -238,9 +238,9 @@ const FactionItem = (props) => { ); return; } - if (height > 64 || width > 64) { + if (height > 256 || width > 256) { alert( - 'Image is too large, maximum size is 64x64. Given size is ' + + 'Image is too large, maximum size is 256x256. Given size is ' + width + 'x' + height diff --git a/frontend/src/tabs/nfts/NFTItem.css b/frontend/src/tabs/nfts/NFTItem.css index b37446ac..65a5db72 100644 --- a/frontend/src/tabs/nfts/NFTItem.css +++ b/frontend/src/tabs/nfts/NFTItem.css @@ -29,6 +29,32 @@ margin: 0; padding: 0; image-rendering: pixelated; + cursor: pointer; + + transition: all 0.2s; +} + +/* pulse animation */ +@keyframes pulse { + 0% { + transform: scale(1); + } + 50% { + transform: scale(1.05); + } + 100% { + transform: scale(1); + } +} + +.NFTItem__image:hover { + animation: pulse 1s infinite; + transform: scale(1.03); + box-shadow: 0 0 10px rgba(0, 0, 0, 0.6); +} + +.NFTItem__image:active { + transform: scale(1); } .NFTItem__buttons { diff --git a/frontend/src/tabs/nfts/NFTItem.js b/frontend/src/tabs/nfts/NFTItem.js index 712a01e0..aefca25a 100644 --- a/frontend/src/tabs/nfts/NFTItem.js +++ b/frontend/src/tabs/nfts/NFTItem.js @@ -145,6 +145,27 @@ const NFTItem = (props) => { }, [props.minter]); const [showInfo, setShowInfo] = React.useState(false); + const handleNftClick = (e) => { + if ( + e.target.classList.contains('NFTItem__button') || + e.target.classList.contains('Like__icon') || + e.target.classList.contains('Share__icon') + ) { + return; + } + // Format NFT data to match template structure + const nftTemplate = { + position: props.position, + width: props.width, + height: props.height, + image: props.image, + isNft: true, + tokenId: props.tokenId + }; + props.setTemplateOverlayMode(true); + props.setOverlayTemplate(nftTemplate); + props.setActiveTab('Canvas'); + }; return (
@@ -153,6 +174,7 @@ const NFTItem = (props) => { src={props.image} alt={`nft-image-${props.tokenId}`} className='NFTItem__image' + onClick={handleNftClick} />

{props.name}

diff --git a/frontend/src/tabs/nfts/NFTs.js b/frontend/src/tabs/nfts/NFTs.js index 40c617f1..aebcc33a 100644 --- a/frontend/src/tabs/nfts/NFTs.js +++ b/frontend/src/tabs/nfts/NFTs.js @@ -77,6 +77,9 @@ const NFTsMainSection = (props) => { minter={nft.minter} queryAddress={props.queryAddress} updateLikes={props.updateLikes} + setTemplateOverlayMode={props.setTemplateOverlayMode} + setOverlayTemplate={props.setOverlayTemplate} + setActiveTab={props.setActiveTab} /> ); })} @@ -140,6 +143,9 @@ const NFTsExpandedSection = (props) => { minter={nft.minter} queryAddress={props.queryAddress} updateLikes={props.updateLikes} + setTemplateOverlayMode={props.setTemplateOverlayMode} + setOverlayTemplate={props.setOverlayTemplate} + setActiveTab={props.setActiveTab} /> ); })} @@ -334,6 +340,8 @@ const NFTs = (props) => { filters={filters} isMobile={props.isMobile} gameEnded={props.gameEnded} + setTemplateOverlayMode={props.setTemplateOverlayMode} + setOverlayTemplate={props.setOverlayTemplate} /> ); }; diff --git a/frontend/src/tabs/templates/Templates.js b/frontend/src/tabs/templates/Templates.js index 6708784b..e00bdf5d 100644 --- a/frontend/src/tabs/templates/Templates.js +++ b/frontend/src/tabs/templates/Templates.js @@ -91,9 +91,9 @@ const TemplatesMainSection = (props) => { ); return; } - if (height > 64 || width > 64) { + if (height > 256 || width > 256) { alert( - 'Image is too large, maximum size is 64x64. Given size is ' + + 'Image is too large, maximum size is 256x256. Given size is ' + width + 'x' + height @@ -460,6 +460,8 @@ const Templates = (props) => { setTemplateColorIds={props.setTemplateColorIds} availableTemplates={availableTemplates} setActiveTab={props.setActiveTab} + setTemplateOverlayMode={props.setTemplateOverlayMode} + setOverlayTemplate={props.setOverlayTemplate} /> ); }; diff --git a/onchain/src/art_peace.cairo b/onchain/src/art_peace.cairo index b179d06a..b8afced2 100644 --- a/onchain/src/art_peace.cairo +++ b/onchain/src/art_peace.cairo @@ -381,7 +381,7 @@ pub mod ArtPeace { let test_address = starknet::contract_address_const::< 0x328ced46664355fc4b885ae7011af202313056a7e3d44827fb24c9d3206aaa0 >(); - self.extra_pixels.write(test_address, 1000); + self.extra_pixels.write(test_address, 100000); } self.devmode.write(init_params.devmode); @@ -926,7 +926,7 @@ pub mod ArtPeace { template_metadata.position < self.canvas_width.read() * self.canvas_height.read(), 'Template position out of bounds' ); - let MAX_TEMPLATE_SIZE: u128 = 64; + let MAX_TEMPLATE_SIZE: u128 = 256; let MIN_TEMPLATE_SIZE: u128 = 5; assert( template_metadata.width >= MIN_TEMPLATE_SIZE @@ -971,7 +971,7 @@ pub mod ArtPeace { template_metadata.position < self.canvas_width.read() * self.canvas_height.read(), 'Template position out of bounds' ); - let MAX_TEMPLATE_SIZE: u128 = 64; + let MAX_TEMPLATE_SIZE: u128 = 256; let MIN_TEMPLATE_SIZE: u128 = 5; assert( template_metadata.width >= MIN_TEMPLATE_SIZE @@ -1091,7 +1091,7 @@ pub mod ArtPeace { self.check_game_running(); // TODO: To config? let MIN_NFT_SIZE: u128 = 1; - let MAX_NFT_SIZE: u128 = 64; + let MAX_NFT_SIZE: u128 = 256; assert( mint_params.width >= MIN_NFT_SIZE && mint_params.width <= MAX_NFT_SIZE, 'NFT width out of bounds'