/PaperJS with GatsbyJS to use Raster mosaic functionality.
Last active Nov 30, 2020
PaperJS with GatsbyJS to use Raster mosaic functionality.
import React from "react" | |
import { Form, Container, Row, Col, Badge } from "react-bootstrap" | |
import colors from "./utils/colors.json" | |
import BadgeColor from "./utils/BadgeColor.tsx" | |
import { IoIosCheckmarkCircle, IoIosCheckbox } from "react-icons/io" | |
import { formatAndDownloadBsxFile } from "./utils/formatBsxFile" | |
import { formatAndDownloadXmlFile } from "./utils/formatXmlFile" | |
import { formatAndDownloadLdrFile } from "./utils/formatLDrawFile" | |
class PaperCanvas extends React.Component { | |
constructor(props) { | |
super(props) | |
// if (typeof window !== `undefined`) { | |
// this.updateDimensions() | |
// } | |
this.fullColors = colors | |
this.NEAREST_COLOR_COMPARE = {} | |
colors.map( | |
color => (this.NEAREST_COLOR_COMPARE[color.bl_name.toString()] = color.hex_code) | |
) | |
this.coords = [ | |
{ | |
id: "16x16", | |
value: 16, | |
}, | |
{ | |
id: "32x32", | |
value: 32, | |
}, | |
{ | |
id: "46x46", | |
value: 46, | |
}, | |
{ | |
id: "48x48", | |
value: 48, | |
}, | |
{ | |
id: "64x64", | |
value: 64, | |
}, | |
] | |
this.state = { | |
height: 900, | |
width: 900, | |
updated: false, | |
isCircle: true, | |
file: null, | |
newPhoto: false, | |
selectedBoardSize: 46, | |
colors: [], | |
hasFileNotUploadedError: false, | |
LDrawMatrix: [], | |
} | |
this.handleImageUpload = this.handleImageUpload.bind(this) | |
this.handleSelectDropdown = this.handleSelectDropdown.bind(this) | |
this.updateDimensions = this.updateDimensions.bind(this) | |
this.makeMosaic = this.makeMosaic.bind(this) | |
this.resizeMethod = this.resizeMethod.bind(this) | |
this.clearCanvas = this.clearCanvas.bind(this) | |
this.updateColors = this.updateColors.bind(this) | |
this.handleBsxSave = this.handleBsxSave.bind(this) | |
this.handleXmlSave = this.handleXmlSave.bind(this) | |
this.handleLdrSave = this.handleLdrSave.bind(this) | |
this.handleChangeShape = this.handleChangeShape.bind(this) | |
} | |
componentDidMount() { | |
if (typeof window !== `undefined`) { | |
let paper = require("paper") | |
paper.setup("paperCanvas") | |
var raster = new paper.Raster("mosaic") | |
raster.visible = false | |
raster.position = paper.view.center | |
} | |
window.addEventListener("resize", this.resizeMethod) | |
} | |
makeMosaic(callback) { | |
if (!this.state.file) { | |
this.setState({ hasFileNotUploadedError: true }) | |
return | |
} | |
this.setState({ hasFileNotUploadedError: false }) | |
this.clearCanvas() | |
let paper = require("paper") | |
var nearestColor = require("nearest-color").from(this.NEAREST_COLOR_COMPARE) | |
// Create a raster item using the image tag with id='mona' | |
var raster = new paper.Raster("mosaic") | |
// Hide the Raster: | |
raster.position = paper.view.center | |
raster.visible = false | |
// The size of our grid cells: | |
var gridSize = this.state.selectedBoardSize | |
// Space the cells by 120%: | |
var spacing = 19 | |
// As the web is asynchronous, we need to wait for the raster to load | |
// before we can perform any operation on its pixels. | |
// passing type of points to raster | |
raster.isCircle = this.state.isCircle | |
raster.fullColors = this.fullColors | |
raster.on("load", function() { | |
let colorCodes = [] | |
// This verification array is an additional copy of colorCodes array | |
// in order to push just hex codes strings and not Objects for later filtering | |
// this should be refactored | |
let colorCodesVerify = [] | |
// LDraw matrix | |
let LDrawMatrix = [] | |
// Since the example image we're using is much too large, | |
// and therefore has way too many pixels, lets downsize it to | |
// 40 pixels wide and 30 pixels high: | |
raster.size = new paper.Size(gridSize, gridSize) | |
// LDraw board is x, y, z coordinates. Here we start by placing first x coordinate | |
// that we will be increasing during iteration. | |
let LDrawXCoord = 10 | |
let LDrawYCoord = -24 | |
for (var x = 0; x < raster.width; x++) { | |
let verticalRowBotToTop = [] | |
let LDrawZCoord = -10 | |
for (var y = raster.height - 1; y >= 0; y--) { | |
// Get the color of the pixel: | |
var color = raster.getPixel(x, y) | |
if (raster.isCircle) { | |
// Create a circle ART MOSAIC shaped path: | |
var path = new paper.Path.Circle({ | |
center: new paper.Point(x * spacing, y * spacing), | |
// center: paper.view.center, | |
// radius: gridSize / 2 / spacing, | |
radius: 9, | |
}) | |
} else { | |
// Create a square PORTRAIT shaped path: | |
var path = new paper.Path.Rectangle({ | |
point: new paper.Point(x * spacing, y * spacing), | |
// center: paper.view.center, | |
// radius: gridSize / 2 / spacing, | |
size: 18, | |
}) | |
} | |
// Set the fill color of the path to the color | |
// of the pixel: | |
let singleColor = {} | |
let hexColor = color.toCSS(true) | |
let pickedColor = nearestColor(hexColor) | |
let filteredColour = raster.fullColors.filter( | |
color => color.hex_code === pickedColor.value | |
) | |
if (!colorCodesVerify.includes(pickedColor.value)) { | |
/* colors contains already the color we're iterating */ | |
colorCodesVerify.push(pickedColor.value) | |
singleColor["hex_code"] = pickedColor.value | |
singleColor["name"] = pickedColor.name | |
singleColor["bl_id"] = filteredColour[0].bl_id | |
singleColor["amount"] = 1 | |
colorCodes.push(singleColor) | |
} else { | |
// Getting the color code from the array of colors that will be used for BUTTONS | |
let newFilteredColour = colorCodes.filter(color => color.hex_code === pickedColor.value) | |
console.log(newFilteredColour) | |
newFilteredColour[0]["amount"] += 1 | |
} | |
// Appending a LDraw color value to vertical row | |
verticalRowBotToTop.push({ | |
x: LDrawXCoord, | |
y: LDrawYCoord, | |
z: LDrawZCoord, | |
color: filteredColour[0].ldraw_id, | |
}) | |
// Increasing vertical location of a next piece | |
LDrawZCoord += 20 | |
path.fillColor = pickedColor.value | |
} | |
LDrawMatrix.push(verticalRowBotToTop) | |
// Increasing horizontal location of next piece | |
LDrawXCoord += 20 | |
} | |
paper.project.activeLayer.position = paper.view.center | |
// Returning colors array to create buttons with infromation and | |
// Returning LDraw matrix of color IDs for LDraw to draw board | |
callback(colorCodes, LDrawMatrix) | |
}) | |
} | |
clearCanvas() { | |
if (typeof window !== `undefined`) { | |
let paper = require("paper") | |
paper.project.activeLayer.removeChildren() | |
paper.project.clear() | |
} | |
} | |
updateDimensions() { | |
this.setState({ | |
height: window.innerWidth, | |
width: window.innerWidth, | |
}) | |
} | |
resizeMethod() { | |
// this.updateDimensions() | |
this.makeMosaic(this.updateColors) | |
} | |
updateColors(colors, LDrawMatrix) { | |
// Order here colours by amount of them in the picture | |
colors.sort((a, b) => | |
a.amount > b.amount ? 1 : b.amount > a.amount ? -1 : 0 | |
) | |
console.log(LDrawMatrix) | |
this.setState({ colors: colors, LDrawMatrix: LDrawMatrix }) | |
} | |
handleImageUpload(event) { | |
this.setState({ | |
file: URL.createObjectURL(event.target.files[0]), | |
}) | |
} | |
handleSelectDropdown(event) { | |
this.setState({ | |
selectedBoardSize: event.target.value, | |
}) | |
} | |
handleBsxSave() { | |
formatAndDownloadBsxFile(this.state.colors) | |
} | |
handleLdrSave() { | |
formatAndDownloadLdrFile(this.state.LDrawMatrix) | |
} | |
handleXmlSave() { | |
formatAndDownloadXmlFile(this.state.colors) | |
} | |
handleCanvasSave() { | |
var FileSaver = require("file-saver") | |
var canvas = document.getElementById("paperCanvas") | |
canvas.toBlob(function(blob) { | |
FileSaver.saveAs(blob, "LEGO-Art.png") | |
}) | |
} | |
handleChangeShape() { | |
this.setState({ isCircle: !this.state.isCircle }) | |
} | |
render() { | |
return ( | |
<div> | |
<Container> | |
<LEGORow> | |
<StyledCol> | |
<Form.Label>Select image:</Form.Label> | |
<Form.Group> | |
<Form.Group id="center"> | |
<StyledInput | |
id="file" | |
type="file" | |
onChange={this.handleImageUpload} | |
/> | |
</Form.Group> | |
</Form.Group> | |
</StyledCol> | |
<StyledCol> | |
<Form.Label>Board size:</Form.Label> | |
<Form.Group controlId="SelectToBucket"> | |
<StyledSelect | |
required | |
type="text" | |
as="select" | |
size="lg" | |
onChange={this.handleSelectDropdown} | |
name="selectedToBucket" | |
value={this.state.selectedBoardSize} | |
> | |
{this.coords.map(sizeOfBoard => ( | |
<StyledOption | |
key={sizeOfBoard.id} | |
value={sizeOfBoard.value} | |
> | |
{sizeOfBoard.id} | |
</StyledOption> | |
))} | |
</StyledSelect> | |
</Form.Group> | |
</StyledCol> | |
<StyledCol> | |
<StyledDiv> | |
<Form.Label>Tile shape:</Form.Label> | |
</StyledDiv> | |
<StyledIcon | |
onClick={() => { | |
this.handleChangeShape() | |
}} | |
> | |
{this.state.isCircle ? ( | |
<IoIosCheckmarkCircle /> | |
) : ( | |
<IoIosCheckbox /> | |
)} | |
</StyledIcon> | |
</StyledCol> | |
</LEGORow> | |
</Container> | |
<StyledContainer> | |
<GenerateButton onClick={() => this.makeMosaic(this.updateColors)}> | |
Generate | |
</GenerateButton> | |
</StyledContainer> | |
{this.state.hasFileNotUploadedError ? ( | |
<StyledContainer> | |
<DangerMark>Please select an image first!</DangerMark> | |
</StyledContainer> | |
) : ( | |
<></> | |
)} | |
{this.state.colors.length !== 0 ? ( | |
<> | |
<StyledContainer> | |
<StyledParagraph> | |
<Tooltip> | |
<InfoMark>Scroll down</InfoMark> to learn how to | |
<InfoMark>use</InfoMark> .bsx and .xml | |
<InfoMark>files</InfoMark> | |
</Tooltip> | |
Download | |
</StyledParagraph> | |
</StyledContainer> | |
<StyledContainer> | |
<DownloadButton onClick={() => this.handleBsxSave()}> | |
.bsx file | |
</DownloadButton> | |
<DownloadButton onClick={() => this.handleXmlSave()}> | |
.xml file | |
</DownloadButton> | |
<DownloadButton onClick={() => this.handleLdrSave()}> | |
.ldr file | |
</DownloadButton> | |
<DownloadButton onClick={() => this.handleCanvasSave()}> | |
.png image | |
</DownloadButton> | |
</StyledContainer> | |
</> | |
) : ( | |
<StyledContainer> | |
Generate art to get | |
<StyledParagraph> | |
<Tooltip> | |
<InfoMark>.png</InfoMark> file is your | |
<InfoMark>mosaic</InfoMark> image to{" "} | |
<InfoMark>Download</InfoMark> | |
</Tooltip> | |
.png | |
</StyledParagraph>{" "} | |
or | |
<StyledParagraph> | |
<Tooltip> | |
<InfoMark>.xml</InfoMark> file you can use in various places to | |
<InfoMark>download</InfoMark> list of needed | |
<InfoMark>pieces</InfoMark> | |
</Tooltip> | |
.xml | |
</StyledParagraph> | |
or | |
<StyledParagraph> | |
<Tooltip> | |
<InfoMark>.bsx</InfoMark> file is used to make | |
<InfoMark>orders</InfoMark> on | |
<InfoMark>BrickLink.com</InfoMark> | |
</Tooltip> | |
.bsx | |
</StyledParagraph> | |
</StyledContainer> | |
)} | |
<Container> | |
{this.state.colors.length !== 0 ? ( | |
<StyledParagraph> | |
<Tooltip> | |
<InfoMark>Hover</InfoMark> on each item to see | |
<InfoMark>amount</InfoMark> of pieces used in the | |
<InfoMark>mosaic</InfoMark> | |
</Tooltip> | |
List of colors : | |
</StyledParagraph> | |
) : ( | |
<></> | |
)} | |
{this.state.colors.map(color => ( | |
<BadgeColor color={color} /> | |
))} | |
{this.state.colors.length !== 0 ? ( | |
<StyledParagraph> | |
<Tooltip> | |
<InfoMark>Average</InfoMark> price of | |
<InfoMark>all</InfoMark> pieces. | |
</Tooltip> | |
Estimated price :{" "} | |
{`${this.state.selectedBoardSize * | |
this.state.selectedBoardSize * | |
0.04} $`} | |
</StyledParagraph> | |
) : ( | |
<></> | |
)} | |
</Container> | |
<LEGORow> | |
<img | |
src={this.state.file} | |
alt="WOMAN" | |
crossOrigin="*" | |
ref="mosaic" | |
id="mosaic" | |
hidden | |
/> | |
<canvas | |
id="paperCanvas" | |
height={this.state.height} | |
width={this.state.width} | |
></canvas> | |
</LEGORow> | |
</div> | |
) | |
} | |
} | |
export default PaperCanvas |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment