diff --git a/backend/routes/nft.go b/backend/routes/nft.go index 1c9b90ee..de6f7762 100644 --- a/backend/routes/nft.go +++ b/backend/routes/nft.go @@ -1,7 +1,6 @@ package routes import ( - "context" "encoding/json" "fmt" "io" @@ -244,94 +243,21 @@ func getNftPixelData(w http.ResponseWriter, r *http.Request) { return } - // Get colors from postgres for consistent palette - colors, err := core.PostgresQuery[string]("SELECT hex FROM colors ORDER BY key") - if err != nil { - routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to get color palette") - 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) + filename := fmt.Sprintf("nfts/round-%s/images/nft-%s.png", roundNumber, tokenId) fileBytes, err := os.ReadFile(filename) if err != nil { - filename = fmt.Sprintf("nfts/round-%s/images/nft-%s.png", roundNumber, tokenId) - fileBytes, err = os.ReadFile(filename) - if err != nil { - // If file not found, fallback to canvas data - ctx := context.Background() - canvas, err := core.ArtPeaceBackend.Databases.Redis.Get(ctx, "canvas").Result() - if err != nil { - routeutils.WriteErrorJson(w, http.StatusNotFound, "NFT image not found and canvas data unavailable") - return - } - - // Create response using NFT dimensions - response := struct { - Width int `json:"width"` - Height int `json:"height"` - PixelData []int `json:"pixelData"` - }{ - Width: nftData.Width, - Height: nftData.Height, - PixelData: make([]int, nftData.Width*nftData.Height), - } - - bitWidth := core.ArtPeaceBackend.CanvasConfig.ColorsBitWidth - canvasWidth := int(core.ArtPeaceBackend.CanvasConfig.Canvas.Width) - - // Extract pixel data from canvas - for y := 0; y < nftData.Height; y++ { - for x := 0; x < nftData.Width; x++ { - pos := nftData.Position + x + (y * canvasWidth) - bitPos := uint(pos) * bitWidth - bytePos := int(bitPos / 8) - bitOffset := uint(bitPos % 8) - - // Handle out of bounds as transparent (0xFF) - if bytePos >= len(canvas) { - response.PixelData[x+y*nftData.Width] = 0xFF - continue - } - - var colorIdx int - if bitOffset <= 3 { - colorIdx = int((canvas[bytePos] >> (8 - bitWidth - bitOffset)) & ((1 << bitWidth) - 1)) - } else { - if bytePos+1 >= len(canvas) { - response.PixelData[x+y*nftData.Width] = 0xFF - continue - } - colorIdx = int(((uint16(canvas[bytePos])<<8)|uint16(canvas[bytePos+1]))>>(16-bitWidth-bitOffset)) & ((1 << bitWidth) - 1) - } - - // Validate color index - if colorIdx >= len(colors) { - response.PixelData[x+y*nftData.Width] = 0xFF - } else { - response.PixelData[x+y*nftData.Width] = colorIdx - } - } - } - - jsonResponse, err := json.Marshal(response) - if err != nil { - routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to create response") - return - } - - routeutils.WriteDataJson(w, string(jsonResponse)) - return - } + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to read image file") + return } // If we have the file, process it using imageToPixelData - pixelData, err := imageToPixelData(fileBytes) + pixelData, err := imageToPixelData(fileBytes, 10) if err != nil { routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to process image") return diff --git a/backend/routes/templates.go b/backend/routes/templates.go index 365c96a6..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 @@ -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 @@ -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/src/configs/backend.config.json b/frontend/src/configs/backend.config.json index c56f3c4d..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": 8081, + "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/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/onchain/src/art_peace.cairo b/onchain/src/art_peace.cairo index 7bae6e4b..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);