A tutorial that teaches how to make complex nfts that are procedurally generated and have onchain metadata and images.
pure
and view
functions called outside the blockchain don’t cost any gas. This means that you can use multiple contracts to assemble a relatively large graphic without additional costs!
LandSeaSkyNFT
. Import OpenZeppelin’s ERC-721, inherit from it, and set it up with the constructor, a mint function, and a counter to keep track of the token ID:
_baseURI()
Function_baseURI()
with the base URL for the location you select to keep your NFT metadata. This could be a website, IPFS folder, or many other possible locations.
Since this contract will be generating the .json file directly, instead set it to indicate this to the browser:
Strings
library. Go ahead and import them:
tokenURI()
FunctiontokenURI
function override. You’ll need to write some other contracts to make this work, but you can write most of the code and stub out a plan for the rest to:
"name"
of the NFT"description"
"image"
_baseURI
and return it.tokenURI
function. You should get something similar to:
echo -n '<string to decode>' | base64 --decode
Do so, and you’ll get:
SVGRenderer
. It doesn’t need a constructor, but it will need the Strings
library:
abi.encodePacked
to build everything from the SVG except the actual art. That’s much too big for one contract, so add stubs instead.
Depending on the tool you used to make the SVG, there may be unneeded extras you can remove from these lines. You also don’t need the items in <defs>
or <styles>
. You’ll take advantage of the flexibility of the format to include those in the pieces returned by the supporting contract.
SeaRenderer
, with a function called render
. The <g>
element is the root of the different pieces of the SVG, so add that and a stub for the rest.
viewBox="0 0 1024 1024"
. Move the <defs>
and <scripts>
tag inside of the <g>
tag. Open the SVG in the browser to make sure it hasn’t broken.
Next, delete the id
and data-name
from the top level <g>
and experiment with the transform="translate(20,2.5)"
property to move the art back down to the bottom of the viewport.
With the sample art, <g transform="translate(0,700)">
should work.
The last edits you need to make are critical - do a find/replace to change all of the cls-1
and similar classnames, to cls-land-1
! Otherwise, the classes will override one another and nothing will be the right color. Also find all instances of linear-gradient
and do the same.
Make sure you change both the definitions, and where they’re called!
Finally, use the tool of your choice to minify only the outermost <g>
tag and its contents. This will flatten the code to a single line and remove extra empty character spaces. Doing so makes it easier to add to your contract, and makes the data smaller. Add it as a constant string to SeaRenderer.sol
:
TODO
with the constant.
<SVG>
tags, it renders as expected!
SVGRenderer.sol
. Add an interface
for the SeaRenderer
. All of your renderer contracts will have a function called render
that either takes a uint _tokenId
, or no arguments, and returns a string. Because of this, you can use a single interface for all the render contracts:
SVGRenderer
contract for the SeaRenderer
, and a constructor that takes an address for the SeaRenderer
:
// TODO: Add the sea,
with a call to your external function.
LandSeaSkyNFT
contract and add an interface
for the SVGRenderer
.
constructor
to set it:
TODO
with a line to Base64.encode
a call to the renderer:
LandSeaSkyNFT
(the above script will do this).
Open the contract in Basescan, connect with your wallet, and mint some NFTs.
Wait a few minutes, then open the testnet version of Opensea and look up your contract. It may take several minutes to show up, but when it does, if everything is working you’ll see NFTs with the ocean part of the art! Neat!
<defs>
and <style>
elements inside the top-level <g>
, and transform/translate that group to the correct location (0,0 will work!).
Change cls-1
to cls-sky-1
in both the definition and where it’s used. Add sky
to the linear-gradient
as well.
Delete the data and layer information for this group as well. You’ll end up with:
<g>
group and contents, then create two constant strings, one for everything before the first <stop>
element, and one for everything after the last <stop>
element.
Neither string should have the <stop>
s. You’ll make those next.
SkyRenderer
. Add your strings:
offset
and stop-color
properties:
render
function. It will use the built in method of using abi.encode
and casting to string
to combine all the parts and return them.
buildStop
:
_tokenId
. It needs to be “random” in the sense that every NFT should be different, but it has to be deterministic, so that you get the same art every time you load the image.
First, subtract 10 from the values and convert them to uints
without decimals in each of your stop constants, and reduce the last to 80
:
_buildStop
.
Add a function to _buildOffsetValue
. This will pick an integer between 0 and 20 for each offset, and add it to the modified offsets you just made. The result will be a change of + or 1 10 for each value (with the last being slightly different to keep it in range):
".12+.20" == ".32"
.
Finally, update your render
function to call _buildStop
:
SVGRenderer.sol
and add an instance of ISVGPartRenderer
for the skyRenderer. Add an argument to the constructor
and initialize it, then call the render
function in place of your TODO
for the background.
viewBox
to 1024x1024 and move the <defs>
and <styles>
inside the top-level group (<g>
).
Find transform/translate values that first put the mountains so that they are at the bottom, and the left-most portion is shown, then the right-most. transform="translate(-150,350)"
and transform="translate(-800,350)"
are about right.
Don’t forget to add -land
to the classnames!
LandRenderer
:
<g>
element, and add a constant with everything after the opening <g>
tag. Use similar techniques as before to generate an offset based on the token id, then build the SVG. You’ll end up with something like this:
SVGRenderer
:
viewBox
to 1024x1024<defs>
and <styles>
into the first group-sun
to the classnamesSunRenderer
s as you have the other rendering contracts. You’ll have to incorporate this one a little differently. Add a function that picks which SunRenderer
to call, based on the NFT id.
skyRenderer
in the main render
function!