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.
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 aradius
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 asnumSlices
- I extended the
- 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!
Leave a Reply