Skip to content

Instantly share code, notes, and snippets.

@rveciana
Last active March 25, 2025 09:38
Show Gist options
  • Save rveciana/7664109 to your computer and use it in GitHub Desktop.
Save rveciana/7664109 to your computer and use it in GitHub Desktop.
Animated arabic kufic calligraphy with D3

Kufic calligraphy has impressed me since long ago. This example is from the walls of the Gudi Khatun Mausoleum in Nakhchivan, Azerbaijan.

Gudi Khatun Mausoleum

The text is animated in the correct order to understand how the words are ordered. The meaning of the text is

There is no God but God, and Muhammad is His prophet. May God bless him.

First, I made the SVG image from the pictures I found. The elements must be lines so they can be animated this way. That's why kufic calligraphy is good for the example, since all the strokes have the same width.

Once the SVG was made, I rotated and scaled, and added to the HTML. Every path was assigned an id of the form id="p14", where the number has to go in the order we want to draw the strokes.

The function drawStroke selects the stroke and changes the stroke-dashoffset as shown in this example.

<!DOCTYPE html>
<html>
<head>
<script src="http://d3js.org/d3.v3.min.js"></script>
<meta charset=utf-8 />
<title>Test Path</title>
</head>
<body>
<svg width="600px" height="600px" >
<g transform="scale(0.5) translate(450,-230) rotate(45) ">
<path
style="fill:none;stroke:#000000;stroke-width:20px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 619.20254,525.91121 0,105.91999 -22.04281,0 0,42.36799 -22.42473,5e-4 0,45.51697 44.46754,-5e-4 0,-45.51697 -22.04281,0 0,-42.36799 -22.42473,5e-4 2.3e-4,-105.91999"
id="p0"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:20px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 530.26791,525.91121 -0.2147,193.80495 -44.25261,0"
id="p1"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:20px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 485.8006,525.91121 0,193.80495 -91.60648,0 0,-43.79935 47.2346,0 0,43.79935"
id="p2"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:20px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 351.53986,525.33527 0,206.38139"
id="p3"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:20px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 306.21385,480.41093 0,150.84603 -22.04281,0 0,42.94223 -22.42473,5e-4 0,45.51697 44.46754,-5e-4 0,-45.51697 -22.04281,0 0,-42.94223 -22.42473,0 2.3e-4,-150.84553"
id="p4"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:20px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 216.99284,436.30862 0,194.94834"
id="p5"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:20px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 218.13793,719.71666 -92.46529,0 4.5e-4,-45.51748"
id="p6"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:20px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 218.71091,674.19918 -93.03782,0 -4.5e-4,-89.03003 48.88336,0 -3.2e-4,46.08803 -48.88336,0"
id="p7"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:20px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 125.81561,496.42537 48.74039,4.9e-4 0,45.42857 -48.74039,-4.9e-4 0,-90.51614"
id="p8"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:20px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 174.91112,392.223 0,59.25794 -49.09551,-0.14314 0,-134.16531 49.09551,0 0,47.85484 -49.09551,0 0,-150.19015 0,58.20198 49.09551,0 0,-58.20198"
id="p9"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:20px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 216.99284,334.37518 0,-106.126 148.63316,0"
id="p10"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:20px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 263.36862,334.11014 -0.57229,-61.07096 45.2307,0 -2.5e-4,61.07096 2.5e-4,-61.07096 42.368,0 0.57229,61.07096 -0.57229,-61.07096 93.89664,0 -2.5e-4,50.47896 -46.66205,0 0,-93.89664 58.66205,0"
id="p11"
inkscape:connector-curvature="0"/>
<path
style="fill:none;stroke:#000000;stroke-width:20px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 484.98708,333.82387 -1.6e-4,-104.20269 58.92638,0"
id="p12"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:20px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 529.045,378.08185 0,-117.94334"
id="p13"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:20px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 558.51324,229.90778 60.68976,3e-4 0,45.51696"
id="p14"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:20px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 558.51324,275.42474 60.68976,3e-4 0,88.17124 -45.52132,-3e-4 0,-43.22681 45.52132,3e-4"
id="p15"
inkscape:connector-curvature="0"/>
<path
style="fill:none;stroke:#000000;stroke-width:20px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 528.45487,454.05745 -0.57254,-48.37975 91.32067,0 0,48.37975 -134.50227,2.4e-4 0,-61.17951 0.57254,103.52424 146.42973,-2.4e-4"
id="p16"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:20px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 497.15244,365.02718 -113.35724,0"
id="p17"
inkscape:connector-curvature="0"/>
<path
style="fill:none;stroke:#000000;stroke-width:20px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 350.967,346.30834 0,107.74984 -42.94,0"
id="p18"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:20px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 308.027,346.30824 0,107.74994 -49.3296,0 3.8e-4,-89.031 -40.88957,0 0,42.008 40.88957,0"
id="p19"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:20px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 397.629,393.27094 0,62.34648 43.8,0.8093 0,-63.15618 -1.2e-4,103.13149"
id="p20"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:20px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 336.02312,496.40233 105.40576,-3e-4 1.2e-4,43.82807"
id="p21"
inkscape:connector-curvature="0"/>
<path
style="fill:none;stroke:#000000;stroke-width:20px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 381.36612,540.2304 60.06288,-3e-4 0,91.02675 -43.8,6.6e-4 0,-46.088 43.8,-6.6e-4"
id="p22"
inkscape:connector-curvature="0" />
</g>
</svg>
<script>
strokeTime = 800; //miliseconds / 500 px
delay = 0;
for (i=0; i<23; i++)
drawStroke(i);
function drawStroke(strokeNum){
var stroke = d3.select('path#p'+strokeNum);
var strokeLength = stroke.node().getTotalLength();
stroke
.style('stroke-dasharray', function(d) {
//debugger
var l = d3.select(this).node().getTotalLength();
return l + 'px, ' + l + 'px';
})
.style('stroke-dashoffset', function(d) {
return d3.select(this).node().getTotalLength() + 'px';
})
.transition()
.delay(delay)
.duration(strokeLength * strokeTime/500)
.ease("linear")
.style('stroke-dashoffset', '0px');
delay = delay + (strokeLength * strokeTime/500);
}
</script>
</body>
</html>
@cyber-ai-dep
Copy link

how do you convert it to one path?

@rveciana
Copy link
Author

I guess you can just merge all the labels into a single d. Since you have the initial m, you are moving to the place, so it's equivalent. You will have to change the animation approach, though. The problem is that now each stroke will be animated after the other, and if it's a single path, this won't be posible in the same way,

@rveciana
Copy link
Author

i made a version at observable using canvas

https://observablehq.com/d/7a0a8c684e21dfd9

@cyber-ai-dep
Copy link

I want to understand how you achieved the effect of a single continuous path that looks handwritten, rather than an outline with an empty interior. I'm trying to create something similar using TTF fonts. Could you explain the technique or share any insights on how to accomplish this?

As the next image:
https://i.sstatic.net/WxD4eMAw.png

@rveciana
Copy link
Author

OK. So I did it using the stroke. Since the fonts are squared in this case, a wide line is good enough for it. In your case, you should first transform the ttf to svg (Inkscape can do that easily). Then, I'm not very sure. Maybe you could use a stroke too in your case, as the letters seem to have the same width all along the line. So from the generated SVG, you should draw the center line or something so get the path you want to use.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment