AI generated Caesar wheel

Generating a Caesar Cipher Wheel with AI

Listen

My son is in 1st grade, and one day, he came home with a puzzle. The puzzle contained an “encrypted text” as a series of numbers, and he had to do additions and subtractions to find the correct letters. He loved the idea of encryption; although this wasn’t encryption but encoding, the idea stuck with him.

Caesar cipher

Lucky for him, his father (me) is also into security and cryptography. What better time to introduce crypto than 7-years-old. I quickly engaged with him and showed him Caesar ciphers. These are simple substitutional ciphers, where you take one letter and exchange it for another. The key is simply the number of character positions you must shift when the letters are in alphabetical order. So the character “A” would become an “F”, if encoded with 5 as the key (assuming an English alphabet). To make things more user friendly, the Romans created rotating wheels, where one could simply rotate a plate to get the correct substitution rules.

Not too secure, but if you used a longer key, for instance “15, 3, 22, 7, 19, 8, 24, 12” it could make things rather complicated for an outside observer.

He wanted to create a Caesar cipher wheel immediately using the Hungarian alphabet.

No problem, it shouldn’t take too long.

How I imagined this would go… 😊

Creating a cipher wheel

Little did I think about that statement before saying it. There are lots of details involved in this. Drawing a perfect circle is easy using household items. Dividing it up into equal slices is a challenge without tools. We were at our grandparents’, so there was no protractor, ruler, “angle measurer,” or any tool for geometry. Okay, let’s do this digitally. It might seem trivial at first, but how do you divide a circle into equal slices using GIMP? How do you even draw a circular path?! It’s more complicated than it looks. So, firing up GIMP didn’t help much. We got stuck at drawing a circle 😅 My quick estimates showed that he’ll lose interest before we finish.

Let’s turn to AI; we can prompt through this in about 10 minutes.

Indeed, we did, and this post documents that journey.

AI for the win

I used Google’s free Gemini and quickly settled on JS canvas and HTML as the base. I also wanted to test how well it performs, so I only gave it a loose prompt at first. It still involves a bit of problem scoping, but it is still very far from a precisely engineered prompt.

First attempt

“i need an html page, that has some JS in it. the page must have a canvas that’s 1024×768 pixels large. in the middle of this canvas there should be a circle that’s as large as possible. the circle must be sliced up to 44 equal parts. i need the html with the javascript that does the above.”

Here is what I got back.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Sliced Circle</title>
    <style>
        body {
            margin: 0;
            padding: 0;
        }
        canvas {
            display: block;
        }
    </style>
</head>
<body>
    <canvas id="myCanvas" width="1024" height="768"></canvas>
    <script>
        const canvas = document.getElementById('myCanvas');
        const ctx = canvas.getContext('2d');

        // Get the center coordinates for the canvas
        const centerX = canvas.width / 2;
        const centerY = canvas.height / 2;

        // Calculate the maximum radius that fits inside the canvas
        const radius = Math.min(centerX, centerY);

        // Define the number of slices
        const numSlices = 44;

        // Function to draw a single slice
        function drawSlice(sliceIndex) {
            const angle = Math.PI * 2 * (sliceIndex / numSlices);
            const startAngle = angle;
            const endAngle = angle + (Math.PI * 2 / numSlices);

            ctx.beginPath();
            ctx.moveTo(centerX, centerY); // Move to center
            ctx.arc(centerX, centerY, radius, startAngle, endAngle, false); // Draw arc
            ctx.lineTo(centerX, centerY); // Line back to center to close the slice
            ctx.fillStyle = `hsl(${sliceIndex * 360 / numSlices}, 50%, 50%)`; // Set fill color based on slice index
            ctx.fill();
        }

        // Draw all slices
        for (let i = 0; i < numSlices; i++) {
            drawSlice(i);
        }
    </script>
</body>
</html>

Looking through this, a few things jump out. There is a canvas, mathematical formulas, and pi, likely dealing with the circular shape. It also has comments. I would not call it clean, and it’s far from what I would have written. However, this took 3 seconds to generate. I consider myself a good problem solver and adept programmer, but it would have taken me 100 or 1000x times longer.

Here is how it looks.

Not bad, but this ain’t a Caesar cipher wheel yet. 😉

Next

“alter it a bit: do not fill it with colors. include all the letters from the hungarian alphabet on the edge of the slice. draw a bit smaller circle as well with the same center point. fill the smaller one with the alphabet like the large one.”

I only included the script for brevity.

