Corbes de Bézier en robòtica i enginyeria

Les corbes de Bézier són fonamentals en robòtica i enginyeria per diverses raons:

Ara, sobre els seus descobridors que treballaven en la indústria de l'automòbil i volen reduir el consum de combustible millorant el disseny i aerodinàmica dels cotxes:

UNISURF CAD va ser un sistema de disseny assistit per ordinador (CAD) desenvolupat a mitjans dels anys 1960 pel dissenyador de cotxes britànic Pierre Bézier mentre treballava a l'empresa d'automòbils francesa Renault. Aquest sistema va ser pioner en l'ús de les corbes de Bézier per al disseny de superfícies complexes i va marcar un abans i un després en la indústria automobilística.

El Peugeot 204 (1965-1983) gràcies a un acord de recerca amb Renault (1966) va ser completament modelat el 1968 amb UNISURF CAD, que va permetre elegants línies i formes amb una precisió i una suavitat fins aleshores impensables. Aquest cotxe es va convertir en un èxit i va posicionar Peugeot com a líder en el disseny de cotxes, establint al mateix temps les bases per a la utilització futura de CAD en la indústria automobilística.

L'any 1982, dos exinvestigadors de Xerox, John Warnock i Charles Geschke, van fundar Adobe Systems Incorporated. Van desenvolupa, entre altres, Adobe Illustrator, un programari de disseny vectorial pioner que aprofitava les corbes de Bézier per al disseny assistit per ordinador (CAD), permetent a dissenyadors i professionals crear traços suaus i precisos. Aquests productes van marcar un abans i un després en la indústria gràfica i van establir les bases per a la tecnologia d'impressió i disseny digital que utilitzem avui en dia.

L'any 2003, Bryce Harrington va crear Inkscape, un programari de disseny vectorial que es destaca per l'ús de les corbes de Bézier, però a diferència del de Adobe es de codi obert i gratuït.


