Last active
October 6, 2021 13:01
-
-
Save hvitis/11712d595b9b2c32f992026c7425cd72 to your computer and use it in GitHub Desktop.
PaperJS with GatsbyJS to use Raster mosaic functionality.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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