How I made an SVG Islamic Tessellation Coloring App with d3.js and React
Hey there fellow curious minded devs! It’s been a while since I’ve written a blog post and seeing as my New Year’s resolution for 2024 is to start getting back to it — I thought a cool topic for this one would be a quick, high level overview / tutorial on how I made one of my most fun projects — an Islamic Geometric Design app where you can design, color, and download an SVG of a tessellated Islamic Geometric pattern — an art form common in many places in the Muslim world.
You can see the final result here and check out the code on GitHub here.
Background
You’ll see that the app allows you to design and color variations of this popular 8 fold rosette pattern:
The construction of the pattern was based on the traditional drawing method as demonstrated by Islamic Design expert and craftsman Mohamad Aljanabi in this youtube tutorial which I highly recommend checking out.
My challenge here was to use code to implement the traditional method and rules Mohamad lays out which in short are that:
- The basic unit of repetition for the pattern is one quarter of the repeating tile.
- The quarter should be constructed in a square.
- The quarter is then mirrored horizontally and vertically to create the tile which when repeated, makes up the tessellation mosaic.
- This graphic will illustrate:
Implementing it with Code
Obviously, the first step was to follow Mr. Aljanabi’s tutorial but draw the needed constructions lines and arcs with code instead of pencil and paper!
To do this, I used a series of codepens just to sketch it all out.
First, I used SVG circle elements to draw the construction arcs and line elements to draw the construction lines — both in white, that form the pattern, drawn in black:
I then used the radii of the arcs to extrapolate basic grid lines needed to find the points of the individual shapes (this was a must since the most fun part of the final app is to be able to color individual shapes!):
Finally, I found the exact coordinates of each shape. These are all calculated dynamically relative to the length of one side of the quarter square. I defined this length as constant s
and gave it a value of 400 px — the view box dimensions I decided on for the SVG element holding the quarter.
Making it a user facing app
To then implement this as an actual user facing app, I basically took the approach of “d3 for the math, React for the DOM” which means I relied on d3 for computing the SVG path definition for each of the the lines and shapes that make up the pattern quarter, but used React to control all rendering to the screen and the different views possible in the app — e.g the ability for the user to choose the pattern variation and different colors for the lines and shapes as well as different sizes for the tessellated composition. (You can read more about the concept and benefits of this approach of combing d3 and React in this great Smashing Magazine article by Marcos Iglesias).
Project Structure
The basic project structure looks like the following:
- Each coordinate calculated relative to the length of one side of the quarter of the tile — derived from the Codepen work shown above — is stored as a constant in
src/constants.js
(here). - The
src/patterns
folder holds the d3 part and uses it with the constants fromsrc/constants.js
to compute the line and shape coordinates for each pattern quarter before exporting them as a full pattern definition schema objects that can be rendered and consumed by React. - The pattern definition is imported into the
src/components
folder where React then uses them to create the full tile that can be edited, colored, and tessellated. - The top level
src/App.js
(here) contains the rootsvg
element to which the needed paths and lines and sub elements are dynamically rendered and their sizes / colors changed in response to changes in state when a user edits the tile. Note that it’s viewBox is now set to 4 times the area of the quarter whose length is s. This is so that it can fit all 4 quarters put together to form the entire tile.
Here is an overview of the key files in each folder:
src/
--constants.js <---- Holds the calculated lengths needed to find x and y
coodinates of all lines and shapes relative to the
constant length s of one side of one quarter of the tile.
These were brought in from the POC Codepens above
--Patterns/ D3
----Pattern1
------coords.js <---- holds the raw x,y coordinates for each line and shape
------paths.js <---- turns the coordinates into computed path definitions
------index.js <---- exports an object defining the parameters of the pattern
based on the above.
----Pattern2
.....
--Components/ REACT
----Lines.js <---- Renders the lines of the tile
----Shapes.js <----- Renders the shapes of the tile and copies / rotates
each one around
the center 8 pointed start
----Tile.js <------- Utility Parent of Lines and Shapes component that renders
both to make up a single tile
----Tesselation.js <----- Repeats the tile as a tesselation
.....[Other components we'll cover shortly!]
-App.js - the root React component holding the svg element. The child elements
are the lines and shapes whose thickness, colors, and size properties
are programatically manipulated based on changes in State
Let’s now focus on each of the React components here and how they work, starting with App.js.
App (root) Component — imported pattern varations
The App Component imports the pattern definitions needed for React to draw the pattern the user chooses. PATTERN_1
is the base pattern shown in the codepens and the main on Mr. Aljanabi demonstrates how to draw. PATTERN_2
and PATTERN_3
are variations I came up with by making slight alterations to the construction base — like Mr. Aljanabi explains here.
import PATTERN_1 from "./patterns/pattern1";
import PATTERN_2 from "./patterns/pattern2";
import PATTERN_3 from "./patterns/pattern3";
const PATTERNS = {
base_pattern: PATTERN_1,
variation_1: PATTERN_2,
variation_2: PATTERN_3
};
function App() {
.....
}
App (root) Component — state
Also living inside App.js
are all pieces of state as well as state setter functions that define the changes the user can make to alter the design and appearance of the tile. These are as follows:
function App() {
// controls which pattern varation is being used in the design
// uses the base_pattern as the default
const [pattern, setPattern] = useState(PATTERNS.base_pattern);
const [selectedPattern, setSelectedPattern] = useState("base_pattern");
// controls whether the SVG will render a single tile the user is coloring
// vs a repeating tesselation of the tile the user has colored
const [tesselation, setTesselation] = useState(false);
// controls each individual color for each individual shape in the tile
// (in the base pattern this 1 8 pointed star, 8 darts, 8 petals, 4 5 pointed
// stars (edge pieces) and 4 octagons (edge pieces)
// the mapInitialColorsFromPattern function simply sets the colors of the
// shapes to some DEFAULT_COLORS I defined as constants in src/constants.js
const [colors, setColors] = useState(
mapInitialColorsFromPattern(pattern, DEFAULT_COLORS)
);
// controls current color the user has chosen from an HTML color picker
// when going to color any
// individual shape in the pattern.
const [currentColor, setCurrentColor] = useState("#FFFFFF");
/** finally a lazy catch all "state" object for everything else **/
// this controls:
// - The color of the tile lines (NOT the shapes)
// - The the thickness of the lines
// - If the app is in tesslation state, the size of each individual
// - tile in the pattern
const [state, setState] = useState({
lines: {
color: "#FFFFFF",
thickness: 5,
},
tileSize: 3,
});
....
}
App Component — handlers + Overview of Shapes, Lines, and Tessellation Components
The Handler functions that change state based on user interaction also live in App.js
. I’ll explain each one in turn while simultaneously showing how they are used as props that form the essence of the other components — the Lines, Shapes and then Tessellation components, respectively.
The handleLines function:
— When the user changes the Line color, it sets the color of the lines based on the users choice of line color by manipulating the stroke attribute of the svg path element for the Lines component.
— When the user changes the Line Weight, it sets the thickness of the lines by by manipulating the stroke-width attribute of the svg path element for the Lines component.
The handleColors function:
— Identifies the exact index / location of the shape the user has clicked on when coloring the pattern
— Sets the color of the shape the user has clicked on to the currentColor
which is set by the Shape fill color picked.
— This is done by manipulating the fill attribute for the svg path elements that make up the Shapes component.
Notice in the console how the fill
attribute of each path
element changes to the currently picked color when clicked on:
The handleTiles function:
— When the app is in Tessellation mode (tessellation === true
), this controls the size of the tiles, or how many tiles there are in the final composition of the Tessellation component.
— This is done by manipulating the percent height and width of the (extremely handy!) svg pattern element to copy the exact tile the user has finished coloring when in the single tile edit mode to be re-used and repeated — e.g a tessellation — of the single tile.
— The was this works is that:
- The pattern definition is then used in a simple svg rect (rectangle) element whose height and width are set to 100% of the overall svg viewbox.
- The fill attribute of the rectangle is then simply set to the pattern itself.
- This means that when the tile size slider is increased, the dimensions of the individual tile (the pattern element) in the tessellation (the rect element) increase:
- Notice in the console how the percentage height and width of the
pattern
element increase relative to the fixed 100% width and height of therect
Last but not least is the here is the downloadSVG function which is triggered whenever a user decides the like that they have created and decided to download it as an SVG by pressing the Download SVG button.
The function basically take copies the full SVG DOM that has resulted from the users customizations by converting it to a blob and then downloading it for the user as an SVG file that can be reused anywhere.
Conclusion
That’s about it for this high-level overview of the project I had tons of fun and challenge in creating and for which I learned tons in the process!
There are of course other components I didn’t cover here like the Editor and TileEdit components which have less to do with direct SVG manipulation via React but instead are convenience wrappers for the UI widgets that trigger the handler functions controlling / manipulating the actual SVG DOM of the tile or tessellation. If you liked this article then let me know and I can cover those in more detail in a possible second installment!
Going forward, I’d really like to add more pattern variations to the app that people can choose from to color.
However, the real challenge here is that I want to do justice to Mr. Aljanabi’s traditional method which — since it is a longstanding compass and straight edge drawing method to form the lines of the pattern whose shapes will then be cut out by craftsmen before being colored to put on different wall or building mosaics — therefore discourages the use of grids or explicit x,y coordinate finding.
This is despite the fact that x,y coordinate finding will ultimately be needed at some point or another when implementing a drawing on a computer screen, and especially needed in order to delineate exact coordinates of each individual shape in the pattern to allow for coloring. And so this is indeed what I needed to do here to the best of my ability while still trying by best to honor and follow the traditional method.