Skip to content

Commit 7047478

Browse files
committed
Merge branch 'main' into multi-canvas
2 parents 2916b67 + 00a99c0 commit 7047478

File tree

6 files changed

+161
-23
lines changed

6 files changed

+161
-23
lines changed

backend/routes/stencils.go

+97-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package routes
22

33
import (
4+
"bytes"
5+
"encoding/json"
46
"fmt"
57
"image"
68
"image/color"
@@ -26,6 +28,7 @@ func InitStencilsRoutes() {
2628
http.HandleFunc("/get-hot-stencils", getHotStencils)
2729
http.HandleFunc("/add-stencil-img", addStencilImg)
2830
http.HandleFunc("/add-stencil-data", addStencilData)
31+
http.HandleFunc("/get-stencil-pixel-data", getStencilPixelData)
2932
if !core.ArtPeaceBackend.BackendConfig.Production {
3033
http.HandleFunc("/add-stencil-devnet", addStencilDevnet)
3134
http.HandleFunc("/remove-stencil-devnet", removeStencilDevnet)
@@ -402,7 +405,7 @@ func addStencilImg(w http.ResponseWriter, r *http.Request) {
402405

403406
r.Body.Close()
404407

405-
imageData, err := imageToPixelData(fileBytes, 1)
408+
imageData, err := worldImageToPixelData(fileBytes, 1, 0)
406409
if err != nil {
407410
routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to convert image to pixel data")
408411
return
@@ -708,3 +711,96 @@ func unfavoriteStencilDevnet(w http.ResponseWriter, r *http.Request) {
708711

709712
routeutils.WriteResultJson(w, "Stencil unfavorited in devnet")
710713
}
714+
715+
func worldImageToPixelData(imageData []byte, scaleFactor int, worldId int) ([]int, error) {
716+
img, _, err := image.Decode(bytes.NewReader(imageData))
717+
if err != nil {
718+
return nil, err
719+
}
720+
721+
colors, err := core.PostgresQuery[ColorType]("SELECT hex FROM WorldsColors WHERE world_id = $1 ORDER BY color_key", worldId)
722+
if err != nil {
723+
return nil, err
724+
}
725+
726+
colorCount := len(colors)
727+
palette := make([]color.Color, colorCount)
728+
for i := 0; i < colorCount; i++ {
729+
colorHex := colors[i]
730+
palette[i] = hexToRGBA(colorHex)
731+
}
732+
733+
bounds := img.Bounds()
734+
width, height := bounds.Max.X, bounds.Max.Y
735+
scaledWidth := width / scaleFactor
736+
scaledHeight := height / scaleFactor
737+
pixelData := make([]int, scaledWidth*scaledHeight)
738+
739+
for y := 0; y < height; y += scaleFactor {
740+
for x := 0; x < width; x += scaleFactor {
741+
newX := x / scaleFactor
742+
newY := y / scaleFactor
743+
rgba := color.RGBAModel.Convert(img.At(x, y)).(color.RGBA)
744+
if rgba.A < 128 { // Consider pixels with less than 50% opacity as transparent
745+
pixelData[newY*scaledWidth+newX] = 0xFF
746+
} else {
747+
closestIndex := findClosestColor(rgba, palette)
748+
pixelData[newY*scaledWidth+newX] = closestIndex
749+
}
750+
}
751+
}
752+
753+
return pixelData, nil
754+
}
755+
756+
func getStencilPixelData(w http.ResponseWriter, r *http.Request) {
757+
// Get stencil hash from query params
758+
hash := r.URL.Query().Get("hash")
759+
if hash == "" {
760+
routeutils.WriteErrorJson(w, http.StatusBadRequest, "Hash parameter is required")
761+
return
762+
}
763+
764+
// Read the stencil image file
765+
filename := fmt.Sprintf("stencils/stencil-%s.png", hash)
766+
fileBytes, err := os.ReadFile(filename)
767+
if err != nil {
768+
routeutils.WriteErrorJson(w, http.StatusNotFound, "Stencil not found")
769+
return
770+
}
771+
772+
// Convert image to pixel data
773+
pixelData, err := worldImageToPixelData(fileBytes, 1, 0)
774+
if err != nil {
775+
routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to process image")
776+
return
777+
}
778+
779+
// Get image dimensions
780+
img, _, err := image.Decode(bytes.NewReader(fileBytes))
781+
if err != nil {
782+
routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to decode image")
783+
return
784+
}
785+
bounds := img.Bounds()
786+
width, height := bounds.Max.X, bounds.Max.Y
787+
788+
// Create response structure
789+
response := struct {
790+
Width int `json:"width"`
791+
Height int `json:"height"`
792+
PixelData []int `json:"pixelData"`
793+
}{
794+
Width: width,
795+
Height: height,
796+
PixelData: pixelData,
797+
}
798+
799+
jsonResponse, err := json.Marshal(response)
800+
if err != nil {
801+
routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to create response")
802+
return
803+
}
804+
805+
routeutils.WriteDataJson(w, string(jsonResponse))
806+
}

backend/routes/templates.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,7 @@ func addTemplateData(w http.ResponseWriter, r *http.Request) {
462462
}
463463

464464
func getTemplatePixelData(w http.ResponseWriter, r *http.Request) {
465+
// Get template hash from query params
465466
hash := r.URL.Query().Get("hash")
466467
if hash == "" {
467468
routeutils.WriteErrorJson(w, http.StatusBadRequest, "Hash parameter is required")
@@ -477,7 +478,7 @@ func getTemplatePixelData(w http.ResponseWriter, r *http.Request) {
477478
}
478479

479480
// Convert image to pixel data using existing function
480-
pixelData, err := imageToPixelData(fileBytes, 1)
481+
pixelData, err := imageToPixelData(fileBytes, 1) // Scale factor 1 for templates
481482
if err != nil {
482483
routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to process image")
483484
return

frontend/src/App.js

+50-16
Original file line numberDiff line numberDiff line change
@@ -735,32 +735,49 @@ function App() {
735735
let timestamp = Math.floor(Date.now() / 1000);
736736
if (!devnetMode) {
737737
await extraPixelPlaceCall(
738-
extraPixelsData.map(
739-
(pixel) => pixel.x + pixel.y * canvasConfig.canvas.width
740-
),
738+
extraPixelsData.map((pixel) => pixel.x + pixel.y * width),
741739
extraPixelsData.map((pixel) => pixel.colorId),
742740
timestamp
743741
);
744742
} else {
745-
let placeExtraPixelsEndpoint = 'place-extra-pixels-devnet';
746-
const response = await fetchWrapper(placeExtraPixelsEndpoint, {
747-
mode: 'cors',
748-
method: 'POST',
749-
body: JSON.stringify({
743+
if (worldsMode) {
744+
const firstPixel = extraPixelsData[0];
745+
const formattedData = {
746+
worldId: openedWorldId.toString(),
747+
position: (firstPixel.x + firstPixel.y * width).toString(),
748+
color: firstPixel.colorId.toString(),
749+
timestamp: timestamp.toString()
750+
};
751+
752+
const response = await fetchWrapper('place-world-pixel-devnet', {
753+
mode: 'cors',
754+
method: 'POST',
755+
body: JSON.stringify(formattedData)
756+
});
757+
if (response.result) {
758+
console.log(response.result);
759+
}
760+
} else {
761+
const formattedData = {
750762
extraPixels: extraPixelsData.map((pixel) => ({
751-
position: pixel.x + pixel.y * canvasConfig.canvas.width,
763+
position: pixel.x + pixel.y * width,
752764
colorId: pixel.colorId
753765
})),
754766
timestamp: timestamp
755-
})
756-
});
757-
if (response.result) {
758-
console.log(response.result);
767+
};
768+
769+
const response = await fetchWrapper('place-extra-pixels-devnet', {
770+
mode: 'cors',
771+
method: 'POST',
772+
body: JSON.stringify(formattedData)
773+
});
774+
if (response.result) {
775+
console.log(response.result);
776+
}
759777
}
760778
}
761779
for (let i = 0; i < extraPixelsData.length; i++) {
762-
let position =
763-
extraPixelsData[i].x + extraPixelsData[i].y * canvasConfig.canvas.width;
780+
let position = extraPixelsData[i].x + extraPixelsData[i].y * width;
764781
colorPixel(position, extraPixelsData[i].colorId);
765782
}
766783
if (basePixelUsed) {
@@ -1149,6 +1166,16 @@ function App() {
11491166
return [];
11501167
};
11511168

1169+
const getStencilPixelData = async (hash) => {
1170+
if (hash !== null) {
1171+
const response = await fetchWrapper(
1172+
`get-stencil-pixel-data?hash=${hash}`
1173+
);
1174+
return response.data;
1175+
}
1176+
return [];
1177+
};
1178+
11521179
const getNftPixelData = async (tokenId) => {
11531180
if (tokenId !== null) {
11541181
const response = await fetchWrapper(
@@ -1177,6 +1204,13 @@ function App() {
11771204
return;
11781205
}
11791206

1207+
// Handle stencil overlay case
1208+
if (overlayTemplate.isStencil && overlayTemplate.hash) {
1209+
const data = await getStencilPixelData(overlayTemplate.hash);
1210+
setTemplatePixels(data);
1211+
return;
1212+
}
1213+
11801214
// Handle template overlay case
11811215
if (overlayTemplate.hash) {
11821216
const data = await getTemplatePixelData(overlayTemplate.hash);
@@ -1444,7 +1478,7 @@ function App() {
14441478
isMobile={isMobile}
14451479
overlayTemplate={overlayTemplate}
14461480
templatePixels={templatePixels}
1447-
width={canvasConfig.canvas.width}
1481+
width={width}
14481482
canvasRef={canvasRef}
14491483
addExtraPixel={addExtraPixel}
14501484
addExtraPixels={addExtraPixels}

frontend/src/tabs/TabPanel.js

+2
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,8 @@ const TabPanel = (props) => {
182182
stencilCreationSelected={props.stencilCreationSelected}
183183
setStencilCreationSelected={props.setStencilCreationSelected}
184184
canvasWidth={props.width}
185+
setTemplateOverlayMode={props.setTemplateOverlayMode}
186+
setOverlayTemplate={props.setOverlayTemplate}
185187
/>
186188
</CSSTransition>
187189
<SwitchTransition mode='out-in'>

frontend/src/tabs/stencils/StencilCreationPanel.js

+8-4
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,14 @@ const StencilCreationPanel = (props) => {
112112
})
113113
});
114114
if (addResponse.result) {
115-
// TODO: after tx done, add stencil to backend
116-
// TODO: Double check hash match
117-
// TODO: Update UI optimistically & go to specific faction in factions tab
118-
console.log(addResponse.result);
115+
props.setOverlayTemplate({
116+
hash: hash,
117+
width: props.stencilImage.width,
118+
height: props.stencilImage.height,
119+
image: props.stencilImage.image,
120+
isStencil: true
121+
});
122+
props.setTemplateOverlayMode(true);
119123
closePanel();
120124
props.setActiveTab('Stencils');
121125
}

frontend/src/tabs/stencils/StencilItem.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,8 @@ const StencilItem = (props) => {
164164
width: props.width,
165165
height: props.height,
166166
position: props.position,
167-
image: props.image
167+
image: props.image,
168+
isStencil: true
168169
};
169170
props.setTemplateOverlayMode(true);
170171
props.setOverlayTemplate(template);

0 commit comments

Comments
 (0)