Utilitzar Inkscape per a SVG amb Corbes de Bèzier

  1. Obre la Imatge en Inkscape:

    Inicia Inkscape i obre una nova pàgina en blanc.

  2. Importa la Imatge PNG:

    Ves a "Arxiu" > "Importar" i selecciona la teva imatge PNG.

  3. Vectoritza l'Imatge:

    Utilitza les eines de dibuix de Bezier (com l'eina "Bézier", drecera de teclat b+Majusc+F6) per dibuixar el contorn del circuit sobre la imatge importada. Crearàs punts i segments de corba.

  4. Edició de Trajectòries Bézier:

    Un cop hagis creat les trajectòries Bézier que defineixen el circuit, pots ajustar els punts de control per ajustar la forma de les corbes.

  5. Exporta a SVG:

    Quan hagis vectoritzat tot el circuit, ves a "Arxiu" > "Desar com a..." i selecciona el format SVG com a format de sortida. Assegura't de guardar-ho com a fitxer SVG.

Això convertirà la imatge PNG amb les trajectòries Bézier que has creat a Inkscape en un fitxer SVG que inclourà els paràmetres <path d> per a les corbes Bézier. Aquest SVG podràs utilitzar-lo en llocs web o altres aplicacions que suportin aquest format vectorial.

Tingues en compte que el procés de vectorització pot ser una mica laboriós, especialment si el circuit és complex, ja que has de definir manualment les trajectòries de les corbes de Bézier.

Exemples de dibuix i animació vectorial SVG per fer circuit de Fórmula 1 amb robot seguidor de línies


Dibuix vectorial SVG: exemples

Línia Recta

        
            <path d="M50 50 L150 150" stroke="black" stroke-width="2" fill="transparent" />
        
    

Comanda "L" crea una línia recta des de (50, 50) fins a (150, 150).

Corba Quadràtica

        
            <path d="M50 50 Q100 150 150 50" stroke="black" stroke-width="2" fill="transparent" />
        
    

Comanda "Q" crea una corba quadràtica amb un punt de control (100, 150).

Arc

        
            <path d="M50 50 A50 50 0 1 0 150 50" stroke="black" stroke-width="2" fill="transparent" />
        
    

Comanda "A" crea un arc el·líptic amb radi (50, 50) i sentit horari.

Línia Horitzontal Relativa

        
            <path d="M50 50 h100" stroke="black" stroke-width="2" fill="transparent" />
        
    

Comanda "h" crea una línia horitzontal relativa de 100 píxels.

Tancar Camí

        
            <path d="M50 50 L150 150 Z" stroke="black" stroke-width="2" fill="transparent" />
        
    

La comanda "Z" tanca el camí, connectant el punt actual amb el primer punt del camí.

Exemple de Corba Bèzier Cúbica Suau (S)

        
            <path d="M50 50 C100 150 150 50 200 100 S250 150 300 100" stroke="black" stroke-width="2" fill="transparent" />
        
    

La comanda "C" crea una corba de Bèzier cúbica amb punts de control (100, 150) i (150, 50), i la comanda "S" crea una corba de Bèzier cúbica suau que utilitza el punt de control anterior (150, 50) i un nou punt de control (250, 150).


Explicació de Corbes de Bèzier en SVG des del binomi de Newton passant pels polinomis de Bernstein

Relacionem el binomi de Newton, el polinomi de Bernstein i la corba de Bézier:

Binomi de Newton:

\( (a + b)^n = \sum_{i=0}^{n} \binom{n}{i} a^i b^{n-i}, \quad \binom{n}{i} = \frac{n!}{i!(n - i)!} \)

Polinomi de Bernstein:

\(B_n^i(t) = \binom{n}{i} t^i (1 - t)^{n-i}\)

Corba de Bézier:

Una corba de Bézier és una corba definida per un conjunt de punts de control. En el cas d'una corba de Bézier cúbica en dues dimensions, la seva equació pot ser expressada com:

\(P(t) = (1 - t)^3 P_0 + 3(1 - t)^2 t P_1 + 3(1 - t) t^2 P_2 + t^3 P_3\)

On \(P(t)\) és el punt a la corba en el paràmetre \(t\) i \(P_0, P_1, P_2\) i \(P_3\) són els punts de control.

Els polinomis de Bernstein estan relacionats amb les corbes de Bézier, ja que s'utilitzen per calcular les posicions intermèdies al llarg de la corba.

Per demostrar els polinomis de Bernstein a partir fel binomi de Newton prenem \(a = t\) i \(b = 1 - t\) en l'expressió anterior, obtenim:

\(1 = (t + 1 - t)^n = \sum_{i=0}^{n} B_n^i(t), \quad B_n^i(t) = \binom{n}{i} t^i (1 - t)^{n-i}\)

Per exemple, els polinomis de Bernstein de grau dos són:

\(B_2^0(t) = (1 - t)^2, \quad B_2^1(t) = 2t(1 - t), \quad B_2^2(t) = t^2\)

Aquests polinomis formen una base alternativa \(\{B_n^0(t), \ldots, B_n^n(t)\}\) dels polinomis de grau \(n\) o inferior en una variable \(t\), i, en comparació amb la base canònica, tenen l'avantatge de ser tots del mateix grau.

Les corbes de Bèzier són una classe de corbes matemàtiques que es fan servir comunament en gràfics vectorials per descriure trajectòries suaus i formes. En SVG (Scalable Vector Graphics), es fan servir principalment dos tipus de corbes de Bèzier: les corbes de Bèzier cúbiques i les corbes de Bèzier quadràtiques.

  1. Corbes de Bèzier Cúbiques:
  2. Corbes de Bèzier Quadràtiques:

Altres tipus de corbes que es poden utilitzar en SVG són les corbes el·líptiques (comanda "A" o "a"), que permeten crear arcs el·líptics o cercles, i les línies horitzontals (comanda "H" o "h") i verticals (comanda "V" o "v") per crear línies rectes sense la necessitat de definir punts de control. No obstant això, les corbes de Bèzier cúbiques i quadràtiques són les més utilitzades per la seva versatilitat i capacitat per descriure trajectòries i formes complexes en gràfics vectorials.



Animacions de corbes de Bézier


Aquestes animacions il·lustren com es construeix una corba de Bézier paramètrica. El paràmetre t oscil·la entre 0 i 1. En el cas més simple, una corba de Bézier de primer ordre, la corba és una línia recta entre els punts de control.

Per a una corba de Bézier de segon ordre (quadràtica), primer trobem dos punts intermedis que són anomenats t al llarg de les línies entre els tres punts de control. A continuació, tornem a realitzar el mateix pas d'interpolació i trobem un altre punt que estigui t al llarg de la línia entre aquests dos punts intermedis. Traçant aquest darrer punt s'obté una corba de Bézier quadràtica. Els mateixos passos es poden repetir per a altres ordres n superiors.

Una corba de Bézier d'ordre \(n\) es genera utilitzant una fórmula matemàtica que combina punts de control per definir la seva forma. Per a una corba de Bézier d'ordre \(n\), tenim \(n+1\) punts de control. La fórmula bàsica per a una corba de Bézier d'ordre \(n\) és:

\[ B(t) = \sum_{i=0}^{n} P(i) \cdot B(i, n, t) \]

On:

La funció de base de Bézier \(B(i, n, t)\) es calcula de la següent manera:

\[ B(i, n, t) = C(n, i) \cdot (1 - t)^{n - i} \cdot t^i \]

On:

Descripció de les coordenades de la corba

Dades les coordenades dels punts de control Pi: el primer punt de control té les coordenades P1 = (x1, y1), el segon: P2 = (x2, y2), i així successivament, les coordenades de la corba es descriuen mitjançant l'equació que depèn del paràmetre t del segment [0,1].

Fórmules per a la corba

La fórmula per a una corba de 2 punts:

\[ B (t) = (1-t)P_0 + tP_1 \]

Per a 3 punts de control:

\[ B (t) = (1−t)^2P_0 + 2(1−t)tP_1 + t^2P_2\]

Per a 4 punts de control:

\[ B (t) = (1−t)^3P_0 + 3(1−t)^2tP_1 +3(1−t)t^2P_2 + t^3P_3 \]

Aquestes són les equacions vectorials. En altres paraules, podem posar x i y en lloc de P per obtenir les coordenades corresponents.


Gràfic exemple de la corba de Bèzier

Punts de control:

\( P_0 = [1, 5] \), \( P_1 = [3, 1] \), \( P_2 = [7, 8] \)

Equacions:

La corba de Bèzier quadràtica es defineix amb les següents equacions:

\( B(t) = (1 - t)^2 \cdot P_0 + 2 \cdot t \cdot (1 - t) \cdot P_1 + t^2 \cdot P_2 \)

on:


Circuit exemple

        
            <!DOCTYPE html>
            <html lang="ca">
            <head>
                <meta charset="UTF-8">
                <title>Circuit de Fórmula 1</title>
            </head>
            <body>
                <svg width="800" height="600">
                    <!-- Carril del circuit de Fórmula 1 -->
                    <path id="circuit" d="M100 200 C100 100 300 100 300 200 C300 300 500 300 500 200 S700 100 700 200 L700 400 L100 400 Z" stroke="black" fill="transparent" stroke-width="10" />
                    
                    <!-- Bola groga que circula pel circuit -->
                    <circle id="bola" r="10" fill="yellow">
                        <animateMotion dur="5s" repeatCount="indefinite">
                            <mpath xlink:href="#circuit" />
                        </animateMotion>
                    </circle>
                </svg>
            </body>
            </html>
        
    

Aquest codi HTML crea un circuit de Fórmula 1 mitjançant SVG. Aquí tens una explicació detallada:



CIRCUIT DE MONTMELÓ



F1 Circuit Simulation









Montmeló Circuit



    <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>F1 Circuit Simulation</title>
    <style>
        /* Add your CSS styles here for better aesthetics */
    </style>
</head>
<body>
    <h1>F1 Circuit Simulation</h1>

    <!-- Circuit Selection Dropdown -->
    <label for="circuitSelect">Select Circuit:</label>
    <select id="circuitSelect" onchange="changeCircuit()">
        <option value="circuit1">Circuit 1</option>
        <option value="circuit2">Circuit 2</option>
    </select><br><br>

    <!-- Customizable Race Settings for Circuit 1 -->
    <div id="customizableSettingsCircuit1">
        <label for="lapDuration1">Lap Duration (seconds):</label>
        <input type="number" id="lapDuration1" min="1" value="5"><br>

        <label for="numLaps1">Number of Laps:</label>
        <input type="number" id="numLaps1" min="1" value="10"><br>

        <label for="yellowColor1">Yellow Ball 1 Color:</label>
        <input type="color" id="yellowColor1" value="#FFFF00"><br>

        <label for="greenColor1">Green Ball 2 Color:</label>
        <input type="color" id="greenColor1" value="#008000"><br>
    </div>

    <!-- Customizable Race Settings for Circuit 2 -->
    <div id="customizableSettingsCircuit2" style="display: none;">
        <label for="lapDuration2">Lap Duration (seconds):</label>
        <input type="number" id="lapDuration2" min="1" value="5"><br>

        <label for="numLaps2">Number of Laps:</label>
        <input type="number" id="numLaps2" min="1" value="10"><br>

        <label for="yellowColor2">Yellow Ball 1 Color:</label>
        <input type="color" id="yellowColor2" value="#FFFF00"><br>

        <label for="greenColor2">Green Ball 2 Color:</label>
        <input type="color" id="greenColor2" value="#008000"><br>
    </div>

    <button id="startButton" onclick="startRace()">Start Race</button>
    <button id="stopButton" onclick="stopRace()" disabled>Stop Race</button><br><br>

    <!-- SVG elements for Circuit 1 -->
    <svg width="1500" height="600" id="svgCircuit1" style="display: block;">
        <!-- Circuit 1 Path -->
        <path id="circuit1" d="M100 250 C100 150 300 150 300 250 C300 350 500 350 500 250 C500 150 700 150 700 250 C700 350 900 350 900 250 L900 450 L100 450 Z" stroke="black" fill="transparent" stroke-width="10" />
        <text x="100" y="500" font-size="20" fill="black">Montmeló Circuit</text>
        

        <!-- Yellow Ball 1 for Circuit 1 -->
        <circle id="bola1_1" r="10" fill="yellow">
            <animateMotion id="animateYellow1_1" dur="5s" repeatCount="indefinite" begin="animating.begin" fill="freeze">
                <mpath xlink:href="#circuit1" />
            </animateMotion>
        </circle>

        <!-- Green Ball 2 for Circuit 1 -->
        <circle id="bola1_2" r="10" fill="green">
            <animateMotion id="animateGreen1_2" dur="10s" repeatCount="indefinite" begin="animating.begin" fill="freeze">
                <mpath xlink:href="#circuit1" />
            </animateMotion>
        </circle>
    </svg>

    <!-- SVG elements for Circuit 2 -->
    <svg width="1500" height="600" id="svgCircuit2" style="display: none;">
        <!-- Circuit 2 Path -->
        <path id="circuit2" d="M100 200 C100 100 300 100 300 200 C300 300 500 300 500 200 S700 100 700 200 L700 400 L100 400 Z" stroke="black" fill="transparent" stroke-width="10" />
<text x="100" y="500" font-size="20" fill="black">Monza Circuit</text>

        <!-- Yellow Ball 1 for Circuit 2 -->
        <circle id="bola2_1" r="10" fill="yellow">
            <animateMotion id="animateYellow2_1" dur="5s" repeatCount="indefinite" begin="animating.begin" fill="freeze">
                <mpath xlink:href="#circuit2" />
            </animateMotion>
        </circle>
        <!-- Green Ball 2 for Circuit 2 -->
        <circle id="bola2_2" r="10" fill="green">
            <animateMotion id="animateGreen2_2" dur="10s" repeatCount="indefinite" begin="animating.begin" fill="freeze">
                <mpath xlink:href="#circuit2" />
            </animateMotion>
        </circle>
    </svg>

    <br><br>
    <div id="lapCounter"></div>
    <div id="lapTime"></div>

    <script>
        var lapCounter1 = 0;
        var lapCounter2 = 0;
        var lapTime = 0;
        var lapInterval;
   
        // Initial visibility based on the selected circuit
        var selectedCircuit = document.getElementById("circuitSelect").value;
        if (selectedCircuit === "circuit1") {
            document.getElementById("svgCircuit1").style.display = "block";
            document.getElementById("svgCircuit2").style.display = "none";
        } else if (selectedCircuit === "circuit2") {
            document.getElementById("svgCircuit1").style.display = "none";
            document.getElementById("svgCircuit2").style.display = "block";
        }

        // Show/hide circuits based on the selected circuit
        function changeCircuit() {
            var selectedCircuit = document.getElementById("circuitSelect").value;

            if (selectedCircuit === "circuit1") {
                document.getElementById("svgCircuit1").style.display = "block";
                document.getElementById("svgCircuit2").style.display = "none";
            } else if (selectedCircuit === "circuit2") {
                document.getElementById("svgCircuit1").style.display = "none";
                document.getElementById("svgCircuit2").style.display = "block";
            }
        }
        
function startRace() {
            // Read selected circuit
            var selectedCircuit = document.getElementById("circuitSelect").value;

            // Read customizable settings for the selected circuit
            var lapDuration, numLaps, yellowColor, greenColor;
            if (selectedCircuit === "circuit1") {
                lapDuration = parseInt(document.getElementById("lapDuration1").value, 10);
                numLaps = parseInt(document.getElementById("numLaps1").value, 10);
                yellowColor = document.getElementById("yellowColor1").value;
                greenColor = document.getElementById("greenColor1").value;
            } else if (selectedCircuit === "circuit2") {
                lapDuration = parseInt(document.getElementById("lapDuration2").value, 10);
                numLaps = parseInt(document.getElementById("numLaps2").value, 10);
                yellowColor = document.getElementById("yellowColor2").value;
                greenColor = document.getElementById("greenColor2").value;
            }

            // Set animation durations based on selected circuit
            var yellowBallDuration = selectedCircuit === "circuit1" ? lapDuration : lapDuration * 2;
            var greenBallDuration = selectedCircuit === "circuit1" ? lapDuration * 2 : lapDuration;
            document.getElementById("animateYellow1_1").setAttribute("dur", yellowBallDuration + "s");
            document.getElementById("animateGreen1_2").setAttribute("dur", greenBallDuration + "s");
            document.getElementById("animateYellow2_1").setAttribute("dur", yellowBallDuration + "s");
            document.getElementById("animateGreen2_2").setAttribute("dur", greenBallDuration + "s");

            // Update lap counters
            lapCounter1 = 0;
            lapCounter2 = 0;
            lapTime = 0;
            document.getElementById("lapCounter").innerHTML = "Number of laps - Circuit 1: " + lapCounter1 + ", Circuit 2: " + lapCounter2;
            document.getElementById("lapTime").innerHTML = "Lap time: " + formatTime(lapTime);
            document.getElementById("startButton").disabled = true;
            document.getElementById("stopButton").disabled = false;

            // Apply ball colors
            document.getElementById("bola1_1").setAttribute("fill", yellowColor);
            document.getElementById("bola1_2").setAttribute("fill", greenColor);
            document.getElementById("bola2_1").setAttribute("fill", yellowColor);
            document.getElementById("bola2_2").setAttribute("fill", greenColor);

            // Show/hide selected circuit's SVG
            if (selectedCircuit === "circuit1") {
                document.getElementById("svgCircuit1").style.display = "block";
                document.getElementById("svgCircuit2").style.display = "none";
            } else if (selectedCircuit === "circuit2") {
                document.getElementById("svgCircuit1").style.display = "none";
                document.getElementById("svgCircuit2").style.display = "block";
            }

            // Start animations
            document.getElementById("animateYellow1_1").beginElement();
            document.getElementById("animateGreen1_2").beginElement();
            document.getElementById("animateYellow2_1").beginElement();
            document.getElementById("animateGreen2_2").beginElement();

            lapInterval = setInterval(function () {
                lapTime++;
                document.getElementById("lapTime").innerHTML = "Lap time: " + formatTime(lapTime);

                // Check if the balls have completed laps
                if (lapTime % lapDuration === 0) {
                    lapCounter1++;
                }
                if (lapTime % (lapDuration * 2) === 0) {
                    lapCounter2++;
                }

                document.getElementById("lapCounter").innerHTML = "Number of laps - Circuit 1: " + lapCounter1 + ", Circuit 2: " + lapCounter2;

                // Check if the race is complete
                if (lapCounter1 >= numLaps || lapCounter2 >= numLaps) {
                    stopRace();
                }
            }, 1000);
        }

        function stopRace() {
            clearInterval(lapInterval);
            document.getElementById("startButton").disabled = false;
            document.getElementById("stopButton").disabled = true;

            // Stop animations
            document.getElementById("animateYellow1_1").endElement();
            document.getElementById("animateGreen1_2").endElement();
            document.getElementById("animateYellow2_1").endElement();
            document.getElementById("animateGreen2_2").endElement();
        }

        function formatTime(time) {
            var minutes = Math.floor(time / 60);
            var seconds = time % 60;
            return minutes + "m " + seconds + "s";
        }

    </script>
    
</body>
</html>
    

El codi font anterior que dona el simulador de F1 passo a explicar les funcions JavaScript, els botons i el selector pas a pas:

1. Funcions JavaScript:

2. Botons:

Start Race Button (<button id="startButton" onclick="startRace()">Start Race</button>)

Quan es fa clic en aquest botó, es crida la funció startRace(), que inicia la simulació de la cursa.

Stop Race Button (<button id="stopButton" onclick="stopRace()" disabled>Stop Race</button>)

Quan es fa clic en aquest botó, es crida la funció stopRace(), que atura la simulació de la cursa. Aquest botó s'habilita només quan la cursa està en marxa.

3. Selector de Circuits i Colors:

<select id="circuitSelect" onchange="changeCircuit()">: Aquest és un desplegable HTML que permet als usuaris seleccionar un dels dos circuits. Quan es selecciona un circuit diferent, es crida la funció changeCircuit(). Aquesta funció canvia la visualització dels circuits SVG per mostrar el circuit seleccionat i amagar l'altre. Això permet als usuaris canviar entre els circuits abans d'iniciar la cursa.

Per personalitzar el color de les boles en la simulació de la cursa, utilitzem un selector de color HTML.

Aquest selector de color es basa en l'ús de l'element <input> amb l'atribut "type" establert com a "color".

A continuació, es mostra com es crea l'element d'entrada de selecció de color per a la bola groga:


<label for="yellowColor1">Yellow Ball 1 Color:</label>
<input type="color" id="yellowColor1" value="#FFFF00">

En aquest codi, tenim un etiqueta que descriu la selecció de color (en aquest cas, "Yellow Ball 1 Color").

L'element <input> té l'atribut "type" definit com a "color" i un identificador únic ("id" com "yellowColor1"). També pot tenir un valor inicial de color (en aquest cas, el groc "#FFFF00").

Quan l'usuari fa clic a aquest element d'entrada de selecció de color, es mostra una finestra emergent amb una paleta de colors, i l'usuari pot triar un color fent clic a qualsevol de les opcions disponibles.