<script>
    const canvas = document.getElementById('myCanvas');
    const ctx = canvas.getContext('2d');

    // Get the center coordinates for the canvas
    const centerX = canvas.width / 2;
    const centerY = canvas.height / 2;

    // Calculate the radius for the large circle
    const largeRadius = Math.min(centerX, centerY) - 50; // Subtract 50 for spacing

    // Define the number of slices
    const numSlices = 44;

    // Hungarian alphabet
    const hungarianAlphabet = "AÁBCDEÉFGHIJKLMNOÓÖŐPQRSTUÚÜŰVWXYZ";

    // Function to draw a single slice with text
    function drawSlice(sliceIndex) {
        const angle = Math.PI * 2 * (sliceIndex / numSlices);
        const startAngle = angle;
        const endAngle = angle + (Math.PI * 2 / numSlices);

        // Draw large circle arc
        ctx.beginPath();
        ctx.moveTo(centerX, centerY);
        ctx.arc(centerX, centerY, largeRadius, startAngle, endAngle, false);
        ctx.stroke(); // Stroke the arc (no fill)

        // Calculate text position
        const textAngle = (startAngle + endAngle) / 2;
        const textX = centerX + largeRadius * Math.cos(textAngle);
        const textY = centerY + largeRadius * Math.sin(textAngle);

        // Draw text
        ctx.textAlign = "center";
        ctx.textBaseline = "middle";
        ctx.font = "20px sans-serif";
        ctx.fillText(hungarianAlphabet[sliceIndex % hungarianAlphabet.length], textX, textY);
    }

    // Draw all slices on large circle
    for (let i = 0; i < numSlices; i++) {
        drawSlice(i);
    }

    // Draw smaller circle filled with text
    const smallRadius = largeRadius / 2;
    ctx.beginPath();
    ctx.arc(centerX, centerY, smallRadius, 0, Math.PI * 2, true); // Fill the smaller circle
    ctx.fillStyle = "#eee"; // Light gray fill color
    ctx.fill();
    ctx.stroke(); // Stroke the circle outline

    // Draw text on the smaller circle
    for (let i = 0; i < numSlices; i++) {
        drawSlice(i, smallRadius); // Call with smaller radius for positioning
    }
</script>

WOW!

Here is how the output looks at this point.

Minor cosmetics

Looking at the code, I had to change a few minor things.

  • Now that there are two circles, the drawSlice(...) call must also include a radius parameter, so the letters are written to the smaller circle as well
  • I changed the letter colors to black
  • I shifted the letters inward so they wouldn’t be on the line

Plus, I had another prompt.

“rotate the letters so their bottom faces the center point of the circle. only return the changed letter drawing code.”

function drawSlice(sliceIndex, radius) {
    const angle = Math.PI * 2 * (sliceIndex / numSlices);
    const startAngle = angle;
    const endAngle = angle + (Math.PI * 2 / numSlices);

    // Draw large circle arc (unchanged)
    ctx.beginPath();
    ctx.moveTo(centerX, centerY);
    ctx.arc(centerX, centerY, radius, startAngle, endAngle, false);
    ctx.stroke();

    // Calculate text position (unchanged)
    const textAngle = (startAngle + endAngle) / 2;
    const textX = centerX + radius * Math.cos(textAngle);
    const textY = centerY + radius * Math.sin(textAngle);

    // Text rotation and drawing
    ctx.save(); // Save current canvas state
    ctx.translate(textX, textY); // Move to text position
    const rotationAngle = textAngle + Math.PI / 2; // Rotate 90 degrees to face center
    ctx.rotate(rotationAngle); // Apply rotation
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.font = "20px sans-serif";
    ctx.fillText(hungarianAlphabet[sliceIndex % hungarianAlphabet.length], 0, 0);
    ctx.restore(); // Restore saved canvas state
}

Voilá! This looks cool.

Everything I asked for is there. The letters are incorrect, but that’s about it. The rest of the output is already functional.

The final result

I had some things I wanted to do before finishing up.

  • Correct the letters and add some punctuation marks
    • I extended the hungarianAlphabet array and used it’s length as numSlices
  • Hide the inner slice radiuses
  • Draw a point in the middle
  • Make multi-character letters smaller
  • Change the background of the inner circle

Some of these required me to prompt, but mostly, they were minor code changes.

After all that, I duplicated the output and made it smaller to fit two on an A4 page.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Sliced Circle with Hungarian Alphabet</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            font-family: sans-serif;
        }
        canvas {
            display: block;
        }
    </style>
</head>
<body>
<div>
    <canvas id="myCanvas" width="800" height="550"></canvas>
    <canvas id="myCanvas2" width="800" height="550"></canvas>
</div>
    <script>
const c1= document.getElementById('myCanvas');
const c2= document.getElementById('myCanvas2');

