From 105759257e340f2b5071fa464c20d6e35c699923 Mon Sep 17 00:00:00 2001
From: Brandon R <54774639+b-j-roberts@users.noreply.github.com>
Date: Mon, 22 Apr 2024 16:13:19 -0500
Subject: [PATCH] feat: NFT and Templates frontend + backend interactions (#67)
* NFTs frontend and devnet interaction, templates upload on frontend and devnet interaction, various patches, ...
* Formatting
* Fix tests
* Go fmt
* Fix local integration
---
backend/config/backend.go | 12 +-
backend/routes/indexer.go | 281 ++++++++++++++-
backend/routes/nft.go | 197 +++++++++++
backend/routes/pixel.go | 45 +++
backend/routes/routes.go | 1 +
backend/routes/templates.go | 180 +++++++++-
configs/backend.config.json | 4 +-
configs/docker-backend.config.json | 4 +-
docker-compose.yml | 2 +
frontend/src/App.js | 77 ++++-
frontend/src/canvas/Canvas.css | 27 ++
frontend/src/canvas/Canvas.js | 323 ++++++++++++++----
frontend/src/canvas/ExtraPixelsPanel.css | 123 +++++++
frontend/src/canvas/ExtraPixelsPanel.js | 57 ++++
frontend/src/canvas/PixelSelector.js | 5 +-
frontend/src/canvas/TemplateBuilderPanel.css | 103 ++++++
frontend/src/canvas/TemplateBuilderPanel.js | 87 +++++
frontend/src/configs/backend.config.json | 4 +-
frontend/src/tabs/ExpandableTab.js | 5 +-
frontend/src/tabs/NFTs.css | 50 ---
frontend/src/tabs/NFTs.js | 59 ----
frontend/src/tabs/TabPanel.js | 12 +-
frontend/src/tabs/Templates.css | 50 ---
frontend/src/tabs/Templates.js | 57 ----
frontend/src/tabs/Voting.js | 2 +-
frontend/src/tabs/nfts/CollectionItem.css | 56 +++
frontend/src/tabs/nfts/CollectionItem.js | 33 ++
frontend/src/tabs/nfts/NFTItem.css | 84 +++++
frontend/src/tabs/nfts/NFTItem.js | 42 +++
frontend/src/tabs/nfts/NFTs.css | 80 +++++
frontend/src/tabs/nfts/NFTs.js | 89 +++++
frontend/src/tabs/quests/Quests.js | 4 +-
frontend/src/tabs/templates/EventItem.css | 107 ++++++
frontend/src/tabs/templates/EventItem.js | 84 +++++
frontend/src/tabs/templates/TemplateItem.css | 118 +++++++
frontend/src/tabs/templates/TemplateItem.js | 54 +++
frontend/src/tabs/templates/Templates.css | 88 +++++
frontend/src/tabs/templates/Templates.js | 217 ++++++++++++
indexer/script.js | 18 +-
onchain/Scarb.toml | 2 +
onchain/src/art_peace.cairo | 47 ++-
onchain/src/nfts/canvas_nft.cairo | 16 +-
onchain/src/nfts/component.cairo | 6 +-
onchain/src/nfts/interfaces.cairo | 6 +-
onchain/src/templates/interfaces.cairo | 1 +
onchain/src/tests/art_peace.cairo | 13 +-
postgres/init.sql | 15 +-
tests/integration/docker/add_template.sh | 36 ++
tests/integration/docker/deploy.sh | 50 ++-
tests/integration/docker/initialize.sh | 2 +-
tests/integration/docker/mint_nft.sh | 36 ++
.../integration/docker/place_extra_pixels.sh | 36 ++
tests/integration/local/add_template.sh | 36 ++
tests/integration/local/deploy.sh | 52 ++-
tests/integration/local/mint_nft.sh | 36 ++
tests/integration/local/place_extra_pixels.sh | 35 ++
tests/integration/local/run.sh | 5 +-
57 files changed, 2888 insertions(+), 383 deletions(-)
create mode 100644 backend/routes/nft.go
create mode 100644 frontend/src/canvas/ExtraPixelsPanel.css
create mode 100644 frontend/src/canvas/ExtraPixelsPanel.js
create mode 100644 frontend/src/canvas/TemplateBuilderPanel.css
create mode 100644 frontend/src/canvas/TemplateBuilderPanel.js
delete mode 100644 frontend/src/tabs/NFTs.css
delete mode 100644 frontend/src/tabs/NFTs.js
delete mode 100644 frontend/src/tabs/Templates.css
delete mode 100644 frontend/src/tabs/Templates.js
create mode 100644 frontend/src/tabs/nfts/CollectionItem.css
create mode 100644 frontend/src/tabs/nfts/CollectionItem.js
create mode 100644 frontend/src/tabs/nfts/NFTItem.css
create mode 100644 frontend/src/tabs/nfts/NFTItem.js
create mode 100644 frontend/src/tabs/nfts/NFTs.css
create mode 100644 frontend/src/tabs/nfts/NFTs.js
create mode 100644 frontend/src/tabs/templates/EventItem.css
create mode 100644 frontend/src/tabs/templates/EventItem.js
create mode 100644 frontend/src/tabs/templates/TemplateItem.css
create mode 100644 frontend/src/tabs/templates/TemplateItem.js
create mode 100644 frontend/src/tabs/templates/Templates.css
create mode 100644 frontend/src/tabs/templates/Templates.js
create mode 100755 tests/integration/docker/add_template.sh
create mode 100755 tests/integration/docker/mint_nft.sh
create mode 100755 tests/integration/docker/place_extra_pixels.sh
create mode 100755 tests/integration/local/add_template.sh
create mode 100755 tests/integration/local/mint_nft.sh
create mode 100755 tests/integration/local/place_extra_pixels.sh
diff --git a/backend/config/backend.go b/backend/config/backend.go
index 6c4b8b78..61921d11 100644
--- a/backend/config/backend.go
+++ b/backend/config/backend.go
@@ -6,8 +6,10 @@ import (
)
type BackendScriptsConfig struct {
- PlacePixelDevnet string `json:"place_pixel_devnet"`
- AddTemplateHashDevnet string `json:"add_template_hash_devnet"`
+ PlacePixelDevnet string `json:"place_pixel_devnet"`
+ PlaceExtraPixelsDevnet string `json:"place_extra_pixels_devnet"`
+ AddTemplateDevnet string `json:"add_template_devnet"`
+ MintNFTDevnet string `json:"mint_nft_devnet"`
}
type BackendConfig struct {
@@ -21,8 +23,10 @@ var DefaultBackendConfig = BackendConfig{
Host: "localhost",
Port: 8080,
Scripts: BackendScriptsConfig{
- PlacePixelDevnet: "../scripts/place_pixel.sh",
- AddTemplateHashDevnet: "../scripts/add_template_hash.sh",
+ PlacePixelDevnet: "../scripts/place_pixel.sh",
+ PlaceExtraPixelsDevnet: "../scripts/place_extra_pixels.sh",
+ AddTemplateDevnet: "../scripts/add_template.sh",
+ MintNFTDevnet: "../scripts/mint_nft.sh",
},
Production: false,
}
diff --git a/backend/routes/indexer.go b/backend/routes/indexer.go
index 5012075a..77ef66f1 100644
--- a/backend/routes/indexer.go
+++ b/backend/routes/indexer.go
@@ -2,10 +2,15 @@ package routes
import (
"context"
+ "encoding/hex"
"encoding/json"
"fmt"
+ "image"
+ "image/color"
+ "image/png"
"io"
"net/http"
+ "os"
"strconv"
"github.com/gorilla/websocket"
@@ -55,6 +60,12 @@ func InitIndexerRoutes() {
}
*/
+const (
+ pixelPlacedEvent = "0x02d7b50ebf415606d77c7e7842546fc13f8acfbfd16f7bcf2bc2d08f54114c23"
+ nftMintedEvent = "0x030826e0cd9a517f76e857e3f3100fe5b9098e9f8216d3db283fb4c9a641232f"
+ templateAddedEvent = "0x03e18ec266fe76a2efce73f91228e6e04456b744fc6984c7a6374e417fb4bf59"
+)
+
// TODO: User might miss some messages between loading canvas and connecting to websocket?
// TODO: Check thread safety of these things
func consumeIndexerMsg(w http.ResponseWriter, r *http.Request) {
@@ -74,11 +85,28 @@ func consumeIndexerMsg(w http.ResponseWriter, r *http.Request) {
return
}
- address := reqBody["data"].(map[string]interface{})["batch"].([]interface{})[0].(map[string]interface{})["events"].([]interface{})[0].(map[string]interface{})["event"].(map[string]interface{})["keys"].([]interface{})[1]
+ events := reqBody["data"].(map[string]interface{})["batch"].([]interface{})[0].(map[string]interface{})["events"].([]interface{})
+
+ for _, event := range events {
+ eventKey := event.(map[string]interface{})["event"].(map[string]interface{})["keys"].([]interface{})[0].(string)
+ if eventKey == pixelPlacedEvent {
+ processPixelPlacedEvent(event.(map[string]interface{}), w)
+ } else if eventKey == nftMintedEvent {
+ processNFTMintedEvent(event.(map[string]interface{}), w)
+ } else if eventKey == templateAddedEvent {
+ processTemplateAddedEvent(event.(map[string]interface{}), w)
+ } else {
+ fmt.Println("Unknown event key: ", eventKey)
+ }
+ }
+}
+
+func processPixelPlacedEvent(event map[string]interface{}, w http.ResponseWriter) {
+ address := event["event"].(map[string]interface{})["keys"].([]interface{})[1]
address = address.(string)[2:]
- posHex := reqBody["data"].(map[string]interface{})["batch"].([]interface{})[0].(map[string]interface{})["events"].([]interface{})[0].(map[string]interface{})["event"].(map[string]interface{})["keys"].([]interface{})[2]
- dayIdxHex := reqBody["data"].(map[string]interface{})["batch"].([]interface{})[0].(map[string]interface{})["events"].([]interface{})[0].(map[string]interface{})["event"].(map[string]interface{})["keys"].([]interface{})[3]
- colorHex := reqBody["data"].(map[string]interface{})["batch"].([]interface{})[0].(map[string]interface{})["events"].([]interface{})[0].(map[string]interface{})["event"].(map[string]interface{})["data"].([]interface{})[0]
+ posHex := event["event"].(map[string]interface{})["keys"].([]interface{})[2]
+ dayIdxHex := event["event"].(map[string]interface{})["keys"].([]interface{})[3]
+ colorHex := event["event"].(map[string]interface{})["data"].([]interface{})[0]
// Convert hex to int
position, err := strconv.ParseInt(posHex.(string), 0, 64)
@@ -141,3 +169,248 @@ func consumeIndexerMsg(w http.ResponseWriter, r *http.Request) {
}
}
}
+
+/*
+indexer-1 | [
+indexer-1 | [Object: null prototype] {
+indexer-1 | event: [Object: null prototype] {
+indexer-1 | fromAddress: "0x07163dbc0d5dc7e65c8fb9697dbd778e70fa9fef66f12a018a69751eb53fec5a",
+indexer-1 | keys: [
+indexer-1 | "0x030826e0cd9a517f76e857e3f3100fe5b9098e9f8216d3db283fb4c9a641232f",
+indexer-1 | "0x0000000000000000000000000000000000000000000000000000000000000000",
+indexer-1 | "0x0000000000000000000000000000000000000000000000000000000000000000"
+indexer-1 | ],
+indexer-1 | data: [
+indexer-1 | "0x0000000000000000000000000000000000000000000000000000000000000091",
+indexer-1 | "0x000000000000000000000000000000000000000000000000000000000000000e",
+indexer-1 | "0x000000000000000000000000000000000000000000000000000000000000000d",
+indexer-1 | "0x0000000000000000000000000000000000000000000000000000000000000000",
+indexer-1 | "0x0000000000000000000000000000000000000000000000000000000000000006",
+indexer-1 | "0x0328ced46664355fc4b885ae7011af202313056a7e3d44827fb24c9d3206aaa0"
+indexer-1 | ],
+indexer-1 | index: "1"
+indexer-1 | }
+indexer-1 | }
+indexer-1 | ]
+*/
+
+func processNFTMintedEvent(event map[string]interface{}, w http.ResponseWriter) {
+ // TODO: combine high and low token ids
+ tokenIdLowHex := event["event"].(map[string]interface{})["keys"].([]interface{})[1]
+ tokenIdHighHex := event["event"].(map[string]interface{})["keys"].([]interface{})[2]
+
+ positionHex := event["event"].(map[string]interface{})["data"].([]interface{})[0]
+ widthHex := event["event"].(map[string]interface{})["data"].([]interface{})[1]
+ heightHex := event["event"].(map[string]interface{})["data"].([]interface{})[2]
+ imageHashHex := event["event"].(map[string]interface{})["data"].([]interface{})[3]
+ blockNumberHex := event["event"].(map[string]interface{})["data"].([]interface{})[4]
+ minterHex := event["event"].(map[string]interface{})["data"].([]interface{})[5]
+
+ fmt.Println("NFT minted with token id low: ", tokenIdLowHex, " and token id high: ", tokenIdHighHex)
+
+ tokenId, err := strconv.ParseInt(tokenIdLowHex.(string), 0, 64)
+ if err != nil {
+ fmt.Println("Error converting token id low hex to int: ", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ position, err := strconv.ParseInt(positionHex.(string), 0, 64)
+ if err != nil {
+ fmt.Println("Error converting position hex to int: ", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ width, err := strconv.ParseInt(widthHex.(string), 0, 64)
+ if err != nil {
+ fmt.Println("Error converting width hex to int: ", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ height, err := strconv.ParseInt(heightHex.(string), 0, 64)
+ if err != nil {
+ fmt.Println("Error converting height hex to int: ", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ blockNumber, err := strconv.ParseInt(blockNumberHex.(string), 0, 64)
+ if err != nil {
+ fmt.Println("Error converting block number hex to int: ", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ minter := minterHex.(string)[2:]
+
+ fmt.Println("NFT minted with position: ", position, " width: ", width, " height: ", height, " image hash: ", imageHashHex, " block number: ", blockNumber, " minter: ", minter, "tokenId", tokenId)
+ // Set NFT in postgres
+ _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "INSERT INTO NFTs (key, position, width, height, imageHash, blockNumber, minter) VALUES ($1, $2, $3, $4, $5, $6, $7)", tokenId, position, width, height, imageHashHex, blockNumber, minter)
+ if err != nil {
+ fmt.Println("Error inserting NFT into postgres: ", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ // TODO: get image from canvas through starknet rpc? What to do about pending transactions?
+
+ // Load image from redis
+ ctx := context.Background()
+ // TODO: Better way to get image
+ canvas, err := core.ArtPeaceBackend.Databases.Redis.Get(ctx, "canvas").Result()
+ if err != nil {
+ panic(err)
+ }
+
+ colorPaletteHex := core.ArtPeaceBackend.CanvasConfig.Colors
+ colorPalette := make([]color.RGBA, len(colorPaletteHex))
+ for idx, colorHex := range colorPaletteHex {
+ r, err := strconv.ParseInt(colorHex[0:2], 16, 64)
+ if err != nil {
+ fmt.Println("Error converting red hex to int: ", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ g, err := strconv.ParseInt(colorHex[2:4], 16, 64)
+ if err != nil {
+ fmt.Println("Error converting green hex to int: ", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ b, err := strconv.ParseInt(colorHex[4:6], 16, 64)
+ if err != nil {
+ fmt.Println("Error converting blue hex to int: ", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ colorPalette[idx] = color.RGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: 255}
+ }
+ bitWidth := int64(core.ArtPeaceBackend.CanvasConfig.ColorsBitWidth)
+ startX := int64(position % int64(core.ArtPeaceBackend.CanvasConfig.Canvas.Width))
+ startY := int64(position / int64(core.ArtPeaceBackend.CanvasConfig.Canvas.Width))
+ oneByteBitOffset := int64(8 - bitWidth)
+ twoByteBitOffset := int64(16 - bitWidth)
+ generatedImage := image.NewRGBA(image.Rect(0, 0, int(width), int(height)))
+ for y := startY; y < startY+height; y++ {
+ for x := startX; x < startX+width; x++ {
+ pos := y*int64(core.ArtPeaceBackend.CanvasConfig.Canvas.Width) + x
+ bitPos := pos * bitWidth
+ bytePos := bitPos / 8
+ bitOffset := bitPos % 8
+ if bitOffset <= oneByteBitOffset {
+ colorIdx := (canvas[bytePos] >> (oneByteBitOffset - bitOffset)) & 0b11111
+ generatedImage.Set(int(x-startX), int(y-startY), colorPalette[colorIdx])
+ } else {
+ colorIdx := (((uint16(canvas[bytePos]) << 8) | uint16(canvas[bytePos+1])) >> (twoByteBitOffset - bitOffset)) & 0b11111
+ generatedImage.Set(int(x-startX), int(y-startY), colorPalette[colorIdx])
+ }
+ }
+ }
+
+ // TODO: Path to save image
+ // Save image to disk
+ filename := fmt.Sprintf("nft-%d.png", tokenId)
+ file, err := os.Create(filename)
+ if err != nil {
+ fmt.Println("Error creating file: ", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ defer file.Close()
+
+ err = png.Encode(file, generatedImage)
+ if err != nil {
+ fmt.Println("Error encoding image: ", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Write([]byte("NFT minted"))
+}
+
+func processTemplateAddedEvent(event map[string]interface{}, w http.ResponseWriter) {
+ templateIdHex := event["event"].(map[string]interface{})["keys"].([]interface{})[1]
+ templateHashHex := event["event"].(map[string]interface{})["data"].([]interface{})[0]
+ templateNameHex := event["event"].(map[string]interface{})["data"].([]interface{})[1]
+ templatePositionHex := event["event"].(map[string]interface{})["data"].([]interface{})[2]
+ templateWidthHex := event["event"].(map[string]interface{})["data"].([]interface{})[3]
+ templateHeightHex := event["event"].(map[string]interface{})["data"].([]interface{})[4]
+ // TODO: Combine low and high token ids
+ templateRewardHighHex := event["event"].(map[string]interface{})["data"].([]interface{})[5]
+ templateRewardLowHex := event["event"].(map[string]interface{})["data"].([]interface{})[6]
+ templateRewardTokenHex := event["event"].(map[string]interface{})["data"].([]interface{})[7]
+
+ fmt.Println("Template added with template id: ", templateIdHex, " template hash: ", templateHashHex, "reward: ", templateRewardLowHex, templateRewardHighHex, "name:", templateNameHex)
+
+ templateId, err := strconv.ParseInt(templateIdHex.(string), 0, 64)
+ if err != nil {
+ fmt.Println("Error converting template id hex to int: ", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ // Parse template name hex as bytes encoded in utf-8
+ decodedName, err := hex.DecodeString(templateNameHex.(string)[2:])
+ if err != nil {
+ fmt.Println("Error decoding template name hex: ", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ // Trim off 0s at the start
+ trimmedName := []byte{}
+ trimming := true
+ for _, b := range decodedName {
+ if b == 0 && trimming {
+ continue
+ }
+ trimming = false
+ trimmedName = append(trimmedName, b)
+ }
+ templateName := string(trimmedName)
+
+ templatePosition, err := strconv.ParseInt(templatePositionHex.(string), 0, 64)
+ if err != nil {
+ fmt.Println("Error converting template position hex to int: ", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ templateWidth, err := strconv.ParseInt(templateWidthHex.(string), 0, 64)
+ if err != nil {
+ fmt.Println("Error converting template width hex to int: ", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ templateHeight, err := strconv.ParseInt(templateHeightHex.(string), 0, 64)
+ if err != nil {
+ fmt.Println("Error converting template height hex to int: ", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ templateReward, err := strconv.ParseInt(templateRewardLowHex.(string), 0, 64)
+ if err != nil {
+ fmt.Println("Error converting template reward hex to int: ", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ templateRewardToken := templateRewardTokenHex.(string)[2:]
+
+ // Add template to postgres
+ _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "INSERT INTO Templates (key, name, hash, position, width, height, reward, rewardToken) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", templateId, templateName, templateHashHex, templatePosition, templateWidth, templateHeight, templateReward, templateRewardToken)
+ if err != nil {
+ fmt.Println("Error inserting template into postgres: ", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Write([]byte("Template added"))
+}
diff --git a/backend/routes/nft.go b/backend/routes/nft.go
new file mode 100644
index 00000000..d902325e
--- /dev/null
+++ b/backend/routes/nft.go
@@ -0,0 +1,197 @@
+package routes
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "os/exec"
+ "strconv"
+
+ "github.com/keep-starknet-strange/art-peace/backend/core"
+)
+
+func InitNFTRoutes() {
+ http.HandleFunc("/get-nft", getNFT)
+ http.HandleFunc("/get-nfts", getNFTs)
+ http.HandleFunc("/get-my-nfts", getMyNFTs)
+ http.HandleFunc("/mint-nft-devnet", mintNFTDevnet)
+ // Create a static file server for the nft images
+ http.Handle("/nft-images/", http.StripPrefix("/nft-images/", http.FileServer(http.Dir("."))))
+ //http.HandleFunc("/nft-image", nftImage)
+}
+
+type NFTData struct {
+ TokenID int `json:"tokenId"`
+ Position int `json:"position"`
+ Width int `json:"width"`
+ Height int `json:"height"`
+ ImageHash string `json:"imageHash"`
+ BlockNumber int `json:"blockNumber"`
+ Minter string `json:"minter"`
+}
+
+func getNFT(w http.ResponseWriter, r *http.Request) {
+ tokenId := r.URL.Query().Get("tokenId")
+
+ var nftData NFTData
+ rows, err := core.ArtPeaceBackend.Databases.Postgres.Query(context.Background(), "SELECT * FROM nfts WHERE token_id = $1", tokenId)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write([]byte(err.Error()))
+ return
+ }
+ defer rows.Close()
+
+ err = rows.Scan(&nftData.TokenID, &nftData.Position, &nftData.Width, &nftData.Height, &nftData.ImageHash, &nftData.BlockNumber, &nftData.Minter)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write([]byte(err.Error()))
+ return
+ }
+
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ out, err := json.Marshal(nftData)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write([]byte(err.Error()))
+ return
+ }
+ w.Write([]byte(out))
+}
+
+func getMyNFTs(w http.ResponseWriter, r *http.Request) {
+ address := r.URL.Query().Get("address")
+
+ var nftDatas []NFTData
+ rows, err := core.ArtPeaceBackend.Databases.Postgres.Query(context.Background(), "SELECT * FROM nfts WHERE minter = $1", address)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write([]byte(err.Error()))
+ return
+ }
+ defer rows.Close()
+
+ for rows.Next() {
+ var nftData NFTData
+ err = rows.Scan(&nftData.TokenID, &nftData.Position, &nftData.Width, &nftData.Height, &nftData.ImageHash, &nftData.BlockNumber, &nftData.Minter)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write([]byte(err.Error()))
+ return
+ }
+ nftDatas = append(nftDatas, nftData)
+ }
+
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ out, err := json.Marshal(nftDatas)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write([]byte(err.Error()))
+ return
+ }
+ w.Write([]byte(out))
+}
+
+func getNFTs(w http.ResponseWriter, r *http.Request) {
+ // TODO: Pagination & Likes
+ var nftDatas []NFTData
+ rows, err := core.ArtPeaceBackend.Databases.Postgres.Query(context.Background(), "SELECT * FROM nfts")
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write([]byte(err.Error()))
+ return
+ }
+ defer rows.Close()
+
+ for rows.Next() {
+ var nftData NFTData
+ err = rows.Scan(&nftData.TokenID, &nftData.Position, &nftData.Width, &nftData.Height, &nftData.ImageHash, &nftData.BlockNumber, &nftData.Minter)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write([]byte(err.Error()))
+ return
+ }
+ nftDatas = append(nftDatas, nftData)
+ }
+
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ out, err := json.Marshal(nftDatas)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write([]byte(err.Error()))
+ return
+ }
+ w.Write([]byte(out))
+}
+
+// func nftImage(w http.ResponseWriter, r *http.Request) {
+// // Get the png image at location "nft-{tokenId}.png"
+// tokenId := r.URL.Query().Get("tokenId")
+// imageLocation := fmt.Sprintf("nft-%s.png", tokenId)
+//
+// image, err := os.Open(imageLocation)
+// if err != nil {
+// w.WriteHeader(http.StatusInternalServerError)
+// w.Write([]byte(err.Error()))
+// return
+// }
+// defer image.Close()
+//
+// w.Header().Set("Access-Control-Allow-Origin", "*")
+// w.Header().Set("Content-Type", "image/png")
+// w.WriteHeader(http.StatusOK)
+//
+// io.Copy(w, image)
+// }
+
+func mintNFTDevnet(w http.ResponseWriter, r *http.Request) {
+ reqBody, err := io.ReadAll(r.Body)
+ if err != nil {
+ panic(err)
+ }
+ var jsonBody map[string]string
+ err = json.Unmarshal(reqBody, &jsonBody)
+ if err != nil {
+ panic(err)
+ }
+
+ position, err := strconv.Atoi(jsonBody["position"])
+ if err != nil {
+ panic(err)
+ }
+
+ width, err := strconv.Atoi(jsonBody["width"])
+ if err != nil {
+ panic(err)
+ }
+
+ height, err := strconv.Atoi(jsonBody["height"])
+ if err != nil {
+ panic(err)
+ }
+
+ shellCmd := core.ArtPeaceBackend.BackendConfig.Scripts.MintNFTDevnet
+ contract := os.Getenv("ART_PEACE_CONTRACT_ADDRESS")
+
+ cmd := exec.Command(shellCmd, contract, "mint_nft", strconv.Itoa(position), strconv.Itoa(width), strconv.Itoa(height))
+ _, err = cmd.Output()
+ if err != nil {
+ fmt.Println("Error executing shell command: ", err)
+ panic(err)
+ }
+
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Write([]byte("Minted NFT on devnet"))
+}
diff --git a/backend/routes/pixel.go b/backend/routes/pixel.go
index f1279503..5d995014 100644
--- a/backend/routes/pixel.go
+++ b/backend/routes/pixel.go
@@ -18,6 +18,7 @@ func InitPixelRoutes() {
http.HandleFunc("/getPixelInfo", getPixelInfo)
if !core.ArtPeaceBackend.BackendConfig.Production {
http.HandleFunc("/placePixelDevnet", placePixelDevnet)
+ http.HandleFunc("/placeExtraPixelsDevnet", placeExtraPixelsDevnet)
}
http.HandleFunc("/placePixelRedis", placePixelRedis)
}
@@ -92,6 +93,50 @@ func placePixelDevnet(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Pixel placed"))
}
+func placeExtraPixelsDevnet(w http.ResponseWriter, r *http.Request) {
+ // TODO: Disable in production
+ reqBody, err := io.ReadAll(r.Body)
+ if err != nil {
+ panic(err)
+ }
+ // TODO: Pixel position instead of x, y
+ // Json data format:
+ /*
+ {
+ "extraPixels": [
+ { "x": 0, "y": 0, "colorId": 1 },
+ { "x": 1, "y": 0, "colorId": 2 },
+ ]
+ }
+ */
+ var jsonBody map[string][]map[string]int
+ err = json.Unmarshal(reqBody, &jsonBody)
+ if err != nil {
+ panic(err)
+ }
+
+ shellCmd := core.ArtPeaceBackend.BackendConfig.Scripts.PlaceExtraPixelsDevnet
+ contract := os.Getenv("ART_PEACE_CONTRACT_ADDRESS")
+
+ positions := strconv.Itoa(len(jsonBody["extraPixels"]))
+ colors := strconv.Itoa(len(jsonBody["extraPixels"]))
+ for _, pixel := range jsonBody["extraPixels"] {
+ pos := pixel["x"] + pixel["y"]*int(core.ArtPeaceBackend.CanvasConfig.Canvas.Width)
+ positions += " " + strconv.Itoa(pos)
+ colors += " " + strconv.Itoa(pixel["colorId"])
+ }
+
+ cmd := exec.Command(shellCmd, contract, "place_extra_pixels", positions, colors)
+ _, err = cmd.Output()
+ if err != nil {
+ fmt.Println("Error executing shell command: ", err)
+ panic(err)
+ }
+
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Write([]byte("Extra pixels placed"))
+}
+
func placePixelRedis(w http.ResponseWriter, r *http.Request) {
// TODO: Only allow mods to place pixels on redis instance
reqBody, err := io.ReadAll(r.Body)
diff --git a/backend/routes/routes.go b/backend/routes/routes.go
index 97bb3226..4dedcc93 100644
--- a/backend/routes/routes.go
+++ b/backend/routes/routes.go
@@ -8,6 +8,7 @@ func InitRoutes() {
InitTemplateRoutes()
InitUserRoutes()
InitContractRoutes()
+ InitNFTRoutes()
InitQuestsRoutes()
InitColorsRoutes()
}
diff --git a/backend/routes/templates.go b/backend/routes/templates.go
index 88e5a9b8..934cdecf 100644
--- a/backend/routes/templates.go
+++ b/backend/routes/templates.go
@@ -1,15 +1,19 @@
package routes
import (
+ "context"
"encoding/json"
"fmt"
"image"
- _ "image/png"
+ "image/color"
+ "image/png"
"io"
"io/ioutil"
"net/http"
"os"
"os/exec"
+ "strconv"
+ "strings"
"github.com/NethermindEth/juno/core/crypto"
"github.com/NethermindEth/juno/core/felt"
@@ -18,10 +22,12 @@ import (
)
func InitTemplateRoutes() {
+ http.HandleFunc("/get-templates", getTemplates)
http.HandleFunc("/addTemplateImg", addTemplateImg)
- http.HandleFunc("/addTemplateData", addTemplateData)
+ http.HandleFunc("/add-template-data", addTemplateData)
+ http.Handle("/templates/", http.StripPrefix("/templates/", http.FileServer(http.Dir("."))))
if !core.ArtPeaceBackend.BackendConfig.Production {
- http.HandleFunc("/addTemplateHashDevnet", addTemplateHashDevnet)
+ http.HandleFunc("/add-template-devnet", addTemplateDevnet)
}
}
@@ -43,6 +49,49 @@ func imageToPixelData(imageData []byte) []byte {
return []byte{0, 1, 1, 2, 2, 3}
}
+type TemplateData struct {
+ Key int `json:"key"`
+ Name string `json:"name"`
+ Hash string `json:"hash"`
+ Width int `json:"width"`
+ Height int `json:"height"`
+ Position int `json:"position"`
+ Reward int `json:"reward"`
+ RewardToken string `json:"rewardToken"`
+}
+
+func getTemplates(w http.ResponseWriter, r *http.Request) {
+ var templates []TemplateData
+ rows, err := core.ArtPeaceBackend.Databases.Postgres.Query(context.Background(), "SELECT * FROM templates")
+ if err != nil {
+ fmt.Println("Error querying templates: ", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ defer rows.Close()
+
+ for rows.Next() {
+ var template TemplateData
+ err := rows.Scan(&template.Key, &template.Name, &template.Hash, &template.Width, &template.Height, &template.Position, &template.Reward, &template.RewardToken)
+ if err != nil {
+ fmt.Println("Error scanning template: ", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ templates = append(templates, template)
+ }
+
+ out, err := json.Marshal(templates)
+ if err != nil {
+ fmt.Println("Error marshalling templates: ", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Write(out)
+}
+
func addTemplateImg(w http.ResponseWriter, r *http.Request) {
file, _, err := r.FormFile("image")
if err != nil {
@@ -52,11 +101,11 @@ func addTemplateImg(w http.ResponseWriter, r *http.Request) {
// Create a temporary file to store the uploaded file
// TODO: change location & determine valid file types
- tempFile, err := ioutil.TempFile("temp-images", "upload-*.png")
- if err != nil {
- panic(err)
- }
- defer tempFile.Close()
+ // tempFile, err := ioutil.TempFile("temp-images", "upload-*.png")
+ // if err != nil {
+ // panic(err)
+ // }
+ // defer tempFile.Close()
// Decode the image to check dimensions
img, format, err := image.Decode(file)
@@ -76,7 +125,7 @@ func addTemplateImg(w http.ResponseWriter, r *http.Request) {
if err != nil {
panic(err)
}
- tempFile.Write(fileBytes)
+ // tempFile.Write(fileBytes)
r.Body.Close()
@@ -94,20 +143,94 @@ func addTemplateData(w http.ResponseWriter, r *http.Request) {
if err != nil {
panic(err)
}
+ // Map like {"width": "64", "height": "64", "image": byte array}
var jsonBody map[string]string
err = json.Unmarshal(reqBody, &jsonBody)
if err != nil {
panic(err)
}
- hash := hashTemplateImage([]byte(jsonBody["image"]))
+ width, err := strconv.Atoi(jsonBody["width"])
+ if err != nil {
+ panic(err)
+ }
+
+ height, err := strconv.Atoi(jsonBody["height"])
+ if err != nil {
+ panic(err)
+ }
+
+ imageData := jsonBody["image"]
+ // Split string by comma
+ imageSplit := strings.Split(imageData, ",")
+ imageBytes := make([]byte, len(imageSplit))
+ for idx, val := range imageSplit {
+ valInt, err := strconv.Atoi(val)
+ if err != nil {
+ panic(err)
+ }
+ imageBytes[idx] = byte(valInt)
+ }
+
+ hash := hashTemplateImage(imageBytes)
// TODO: Store image hash and pixel data in database
+ colorPaletteHex := core.ArtPeaceBackend.CanvasConfig.Colors
+ colorPalette := make([]color.RGBA, len(colorPaletteHex))
+ for idx, colorHex := range colorPaletteHex {
+ r, err := strconv.ParseInt(colorHex[0:2], 16, 64)
+ if err != nil {
+ fmt.Println("Error converting red hex to int: ", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ g, err := strconv.ParseInt(colorHex[2:4], 16, 64)
+ if err != nil {
+ fmt.Println("Error converting green hex to int: ", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ b, err := strconv.ParseInt(colorHex[4:6], 16, 64)
+ if err != nil {
+ fmt.Println("Error converting blue hex to int: ", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ colorPalette[idx] = color.RGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: 255}
+ }
+ generatedImage := image.NewRGBA(image.Rect(0, 0, int(width), int(height)))
+ for y := 0; y < int(height); y++ {
+ for x := 0; x < int(width); x++ {
+ pos := y*int(width) + x
+ colorIdx := int(imageBytes[pos])
+ if colorIdx < len(colorPalette) {
+ generatedImage.Set(x, y, colorPalette[colorIdx])
+ }
+ }
+ }
+
+ // TODO: Path to store generated image
+ filename := fmt.Sprintf("template-%s.png", hash)
+ file, err := os.Create(filename)
+ if err != nil {
+ fmt.Println("Error creating file: ", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ defer file.Close()
+
+ err = png.Encode(file, generatedImage)
+ if err != nil {
+ fmt.Println("Error encoding image: ", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Write([]byte(hash))
}
-func addTemplateHashDevnet(w http.ResponseWriter, r *http.Request) {
+func addTemplateDevnet(w http.ResponseWriter, r *http.Request) {
// Disable this in production
if core.ArtPeaceBackend.BackendConfig.Production {
http.Error(w, "Not available in production", http.StatusNotImplemented)
@@ -124,11 +247,40 @@ func addTemplateHashDevnet(w http.ResponseWriter, r *http.Request) {
panic(err)
}
+ hash := jsonBody["hash"]
+
+ // name to hex encoding using utf-8 bytes
+ name := jsonBody["name"]
+ nameHex := fmt.Sprintf("0x%x", name)
+
+ position, err := strconv.Atoi(jsonBody["position"])
+ if err != nil {
+ panic(err)
+ }
+
+ width, err := strconv.Atoi(jsonBody["width"])
+ if err != nil {
+ panic(err)
+ }
+
+ height, err := strconv.Atoi(jsonBody["height"])
+ if err != nil {
+ panic(err)
+ }
+
+ // TODO: u256
+ reward, err := strconv.Atoi(jsonBody["reward"])
+ if err != nil {
+ panic(err)
+ }
+
+ rewardToken := jsonBody["rewardToken"]
+
// TODO: Create this script
- shellCmd := core.ArtPeaceBackend.BackendConfig.Scripts.AddTemplateHashDevnet
+ shellCmd := core.ArtPeaceBackend.BackendConfig.Scripts.AddTemplateDevnet
// TODO: remove contract from jsonBody
contract := os.Getenv("ART_PEACE_CONTRACT_ADDRESS")
- cmd := exec.Command(shellCmd, contract, "add_template", jsonBody["hash"])
+ cmd := exec.Command(shellCmd, contract, "add_template", hash, nameHex, strconv.Itoa(position), strconv.Itoa(width), strconv.Itoa(height), strconv.Itoa(reward), rewardToken)
_, err = cmd.Output()
if err != nil {
fmt.Println("Error executing shell command: ", err)
@@ -136,5 +288,5 @@ func addTemplateHashDevnet(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Access-Control-Allow-Origin", "*")
- w.Write([]byte("Hash added to devnet"))
+ w.Write([]byte("Template added to devnet"))
}
diff --git a/configs/backend.config.json b/configs/backend.config.json
index 228cf22b..06f5881d 100644
--- a/configs/backend.config.json
+++ b/configs/backend.config.json
@@ -3,7 +3,9 @@
"port": 8080,
"scripts": {
"place_pixel_devnet": "../tests/integration/local/place_pixel.sh",
- "add_template_hash_devnet": "../tests/integration/local/add_template_hash.sh"
+ "place_extra_pixels_devnet": "../tests/integration/local/place_extra_pixels.sh",
+ "add_template_devnet": "../tests/integration/local/add_template.sh",
+ "mint_nft_devnet": "../tests/integration/local/mint_nft.sh"
},
"production": false
}
diff --git a/configs/docker-backend.config.json b/configs/docker-backend.config.json
index 16a2e491..bc35db6b 100644
--- a/configs/docker-backend.config.json
+++ b/configs/docker-backend.config.json
@@ -3,7 +3,9 @@
"port": 8080,
"scripts": {
"place_pixel_devnet": "/scripts/place_pixel.sh",
- "add_template_hash_devnet": "/scripts/add_template_hash.sh"
+ "place_extra_pixels_devnet": "/scripts/place_extra_pixels.sh",
+ "add_template_devnet": "/scripts/add_template.sh",
+ "mint_nft_devnet": "/scripts/mint_nft.sh"
},
"production": false
}
diff --git a/docker-compose.yml b/docker-compose.yml
index 25796567..4e9e2252 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -83,6 +83,8 @@ services:
depends_on:
deployer:
condition: service_completed_successfully
+ apibara:
+ condition: service_started
links:
- backend
- apibara
diff --git a/frontend/src/App.js b/frontend/src/App.js
index 43656ad4..40a00d8b 100644
--- a/frontend/src/App.js
+++ b/frontend/src/App.js
@@ -1,13 +1,16 @@
-import React, { useEffect, useState } from 'react';
+import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useMediaQuery } from 'react-responsive'
import './App.css';
import Canvas from './canvas/Canvas.js';
import PixelSelector from './canvas/PixelSelector.js';
import SelectedPixelPanel from './canvas/SelectedPixelPanel.js';
+import ExtraPixelsPanel from './canvas/ExtraPixelsPanel.js';
+import TemplateBuilderPanel from './canvas/TemplateBuilderPanel.js';
import TabsFooter from './tabs/TabsFooter.js';
import TabPanel from './tabs/TabPanel.js';
import { usePreventZoom } from './utils/Window.js';
import logo from './resources/logo.png';
+import canvasConfig from "./configs/canvas.config.json"
function App() {
// Window management
@@ -33,6 +36,13 @@ function App() {
}
}
+ // Canvas
+ const width = canvasConfig.canvas.width
+ const height = canvasConfig.canvas.height
+
+ const canvasRef = useRef(null);
+ const extraCanvasRef = useRef(null);
+
// Pixel selection data
const [selectedColorId, setSelectedColorId] = useState(-1);
const [pixelSelectedMode, setPixelSelectedMode] = useState(false);
@@ -40,18 +50,71 @@ function App() {
const [selectedPositionY, setSelectedPositionY] = useState(null)
const [pixelPlacedBy, setPixelPlacedBy] = useState("");
+ const [extraPixels, setExtraPixels] = useState(42); // TODO: fetch from server
+ const [extraPixelsUsed, setExtraPixelsUsed] = useState(0);
+ const [extraPixelsData, setExtraPixelsData] = useState([]);
+
const clearPixelSelection = () => {
setSelectedPositionX(null);
setSelectedPositionY(null);
setPixelSelectedMode(false);
+ setPixelPlacedBy("");
}
const setPixelSelection = (x, y) => {
setSelectedPositionX(x);
setSelectedPositionY(y);
setPixelSelectedMode(true);
+ // TODO: move http fetch for pixel data here?
}
+ const clearExtraPixels = useCallback(() => {
+ setExtraPixelsUsed(0);
+ setExtraPixelsData([]);
+
+ const canvas = extraCanvasRef.current
+ const context = canvas.getContext('2d')
+ context.clearRect(0, 0, width, height)
+ }, [width, height])
+
+ // TODO: thread safety?
+ const clearExtraPixel = useCallback((index) => {
+ setExtraPixelsUsed(extraPixelsUsed - 1);
+ setExtraPixelsData(extraPixelsData.filter((_, i) => i !== index));
+ const canvas = extraCanvasRef.current
+ const context = canvas.getContext('2d')
+ const pixel = extraPixelsData[index]
+ const x = pixel.x
+ const y = pixel.y
+ context.clearRect(x, y, 1, 1)
+ }, [extraPixelsData, extraPixelsUsed, setExtraPixelsData, setExtraPixelsUsed])
+
+ const addExtraPixel = useCallback((x, y) => {
+ // Overwrite pixel if already placed
+ const existingPixelIndex = extraPixelsData.findIndex((pixel) => pixel.x === x && pixel.y === y)
+ if (existingPixelIndex !== -1) {
+ let newExtraPixelsData = [...extraPixelsData]
+ newExtraPixelsData[existingPixelIndex].colorId = selectedColorId
+ setExtraPixelsData(newExtraPixelsData)
+ } else {
+ setExtraPixelsUsed(extraPixelsUsed + 1);
+ setExtraPixelsData([...extraPixelsData, {x: x, y: y, colorId: selectedColorId}]);
+ }
+ }, [extraPixelsData, extraPixelsUsed, selectedColorId])
+
+ // Templates
+ const [templateCreationMode, setTemplateCreationMode] = useState(false);
+ const [templatePlacedMode, setTemplatePlacedMode] = useState(false);
+ const [templateImage, setTemplateImage] = useState(null);
+ const [templateColorIds, setTemplateColorIds] = useState([]);
+ const [templateImagePositionX, setTemplateImagePositionX] = useState(0)
+ const [templateImagePositionY, setTemplateImagePositionY] = useState(0)
+ const [templateImagePosition, setTemplateImagePosition] = useState(0)
+
+ // NFTs
+ const [nftSelectionMode, setNftSelectionMode] = useState(false);
+
+ // Timing
const [timeLeftInDay, setTimeLeftInDay] = useState('');
const startTime = "15:00";
const [hours, minutes] = startTime.split(":");
@@ -83,7 +146,7 @@ function App() {
return (
-
+
{ !isDesktopOrLaptop && (
)}
@@ -91,10 +154,16 @@ function App() {
{ (!isPortrait ? pixelSelectedMode : pixelSelectedMode && activeTab === tabs[0]) && (
)}
-
+ { (!isPortrait ? extraPixelsUsed > 0 : extraPixelsUsed > 0 && activeTab === tabs[0]) && (
+
+ )}
+ { (templateCreationMode || templatePlacedMode) && (
+
+ )}
+
diff --git a/frontend/src/canvas/Canvas.css b/frontend/src/canvas/Canvas.css
index dcaea0fa..19a23e70 100644
--- a/frontend/src/canvas/Canvas.css
+++ b/frontend/src/canvas/Canvas.css
@@ -25,6 +25,14 @@
box-shadow: 0 0 0.05rem 0.01rem rgba(0, 0, 0, 0.4);
}
+.Canvas__extras {
+ position: absolute;
+ left: 0;
+ z-index: 3;
+ background-color: rgba(0, 0, 0, 0);
+ image-rendering: pixelated;
+}
+
.Canvas__selected {
position: absolute;
z-index: 4;
@@ -44,3 +52,22 @@
pointer-events: none;
}
+
+.Canvas__nftSelection {
+ position: absolute;
+ z-index: 5;
+
+ background-color: rgba(0, 0, 0, 0);
+ outline: 0.1px solid rgba(255, 0, 0, 0.5);
+}
+
+.Canvas__template {
+ position: absolute;
+ z-index: 6;
+
+ background-color: rgba(0, 0, 0, 0);
+ image-rendering: pixelated;
+ opacity: 0.5;
+
+ outline: 0.1px solid rgba(255, 0, 0, 0.5);
+}
diff --git a/frontend/src/canvas/Canvas.js b/frontend/src/canvas/Canvas.js
index 70f2a0c7..e29ae923 100644
--- a/frontend/src/canvas/Canvas.js
+++ b/frontend/src/canvas/Canvas.js
@@ -110,7 +110,7 @@ const Canvas = props => {
if (setup) {
return;
}
- const canvas = canvasRef.current
+ const canvas = props.canvasRef.current
const context = canvas.getContext('2d')
let getCanvasEndpoint = backendUrl + "/getCanvas"
@@ -173,67 +173,173 @@ const Canvas = props => {
setupColors,
]);
+ const colorPixel = useCallback((position, color) => {
+ const canvas = props.canvasRef.current
+ const context = canvas.getContext('2d')
+ const x = position % width
+ const y = Math.floor(position / width)
+ const colorIdx = color
+ const colorHex = "#" + colors[colorIdx] + "FF"
+ context.fillStyle = colorHex
+ context.fillRect(x, y, 1, 1)
+ }, [colors, width])
+
+ const colorExtraPixel = useCallback((position, color) => {
+ const canvas = props.extraCanvasRef.current
+ const context = canvas.getContext('2d')
+ const x = position % width
+ const y = Math.floor(position / width)
+ const colorIdx = color
+ const colorHex = "#" + colors[colorIdx] + "FF"
+ context.fillStyle = colorHex
+ context.fillRect(x, y, 1, 1)
+ }, [colors, width])
+
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 [r, g, b, a] = color.match(/\w\w/g).map(x => parseInt(x, 16))
- context.fillStyle = color;
- context.fillRect(x, y, 1, 1);
+ colorPixel(lastJsonMessage.position, lastJsonMessage.color)
}
- }, [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);
+ }, [lastJsonMessage, colorPixel])
+
+ const pixelSelect = useCallback((clientX, clientY) => {
+ const canvas = props.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",
+ const position = y * width + x;
+ let getPixelInfoEndpoint =
+ backendUrl + "/getPixelInfo?position=" + position.toString();
+ fetch(getPixelInfoEndpoint, {
+ mode: "cors",
+ })
+ .then((response) => {
+ return response.text();
})
- .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);
- });
+ .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);
+ if (props.nftSelectionMode) {
+ if (!nftSelectionStarted) {
+ const canvas = props.canvasRef.current
+ const rect = canvas.getBoundingClientRect()
+ const x = Math.floor((e.clientX - rect.left) / (rect.right - rect.left) * width)
+ const y = Math.floor((e.clientY - rect.top) / (rect.bottom - rect.top) * height)
+ if (x < 0 || x >= width || y < 0 || y >= height) {
+ return
+ }
+ setNftSelectionStartX(x)
+ setNftSelectionStartY(y)
+ setNftSelectionEndX(x+1)
+ setNftSelectionEndY(y+1)
+ setNftSelectionPositionX(x)
+ setNftSelectionPositionY(y)
+ setNftSelectionWidth(1)
+ setNftSelectionHeight(1)
+ setNftSelectionStarted(true)
+ return
+ } else {
+ const canvas = props.canvasRef.current
+ const rect = canvas.getBoundingClientRect()
+ const x = Math.floor((e.clientX - rect.left) / (rect.right - rect.left) * width)
+ const y = Math.floor((e.clientY - rect.top) / (rect.bottom - rect.top) * height)
+ if (x < 0 || x >= width || y < 0 || y >= height) {
+ return
+ }
+ setNftSelectionEndX(x+1)
+ setNftSelectionEndY(y+1)
+ if (nftSelectionEndX <= nftSelectionStartX) {
+ setNftSelectionPositionX(nftSelectionEndX - 1)
+ setNftSelectionWidth(nftSelectionStartX - nftSelectionEndX + 1)
+ } else {
+ setNftSelectionPositionX(nftSelectionStartX)
+ setNftSelectionWidth(nftSelectionEndX - nftSelectionStartX)
+ }
+ if (nftSelectionEndY <= nftSelectionStartY) {
+ setNftSelectionPositionY(nftSelectionEndY - 1)
+ setNftSelectionHeight(nftSelectionStartY - nftSelectionEndY + 1)
+ } else {
+ setNftSelectionPositionY(nftSelectionStartY)
+ setNftSelectionHeight(nftSelectionEndY - nftSelectionStartY)
+ }
+ console.log("Making NFT with position: ", nftSelectionPositionX, nftSelectionPositionY, nftSelectionWidth, nftSelectionHeight)
+
+ // Mint NFT
+ let mintNFTEndpoint = backendUrl + "/mint-nft-devnet"
+ let nftPosition = nftSelectionPositionX + nftSelectionPositionY * width
+ let nftWidth = nftSelectionWidth
+ let nftHeight = nftSelectionHeight
+ fetch(mintNFTEndpoint, {
+ mode: "cors",
+ method: "POST",
+ body: JSON.stringify({
+ position: nftPosition.toString(),
+ width: nftWidth.toString(),
+ height: nftHeight.toString(),
+ }),
+ }).then(response => {
+ return response.text()
+ }).then(data => {
+ console.log(data)
+ }).catch(error => {
+ console.error("Error minting nft")
+ console.error(error)
+ });
+
+ setNftSelectionStarted(false)
+ setNftSelectionPositionX(-1)
+ setNftSelectionPositionY(-1)
+ setNftSelectionWidth(0)
+ setNftSelectionHeight(0)
+ setNftSelectionStartX(0)
+ setNftSelectionStartY(0)
+ setNftSelectionEndX(0)
+ setNftSelectionEndY(0)
+ props.setNftSelectionMode(false)
+
+ return
+ }
+ }
+
+ if (props.templateCreationMode) {
+ const canvas = props.canvasRef.current
+ const rect = canvas.getBoundingClientRect()
+ const x = Math.floor((e.clientX - rect.left) / (rect.right - rect.left) * width)
+ const y = Math.floor((e.clientY - rect.top) / (rect.bottom - rect.top) * height)
+ if (x < 0 || x >= width || y < 0 || y >= height) {
+ return
+ }
+
+ let templatePosition = x + y * width
+ // TODO: Template preview
+
+ props.setTemplateImagePositionX(x)
+ props.setTemplateImagePositionY(y)
+ props.setTemplateImagePosition(templatePosition)
+ props.setTemplatePlacedMode(true)
+ props.setTemplateCreationMode(false)
+ return
+ }
+
+ pixelSelect(e.clientX, e.clientY)
if (props.selectedColorId === -1) {
return;
}
@@ -241,7 +347,18 @@ const Canvas = props => {
return;
}
-
+ if (props.extraPixels > 0) {
+ // TODO: allow overwrite on all pixels used
+ if (props.extraPixelsUsed < props.extraPixels) {
+ props.addExtraPixel(props.selectedPositionX, props.selectedPositionY)
+ colorExtraPixel(props.selectedPositionX + props.selectedPositionY * width, props.selectedColorId)
+ return
+ } else {
+ // TODO: Notify user of no more extra pixels
+ return
+ }
+ }
+
const position = props.selectedPositionX + props.selectedPositionY * width
const colorIdx = props.selectedColorId
let placePixelEndpoint = backendUrl + "/placePixelDevnet"
@@ -336,8 +453,86 @@ const Canvas = props => {
return "#" + colors[props.selectedColorId] + "FF";
};
+ // TODO
+ //const templateImage = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 4, 3]
+ //const templateWidth = 4
+ //const templatePositionX = 13
+ //const templatePositionY = 13
+ //
+
+ // TODO
+ // TODO: setup nft selection mode func w/ stop pixel selection
+ const [nftSelectionPositionX, setNftSelectionPositionX] = useState(-1)
+ const [nftSelectionPositionY, setNftSelectionPositionY] = useState(-1)
+ const [nftSelectionWidth, setNftSelectionWidth] = useState(0)
+ const [nftSelectionHeight, setNftSelectionHeight] = useState(0)
+ const [nftSelectionStartX, setNftSelectionStartX] = useState(0)
+ const [nftSelectionStartY, setNftSelectionStartY] = useState(0)
+ const [nftSelectionEndX, setNftSelectionEndX] = useState(0)
+ const [nftSelectionEndY, setNftSelectionEndY] = useState(0)
+ const [nftSelectionStarted, setNftSelectionStarted] = useState(false)
+
useEffect(() => {
const setFromEvent = (e) => {
+ if (props.nftSelectionMode) {
+ if (!nftSelectionStarted) {
+ const canvas = props.canvasRef.current
+ const rect = canvas.getBoundingClientRect()
+ const x = Math.floor((e.clientX - rect.left) / (rect.right - rect.left) * width)
+ const y = Math.floor((e.clientY - rect.top) / (rect.bottom - rect.top) * height)
+ if (x < 0 || x >= width || y < 0 || y >= height) {
+ return
+ }
+ setNftSelectionStartX(x)
+ setNftSelectionStartY(y)
+ setNftSelectionEndX(x+1)
+ setNftSelectionEndY(y+1)
+ setNftSelectionPositionX(x)
+ setNftSelectionPositionY(y)
+ setNftSelectionWidth(1)
+ setNftSelectionHeight(1)
+ return
+ } else {
+ const canvas = props.canvasRef.current
+ const rect = canvas.getBoundingClientRect()
+ const x = Math.floor((e.clientX - rect.left) / (rect.right - rect.left) * width)
+ const y = Math.floor((e.clientY - rect.top) / (rect.bottom - rect.top) * height)
+ if (x < 0 || x >= width || y < 0 || y >= height) {
+ return
+ }
+ setNftSelectionEndX(x+1)
+ setNftSelectionEndY(y+1)
+ if (nftSelectionEndX <= nftSelectionStartX) {
+ setNftSelectionPositionX(nftSelectionEndX - 1)
+ setNftSelectionWidth(nftSelectionStartX - nftSelectionEndX + 1)
+ } else {
+ setNftSelectionPositionX(nftSelectionStartX)
+ setNftSelectionWidth(nftSelectionEndX - nftSelectionStartX)
+ }
+ if (nftSelectionEndY <= nftSelectionStartY) {
+ setNftSelectionPositionY(nftSelectionEndY - 1)
+ setNftSelectionHeight(nftSelectionStartY - nftSelectionEndY + 1)
+ } else {
+ setNftSelectionPositionY(nftSelectionStartY)
+ setNftSelectionHeight(nftSelectionEndY - nftSelectionStartY)
+ }
+ return
+ }
+ }
+ if (props.templateCreationMode) {
+ const canvas = props.canvasRef.current
+ const rect = canvas.getBoundingClientRect()
+ const x = Math.floor((e.clientX - rect.left) / (rect.right - rect.left) * width)
+ const y = Math.floor((e.clientY - rect.top) / (rect.bottom - rect.top) * height)
+ if (x < 0 || x >= width || y < 0 || y >= height) {
+ return
+ }
+ // TODO: Stop template overflows
+ props.setTemplateImagePositionX(x)
+ props.setTemplateImagePositionY(y)
+ return
+ }
+
if (props.selectedColorId === -1) {
return;
}
@@ -348,14 +543,7 @@ const Canvas = props => {
return () => {
window.removeEventListener("mousemove", setFromEvent);
};
- }, [props.selectedColorId, pixelSelect]);
-
- // TODO
- //const templateImage = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 4, 3]
- //const templateWidth = 4
- //const templatePositionX = 13
- //const templatePositionY = 13
- //
+ }, [props.selectedColorId, pixelSelect, props.nftSelectionMode, nftSelectionStarted, nftSelectionPositionX, nftSelectionPositionY, nftSelectionWidth, nftSelectionHeight, height, width, props.canvasRef, nftSelectionEndX, nftSelectionEndY, nftSelectionStartX, nftSelectionStartY]);
// TODO: both place options
return (
@@ -369,12 +557,21 @@ const Canvas = props => {
)}
-
+