I'm developing a digital circuit design and simulation tool similar to Logisim, and I'm struggling immensely to render the distinctive curved shape of logic gates like OR/NOR gates. Despite numerous attempts with Bezier curve parameters, I cannot achieve the classic "pointed-oval" shape seen in standard schematics.
My architecture consists of two main components that work together to render logic gates from JSON definitions:
0. Shape Definition System
Each component is defined in a JSON file (canvas_elements.json) using primitive shapes:
{
"id": "OR_Gate",
"name": "OR Gate",
"anchorPoint": [80, 60],
"shapes": [
{
"type": "BezierShape",
"p0": {"x": 43, "y": 20},
"p1": {"x": 85, "y": 24},
"p2": {"x": 110, "y": 60},
"color": "#333333"
},
{
"type": "BezierShape",
"p0": {"x": 43, "y": 100},
"p1": {"x": 85, "y": 96},
"p2": {"x": 110, "y": 60},
"color": "#333333"
},
{
"type": "BezierShape",
"p0": {"x": 43, "y": 20},
"p1": {"x": 60, "y": 60},
"p2": {"x": 43, "y": 100},
"color": "#333333"
}
]
}
Important context: The Bezier parameters in my JSON files are the result of painstaking manual tuning over many hours. I haven't found any tools for visually debugging these parameters; it's purely trial-and-error, adjusting bit by bit based on whether the drawn shape looks more "standard." Each adjustment requires recompiling and running the program to see the effect, which is extremely inefficient.
1. CanvasModel: JSON Parsing and Element Factory (CanvasModel.h) The CanvasModel.h header defines a loader function that reads my canvas_elements.json file and constructs CanvasElement objects:
// CanvasModel.h - JSON deserialization entry point
#pragma once
#include <wx/wx.h>
#include <vector>
#include <json/json.h>
class CanvasElement;
// Global function: JSON -> CanvasElement list
std::vector<CanvasElement> LoadCanvasElements(const wxString& jsonPath);
This function parses the JSON array, creates a CanvasElement instance for each gate definition, and calls the element's AddShape() method to populate its internal shape storage.
2. CanvasElement: Shape Storage and Bezier Rendering Engine (CanvasElement.h) The CanvasElement.h header defines the complete data structures and rendering pipeline:
// CanvasElement.h - Core shape storage and Bezier math
#pragma once
#include <wx/wx.h>
#include <wx/dcgraph.h>
#include <wx/graphics.h>
#include <vector>
#include <variant>
struct Point { int x, y; };
// Quadratic Bezier curve definition (loaded directly from JSON)
struct BezierShape {
Point p0, p1, p2;
wxColour color;
BezierShape(Point p0 = Point(), Point p1 = Point(), Point p2 = Point(),
wxColour c = wxColour(0, 0, 0))
: p0(p0), p1(p1), p2(p2), color(c) {}
};
// Shape variant that stores all drawable primitives
using Shape = std::variant<Line, PolyShape, Circle, Text, Path, ArcShape, BezierShape>;
class CanvasElement {
private:
std::vector<Shape> m_shapes; // All shapes for this element, including Bezier curves
// Core Bezier math: samples quadratic curve into polyline
std::vector<wxPoint> CalculateBezier(const Point& p0, const Point& p1,
const Point& p2, int segments = 16) const {
std::vector<wxPoint> points;
for (int i = 0; i <= segments; ++i) {
double t = static_cast<double>(i) / segments;
// Quadratic Bezier formula: P(t) = (1-t)²·P₀ + 2(1-t)t·P₁ + t²·P₂
double x = (1-t)*(1-t)*p0.x + 2*(1-t)*t*p1.x + t*t*p2.x;
double y = (1-t)*(1-t)*p0.y + 2*(1-t)*t*p1.y + t*t*p2.y;
points.push_back(wxPoint(static_cast<int>(x), static_cast<int>(y)));
}
return points;
}
void DrawVector(wxGCDC& gcdc) const;
void DrawFallback(wxDC& dc) const;
public:
// Called by LoadCanvasElements() for each shape in JSON
void AddShape(const Shape& shape) { m_shapes.push_back(shape); }
// Main draw entry point
void Draw(wxDC& dc) const;
// Retrieves stored shapes (used by Draw() method)
const std::vector<Shape>& GetShapes() const { return m_shapes; }
};
The Complete Rendering Pipeline:
LoadCanvasElements() reads JSON and creates CanvasElement instances
For each Bezier definition in JSON, it constructs a BezierShape and calls AddShape() to push it into m_shapes
During rendering, CanvasElement::Draw() iterates through m_shapes variant
When a BezierShape is encountered, CalculateBezier() converts the mathematical curve into a polyline of 16 segments
The resulting wxPoint array is drawn using wxGCDC::DrawLines() or equivalent
3. Current Output The result after this pipeline processes my manually-tuned parameters:

My current OR gate rendering: After countless manual parameter adjustments, the three Bezier curves combine into a fat pear shape. The back is too rounded, there are visible creases where curves meet, and the overall shape is far from the standard OR gate.
This is what a standard OR gate should look like (screenshot from open-source software logisim-evolution):

Standard OR gate shape: Relatively flat back, smooth and natural curves, distinct "pointed" output side, and well-proportioned overall geometry.
The three Bezier curves don't create a seamless, mathematically precise OR gate shape. The issues are:
Discontinuity: Curves meet with visible kinks instead of smooth tangents
Wrong geometry: The back curve is too semicircular; real OR gates have a flatter back with specific curvature
No standard: I can't find canonical Bezier control points for logic gates anywhere
Is there a standard set of quadratic Bezier parameters that produces the correct OR/NOR gate shape used in IEEE/ANSI symbols?
Should I abandon Bezier curves and switch to:
SVG path definitions loaded from files?
Custom C++ drawing functions with hardcoded geometry?
Arcs + lines instead of pure Bezier curves?
How do professional EDA tools like Logisim, KiCad, or digital logic simulators typically render these shapes? Do they use vector primitives or bitmap icons?
If sticking with my current Bezier approach, what's the mathematical relationship between the control points to ensure:
Smooth continuity (G1 continuity) where curves meet
Proper aspect ratio (height vs width)
The characteristic "pointed" output side?
Any guidance or examples from someone who has implemented logic gate rendering would be greatly appreciated. I'd prefer to keep my JSON-driven vector system if possible, but I'm open to architectural changes if that's the wrong approach entirely.
In short, there are many reasons to choose SVG as the basis (or inspiration) for your data structure such as easy visualization or editing in graphic applications.
Fortunately, the "core-concepts" of Bézier (quadratic or cubic) descriptions are quite universal – regardless of the description language or object model.
While the notation structure can vary significantly we can usually translate graphics quite easily.
Based on you JSON example you could create a rendered output in SVG based on the exact same x/y coordinates:
M (Moveto – where to start drawing – introducing a new sub-shape)Q 1. quadratic Bézier control pointQ 2. quadratic Bézier control pointIn your example we can easily connect all segments to get a continuous closed shape by omitting new start points:
svg {
display: block;
outline: 1px solid #ccc;
overflow: visible;
}
.grd {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1em;
}
<div class="grd">
<div class="col">
<h3>Original graphic</h3>
<svg viewBox="0 0 150 120">
<path fill="none" stroke="red" stroke-width="10" d="
M
43 20
Q
85 24
110 60
M
43 100
Q
85 96
110 60
M
43 20
Q
60 60
43 100
" />
</svg>
</div>
<div class="col">
<h3>Connected segments</h3>
<svg viewBox="0 0 150 120">
<path fill="none" stroke="red" stroke-width="10" d="
M
43 20
Q
85 24
110 60
Q
85 96
43 100
Q
60 60
43 20
z
" />
</svg>
</div>
</div>
This get's tricky as many "plotting" environments solely rely on polygons/polylines.
Some might be capable of converting Bézier or circular Arc descriptions internally – all in all a polyline conversion is the most robust approach.
Next problem: many of these environments can't express "complex" polygons so multiple separated polygon vertice arrays.
In other words:
You need to "group" connected curves for pointed corners – otherwise
all vertices are connected to a single polyline and may result in weird renderings like this:
I've used Wikipedia contributor Jbeard's SVG example
In this case we need to create multiple separate shape objects:
While we could do this grouping programmatically by comparing start and end points of segments it's usually easier to do this visually in a graphic editor like Inkscape.
However, this common lack of sub-path definitions is a common pitfall so we need to be careful how we process our graphics – a sub path splitting/grouping is necessary for most applications.
Fun fact: even in SVG we need to use <path> as <polyline> or <polygon> only supports flat vertex arrays.
For your data structure you may also take inspiration from the SVG pathData notation as suggested in SVG 2 draft that describes vector/bezier commands as an array of objects like so:
let pathData = [
{
type: 'M',
values: [43,20]
},
{
type: 'Q',
values: [85,24,110,60]
}
]
The main advantage of this notation is the balance between verbosity and simplicity – all command types have the same number of object properties and can easily be concatenated to rendered SVG d strings. Since we have a type property we can switch different calculations according to the type without the need to introduce different properties for each type.
So a cleaned up output – that could be used in your asset data structure may look like this:
{
id: 'OR_Gate',
name: 'OR Gate',
anchorPoint: [80,60],
shapes: [
[
{
type: 'M',
values: [43,20]
},
{
type: 'Q',
values: [85,24,110,60]
},
{
type: 'Q',
values: [85,96,43,100]
},
{
type: 'Q',
values: [60,60,43,20]
},
{
type: 'Z',
values: []
}
]
]
}
There is nothing wrong with using quadratic Béziers (compact notation, many advantages for geometry calculations) the main reason the vast majority of graphic applications solely use cubic Béziers is they can replicate more complex curvatures and are more intuitive to handle in a UI (subjective).
Regardless of the language you can easily calculate points on Béziers for polyline approximations based on De Casteljau algorithm like so (you already did this):
// cubic
let pt = {
x:
t1 ** 3 * p0.x +
3 * t1 ** 2 * t * cp1.x +
3 * t1 * t ** 2 * cp2.x +
t ** 3 * p.x,
y:
t1 ** 3 * p0.y +
3 * t1 ** 2 * t * cp1.y +
3 * t1 * t ** 2 * cp2.y +
t ** 3 * p.y
};
// quadratic
let pt = {
x: t1 * t1 * p0.x + 2 * t1 * t * cp1.x + t ** 2 * p.x,
y: t1 * t1 * p0.y + 2 * t1 * t * cp1.y + t ** 2 * p.y
};
Where p0 is the previous segments on-path coordinate, cp1, cp2 the respective control points, p the commands final on-path point and t a number between 0 and 1 – e.g steps of 0.25 would split a curve to 4 vertices.
These are in 99% percent converted to something like cubic or polyline approximations. So all in all clearly overrated: you won't be able to tell the difference in any rendering or output despite the geometrical inaccuracy of cubic approximations (also, no output device is by any means mathematically accurate)
Also a common pitfall is the interpretation of Y coordinates. Many applications use a top to bottom y-axis (e.g SVG, Canvas) others use the traditional Cartesian bottom to top y-axis system (e.g PDF or Opentype fonts). So depending on your target format and your coordinate notation you may need to "flip" y-coordinates by subtracting the document height from each y-value (or the opposite).
As mentioned design tasks are way more intuitive using a GUI.
But for a calculable coordinate structure we need to be careful the application returns a "non-optimized" output by means of:
H, v, S or T (reflecting previous coordinates)JS Example/Proof of concept (polyline generation from json)
let shapesJson = `{"id":"OR_Gate","name":"OR Gate","anchorPoint":[80,60],"shapes":[[{"type":"M","values":[15,9]},{"type":"L","values":[5,9]}],[{"type":"M","values":[15,21]},{"type":"L","values":[5,21]}],[{"type":"M","values":[35,15]},{"type":"L","values":[45,15]}],[{"type":"M","values":[35,15]},{"type":"C","values":[30,5,20,5,20,5]},{"type":"L","values":[13,5]},{"type":"C","values":[13,5,16,9,16,15]},{"type":"C","values":[16,21,13,25,13,25]},{"type":"L","values":[20,25]},{"type":"C","values":[20,25,30,25,35,15]},{"type":"Z","values":[]}]]}`;
let shapeDef = JSON.parse(shapesJson)
// convert to polyline
let precision = 4
let polys = toPolyline(shapeDef, precision);
// render new polyline elements
polys.forEach(poly => {
let polyEl = `<polygon points="${poly.map( pt=>{ return `${pt.x} ${pt.y}` } )}" />`
polylines.insertAdjacentHTML('beforeend', polyEl)
})
//polylines
function toPolyline(shapeDef, splits = 4) {
let {
shapes
} = shapeDef;
// all poly shapes
let polys = [];
// loop through all shapes
shapes.forEach((shape) => {
// current polygon
let poly = [];
// loop pathdata
shape.forEach((com, i) => {
let {
type,
values
} = com;
if (values.length) {
// M or L commands
if (values.length === 2) {
poly.push({
x: values[0],
y: values[1]
});
}
// beziers
else {
// get previous on-path point
let comPrev = shape[i - 1]
let lastXY = comPrev.values.slice(-2)
let p0 = {
x: lastXY[0],
y: lastXY[1]
}
// first bezier control point
let cp1 = {
x: values[0],
y: values[1]
}
let pts = [p0, cp1]
// last on-path point
let p = {
x: values[values.length - 2],
y: values[values.length - 1]
}
// add second control point for cubic beziers
if (type === 'C') {
let cp2 = {
x: values[2],
y: values[3]
}
pts.push(cp2)
}
// add final point for bezier calculations
pts.push(p)
// calculate points on bezier
let tInterval = 1 / splits;
for (let t = tInterval; t < 1; t += tInterval) {
// calculate point on curve and add to poly
let vertex = pointAtT(pts, t)
poly.push(vertex);
}
// add last on-path point at t=1
poly.push(p);
}
}
});
polys.push(poly)
});
return polys
}
function pointAtT(pts, t = 0.5) {
// used for linetos
const interpolate = (p1, p2, t) => {
return {
x: (p2.x - p1.x) * t + p1.x,
y: (p2.y - p1.y) * t + p1.y
};
};
// used for quadratic or cubic beziers
const getPointAtBezierT = (pts, t = 0) => {
let p0 = pts[0];
let cp1 = pts[1];
let p = pts[pts.length - 1];
let t1 = 1 - t;
// cubic beziers
if (pts.length === 4) {
let cp2 = pts[2];
return {
x: t1 ** 3 * p0.x +
3 * t1 ** 2 * t * cp1.x +
3 * t1 * t ** 2 * cp2.x +
t ** 3 * p.x,
y: t1 ** 3 * p0.y +
3 * t1 ** 2 * t * cp1.y +
3 * t1 * t ** 2 * cp2.y +
t ** 3 * p.y
};
}
// quadratic beziers
else {
return {
x: t1 * t1 * p0.x + 2 * t1 * t * cp1.x + t ** 2 * p.x,
y: t1 * t1 * p0.y + 2 * t1 * t * cp1.y + t ** 2 * p.y
};
}
};
// switch between bezier or lineto
return pts.length > 2 ? getPointAtBezierT(pts, t) : interpolate(pts[0], pts[1], t);
}
svg {
display: block;
outline: 1px solid #ccc;
overflow: visible;
}
.grd {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1em;
}
<div class="grd">
<div class="col">
<h3>Grouped shapes</h3>
<svg id="svg1" viewBox="0 0 50 30">
<g fill="none" stroke="red">
<path d="M 15 9L 5 9" />
<path d="M 15 21 L 5 21" />
<path d="M 35 15 L 45 15" />
<path d="M 35 15
C 30 5 20 5 20 5
L 13 5
C 13 5 16 9 16 15
C 16 21 13 25 13 25
L 20 25
C 20 25 30 25 35 15
z" />
</g>
</svg>
</div>
<div class="col">
<h3>Polyline shapes</h3>
<svg id="path2" viewBox="0 0 50 30">
<g id="polylines" fill="none" stroke="red"></g>
</svg>
</div>
</div>