drawCesar(c1); drawCesar(c2);

        function drawCesar(canvas) {
        
        const ctx = canvas.getContext('2d');

        // Get the center coordinates for the canvas
        const centerX = canvas.width / 2;
        const centerY = canvas.height / 2;

        // Calculate the radius for the large circle
        const largeRadius = Math.min(centerX, centerY) - 10; // Subtract 50 for spacing

        // Hungarian alphabet
        const hungarianAlphabet = [
		"A", 
		"Á", 
		"B",
		"C",
		"CS",
		"D",
		"DZ",
		"DZS",
		"E",
		"É",
		"F",
		"G",
		"GY",
		"H",
		"I",
		"Í",
		"J",
		"K",
		"L",
		"LY",
		"M",
		"N",
		"NY",
		"O",
		"Ó",
		"Ö",
		"Ő",
		"P",
		"Q",
		"R",
		"S",
		"SZ",
		"T",
		"TY",
		"U",
		"Ú",
		"Ü",
		"Ű",
		"V",
		"W",
		"X",
		"Y",
		"Z",
		"ZS",
		"⎵",
		".",
		"?",
		",",
		"!"];

        // Define the number of slices
        const numSlices = hungarianAlphabet.length;

        // Function to draw a single slice with text
        function drawSlice(sliceIndex, r) {
            const angle = Math.PI * 2 * (sliceIndex / numSlices);
            const startAngle = angle;
            const endAngle = angle + (Math.PI * 2 / numSlices);

            // Draw large circle arc
            ctx.beginPath();
            ctx.moveTo(centerX, centerY);
            ctx.arc(centerX, centerY, r, startAngle, endAngle, false);
            ctx.stroke(); // Stroke the arc (no fill)

            // Calculate text position
	const letter = hungarianAlphabet[sliceIndex % hungarianAlphabet.length];
	    r *= 0.92;
if (letter === "⎵") {
	r *= 1.03;
}
            const textAngle = (startAngle + endAngle) / 2;
            const textX = centerX + r * Math.cos(textAngle);
            const textY = centerY + r * Math.sin(textAngle);

            // Draw text
	   // ctx.fillStyle = "#000";
            //ctx.textAlign = "center";
            //ctx.textBaseline = "middle";
            //ctx.font = "18px sans-serif";
            //ctx.fillText(hungarianAlphabet[sliceIndex % hungarianAlphabet.length], textX, textY);

	    // Text rotation and drawing
	    ctx.save(); // Save current canvas state
	    ctx.fillStyle = "#000";
	    ctx.translate(textX, textY); // Move to text position
	    const rotationAngle = textAngle + Math.PI / 2; // Rotate 90 degrees to face center
	    ctx.rotate(rotationAngle); // Apply rotation
	    ctx.textAlign = "center";
	    ctx.textBaseline = "middle";
	    
	    ctx.font = "22px sans-serif";
	    if (letter.length > 1) {
		ctx.font = "18px sans-serif";
	    }
	    if (letter.length > 2) {
		ctx.font = "12px sans-serif";
	    }
	    ctx.fillText(letter, 0, 0);
	    ctx.restore(); // Restore saved canvas state
        }


        // Draw all slices on large circle
        for (let i = 0; i < numSlices; i++) {
            drawSlice(i, largeRadius);
        }

        // Draw smaller circle filled with text
        const smallRadius = largeRadius / 1.15;
        ctx.beginPath();
        ctx.arc(centerX, centerY, smallRadius, 0, Math.PI * 2, true); // Fill the smaller circle
        ctx.fillStyle = "#eee"; // Light gray fill color
        ctx.fill();
        ctx.stroke(); // Stroke the circle outline

        // Draw all slices on large circle
        for (let i = 0; i < numSlices; i++) {
	    drawSlice(i, smallRadius);
        }

        const smallRadius2 = smallRadius / 1.17;
        ctx.beginPath();
        ctx.arc(centerX, centerY, smallRadius2, 0, Math.PI * 2, true); // Fill the smaller circle
        ctx.fillStyle = "#fff"; // Light gray fill color
        ctx.fill();
        ctx.stroke(); // Stroke the circle outline

	  // Draw a point at the center
	  ctx.beginPath();
	  ctx.arc(centerX, centerY, 2, 0, Math.PI * 2, true); // Small circle to represent a point
	  ctx.fillStyle = "black"; // Black fill color for the point
	  ctx.fill();
}
    </script>
</body>
</html>

The code is a mess, but the result is beautiful.

This is impressive. Overall, 95% of the work was created using generative AI. The rest is nudging and guiding it. The process took less than 15 minutes, including printing and cutting the plates. Note that my prompting was lousy as well.

Let’s try to 0-shot this

Here is what I came up with after about 3 refinement rounds.

# Role
You are an expert web developer proficient in HTML and Javascript. 

# Task 
You must create an HTML page that, when printed, can be used as a Caesar cipher wheel. 

# Steps 
- Put a canvas on the page with a size of 1024x768 pixels 
- Draw a large circle on the canvas, mostly filling it 
    - Divide it up into 44 equal slices; these will contain the Hungarian alphabet letters 
- Draw a bit smaller circle with the same center point
    - Regarding size, it only has to be a bit smaller so that the larger circle's letters appear comfortably within it 
    - Divide it up into 44 equal slices; these will contain the Hungarian alphabet letters 
- Write each letter of the Hungarian alphabet inside the slices of the larger and smaller circle 
    - The letters are: A,Á,B,C,CS,D,DZ,DZS,E,É,F,G,GY,H,I,Í,J,K,L,LY,M,N,NY,O,Ó,Ö,Ő,P,Q,R,S,SZ,T,TY,U,Ú,Ü,Ű,V,W,X,Y,Z,ZS 
    - Letter with multiple characters must be smaller to fit inside the given space 
    - Make sure the letters bottom point towards the middle of the circle 
- Draw the center point of the circle

It still isn’t super specific, but you can see how I got more precise and elaborated on certain parts. Here is what it generated.

Not bad!

I tried removing the inner circle’s radiuses, but that proved more difficult than I anticipated in a 0-shot fashion. It is trivial in code but much more complicated to explain in layman’s terms.

So, where does it all bring us?

Generative AI in software development

I regularly talk to people who scorn generative AI and don’t see the benefits nor share my enthusiasm. The above story makes them more friendly towards this tech. I often see people entering this new technology with the wrong expectations. They heard somewhere that it’ll take their job away soon, and they’ll be replaced, and they already have a narrative they want to prove. Usually, the opposite of that. I have been there 😅

A different perspective

Natural language is so much more expressive and yet much less precise. That’s precisely the challenge of prompt engineering. Programmers are used to communicating in code and do so very efficiently. Code is perfect for communicating precise ideas. Using it for more complex things is more complicated because it would take many characters. And of course, that’s when you turn to abstractions, and well known patterns. Coders can make code more expressive by naming methods, grouping functionalities, and using clean code practices. It elevates the code’s expressiveness, but it won’t reach the level of a human natural language. Also note that as the level of abstraction increases precision falls away. And that’s okay.

Natural language and prompting are the ultimate abstraction layer.

It’s so high and so detached from code that even non-programmers can express themselves in a way that machines understand. Imagine the power of programming in the hands of everyone on Earth. The creative potential this can unleash is hard to overstate.

It’s incredible how little time it took to go from idea to physical implementation. The flow seems natural to engineers, but it sure isn’t that clear for non-programmers. Software people have a frame of mind that allows them to think this way. This is a huge advantage to have and it’s a skill that won’t go away anytime soon.

If it isn’t already, the power to think clearly and express yourself will be a core competency. Everyone can learn this. And once they do, their creative selves will light up.

Caveats and slippery slopes

I love to consider different perspectives, as they help me better understand our world and solidify my knowledge and beliefs. Here, I want to highlight three counterarguments I have and heard from others when sharing this story.

HTML, JS, Canvas

One might argue that settling on HTML, JS, and Canvas API already requires engineering knowledge. That’s fair, but nothing stops someone from stepping back and exploring options before jumping into implementation. Someone completely unfamiliar with this tech might take up to an hour to achieve the same results. However, their next time will be a lot faster.

The 5% is the key

The gist of the argument is the following: the 5% of the work I did requires years of software development experience. This reminds me of a favorite joke.

A machine breaks down in a factory. The manager calls an external expert. The expert comes on-site and, without inspecting the machine, smashes his hammer on the side, and it starts back up. Then, he hands in the bill

  • Expert: It’ll be $3001. 😎
  • Manager: What, for 5 seconds of work? 😳 How did you come up with that number anyway?
  • Expert: Simple, $1 since the hammer degrades with every smash, and $3000 ’cause I know exactly where to smash it. 😏

While software engineering knowledge surely helps, I think it’s more about mindset. Being able to think in terms of cause and effect, experimenting with the code, and deducing patterns and rules. This is within reach for anyone willing to invest some time. As the tech gets better, I think it’ll be easier to interact with, but this mindset will still be a crucial component of success.

Truth be told, I see lots of people ignoring this skill.

Underestimating the human in the loop

This one is something I came up with.

I might have done more than 5%, and my perception is heavily biased.

There is some truth to this claim, and it’s rather complicated to evaluate from my point of view. Therefore, I invite you to try this out on a similar problem and see where you can get.

Back to the story

My son loved the cipher wheels and spent weeks sending encoded messages. It was a lot of fun, and both he and I learned a lot. I encourage everyone to experiment with this technology. There is a lot more under the hood than you might first think.

Here is our final product 😎

Drop your expectations and allow yourself to be surprised. It’s fun!


Posted

in

,

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *