Apuntes mientras aprendo sobre software y computadoras.

programación

Cómo estudiar programación orientada a objetos mientras creamos motion graphics en Processing

En este apunte de título más complicado de lo necesario vamos a revisar cómo estudiar programación orientada a objetos mientras creamos motion graphics en Processing.

Para ejemplificar la explicación vamos a utilizar Processing, un entorno de desarrollo que funciona muy bien para crear imágenes.

Ya escribí en otra oportunidad sobre Processing, por lo que si te interesa aprender más este es el enlace a ese otro apunte.

Vale mencionar que Processing utiliza el lenguaje “Java”, que es un lenguaje de programación orientado a objetos. Python es otro lenguaje orientado a objetos que es muy famoso.

¿Cuál es el objetivo de la entrada? Vamos a crear un sencillo video de “gráficos en movimiento” utilizando código en Processing, y esos gráficos van a tomar el lugar de clases y objetos en la ya tan mencionada “programación orientada a objetos”.

Una explicación sin explicación

Este tema que nos entretiene puede ser comprendido por una serie de inteligentes explicaciones graciosamente desarrolladas. Lamentablemente esas explicaciones de ser escritas por alguien con poca experiencia pueden resultar en algo muy confuso y aburrido. Soy una persona con poca experiencia. ¿Es este el final de este apunte?

No, este no es el final de este texto. El apunte sigue adelante.

Tengo que decir que estamos de suerte, porque no tengo muchas ganas de escribir y no estoy seguro de saber lo suficiente como para poder escribir una explicación coherente.

Lo que voy a hacer en lugar de todo eso es crear una explicación que se construye con una serie de ejemplos de código que sumados en conjunto van a funcionar como una especie de explicación completa.

Para ser honesto, la mayoría de las cosas se explican menor cuándo las ponemos en práctica. Y este es uno de esos temas que se explican mejor en la forma de una epifanía. En un momento no sabemos lo que es la programación orientada a objetos porque no necesitamos de ella, y al otro encontramos una oportunidad para usar sus ventajas y tenemos una relevación que explica qué es y para qué sirve.

Entonces advierto que esta no va a ser la explicación que mejor se ajuste a los conceptos teóricos contemporáneos. Pero al terminar de leerla vas a encontrar que pudiste utilizar la idea para crear algo valioso, algo que luego vas a poder seguir utilizando en otro tipo de proyectos.

Entonces sin más excusas…

Cómo crear un sencillo separador de video usando motion graphics en Processing

Processing nos sirve para crear imágenes y videos. En esta oportunidad me interesa que hagamos un sencillo video, un video que puede servir como una presentación para un proyecto (digamos para un proyecto en las redes) o como un separador por ejemplo para un video más grande.

El video va a estar compuesto por una serie de placas de colores que suben y bajan en forma de cortina, descubriendo el título del proyecto.

Paso 1: un proyecto, muchos capas de color

Lo primero que vamos a hacer es crear un sketch con las dimensiones de la pantalla.

Siendo que queremos hacer un video, vamos a crear la pantalla
en tamaño HD (1280 x 720) y vamos a dejarla de color negro. Por supuesto es posible ajustar estos parámetros según lo que necesites en el momento.

Acto seguido vamos a crear una nueva “capa” por llamarla de algún modo, un color solido naranja que cubra toda la pantalla.

El primer código nos queda de esta forma:

void setup() {
  size(1280, 720);
  background(0);
}

void draw() {
  fill(255, 128, 0); // Color naranja
  rect(0, 0, width, height); // Rectangulo que cubre la pantalla
}

Por ahora nuestro primer rectángulo creado con la función “rect()” tiene algunas características definidas. Por ejemplo tiene un color con la notación (255, 128, 0).

Y el rectángulo tiene una posición en la pantalla determinada por (0,0). Esto quiere decir que la parte superior izquierda del rectángulo coincide con la coordenada del ángulo superior izquierdo de la pantalla. Esto es en la forma (x, y), de una forma similar a la de un plano cartesiano.

Los parámetros “width, height” definen el ancho y el alto del rectángulo. Escritos de esta forma indican que nuestra capa tiene que copiar el tamaño de la pantalla.

Este rectángulo naranja es el primero de los que vamos a usar para crear nuestra cortina animada de motion graphics.

¿Por qué no agregar algunos nuevos colores?

Revisemos este nuevo ejemplo de código:

void setup() {
  size(1280, 720);
  background(0);
}

void draw() {
  fill(255, 128, 0); // Color naranja
  rect(0, 0, width, height); //  Posición del color naranja
  
  fill(0, 0, 255); // Color Azul
  rect(0, 0, width, height); // Posición del azul
  
  fill(255, 0, 0); // Color rojo
  rect(0, 0, width, height); // Datos de posición rojo
  
  fill(0, 255, 0); // Color Verde
  rect(0, 0, width, height); // Posición del color verde
}

Ahora tenemos cuatro rectángulos en total. Pero tenemos que notar algunas cosas:

1- Cada capa se encuentra por debajo de la anterior. La última de la lista es que primero aparece, por eso si ejecutamos el código vamos a ver la pantalla de color verde. Y si borramos la capa verde, va a aparecer la siguiente de color rojo y así sucesivamente.

2- Las capas son difíciles de identificar. Las capas no tienen nombres que las separen, y lo único que nos dice fácilmente lo que hace cada na es el comentario con el que estamos acompañándolas.

3- Las capas todavía no se mueven lo que hace a un video bastante aburrido.

¿Cómo podemos solucionar todo esto? ¿Cómo podemos organizar mejor el proyecto? Aquí es donde entra el momento de la programación orientada a objetos.

Paso 2: una nueva clase de rectángulo

Revisemos este nuevo código, expandiendo nuestro anterior ejemplo:

class Rectangulo {
  int x;
  int y;
  int width;
  int height;
  color fillColor;
  
  Rectangulo(int _x, int _y, int _width, int _height, color _fillColor) {
    x = _x;
    y = _y;
    width = _width;
    height = _height;
    fillColor = _fillColor;
  }
  
  void draw() {
    fill(fillColor);
    rect(x, y, width, height);
  }
  
}

Rectangulo naranjaCapa;
Rectangulo azulCapa;
Rectangulo rojoCapa;
Rectangulo verdeCapa;


void setup() {
  size(1280, 720);
  
  // Crear objetos de la clase Rectangulo
  naranjaCapa = new Rectangulo(0, 0, width, height, color(255, 165, 0));
  azulCapa = new Rectangulo(0, 0, width, height, color(0, 0, 255));
  rojoCapa = new Rectangulo(0, 0, width, height, color(255, 0, 0));
  verdeCapa = new Rectangulo(0, 0, width, height, color(0, 255, 0));
  
}

void draw() {
  background(0);
   
  naranjaCapa.draw();
  azulCapa.draw();
  rojoCapa.draw();
  verdeCapa.draw();
 
}

Bueno, en un primer momento todo este código parece mucho más complejo que el primero que estuvimos revisando. Pero si lo ejecutamos vamos a notar que tenemos el mismo resultado. Una capa verde cubre la pantalla.

Sin embargo es en este punto dónde empezamos a descubrir la idea de la programación orientada a objetos.

Antes necesitábamos crear cada rectángulo por separado, de esta forma:

fill(0, 0, 255); // Color Azul
rect(0, 0, width, height); // Posición del azul

Pero ahora estamos creando una clase o “class”.

Al definir una clase podemos declarar las características, los datos y las funciones que a nuestro parecer tiene que tener un rectángulo. Se trata del “plano” del que luego vamos a construir los objetos.

Pero podríamos definir clases para cualquier tipo de objeto que utilicemos en el futuro, coincide en este caso que nuestra primera clase podemos entenderla de forma gráfica como una figura geométrica.

Realmente una clase puede representar cualquier cosa, como por ejemplo un concepto dentro de una base de datos. Pero volvamos a nuestra clase rectángulo.

class Rectangulo {
  int x;
  int y;
  int width;
  int height;
  color fillColor;
  
  Rectangulo(int _x, int _y, int _width, int _height, color _fillColor) {
    x = _x;
    y = _y;
    width = _width;
    height = _height;
    fillColor = _fillColor;
  }
  
  void draw() {
    fill(fillColor);
    rect(x, y, width, height);
  }
  
}

El manual de referencias de Processing dice que el nombre de una clase es escrito en Mayúscula, para diferenciarla de otras variables. En nuestro
caso lo definimos como “class Rectangulo”.

Luego definimos las propiedades del rectángulo, por ejemplo que su alto y ancho son números enteros:

int x;
int y;
int width;
int height;
color fillColor;

Acto seguido construimos la forma que va a tener el nuevo objeto cuando llamemos al objeto:

Rectangulo(int _x, int _y, int _width, int _height, color _fillColor) {
x = _x;
y = _y;
width = _width;
height = _height;
fillColor = _fillColor;
}

Y luego creamos un método, una función que es parte de esta clase:

void draw() {
fill(fillColor);
rect(x, y, width, height);
}

Esa función define la forma en que el rectángulo va a dibujarse en la pantalla.

Con eso terminamos de crear la clase, que podemos considerar como el plano o la serie de instrucciones que definen nuestro objeto. Recordemos que en este caso estamos definiendo las características que queremos agregarle a la clase rectángulo.

¿Dónde empezamos a utilizar a los objetos en todo esto?

Ahora dentro del código principal le asignamos un nombre diferente a cada capa como variable, y creamos nuevos objetos a partir de la serie de características que agregamos en la clase:

Rectangulo naranjaCapa;
Rectangulo azulCapa;
Rectangulo rojoCapa;
Rectangulo verdeCapa;


void setup() {
  size(1280, 720);
  
  // Crear objetos de la clase Rectangulo
  naranjaCapa = new Rectangulo(0, 0, width, height, color(255, 165, 0));
  azulCapa = new Rectangulo(0, 0, width, height, color(0, 0, 255));
  rojoCapa = new Rectangulo(0, 0, width, height, color(255, 0, 0));
  verdeCapa = new Rectangulo(0, 0, width, height, color(0, 255, 0));
  
}

Esto nos permite que cada capa de rectángulo pueda manipularse por separado.

Los objetos en movimiento

Ahora queremos pasar a la parte de “movimiento” de “gráficos en movimiento”.

Esta es la nueva versión de nuestro código:

class Rectangulo {
  int x;
  int y;
  int width;
  int height;
  color fillColor;
  float velocidad;
  
  Rectangulo(int _x, int _y, int _width, int _height, color _fillColor, float _velocidad) {
    x = _x;
    y = _y;
    width = _width;
    height = _height;
    fillColor = _fillColor;
    velocidad = _velocidad;
  }
  
  void draw() {
    fill(fillColor);
    rect(x, y, width, height);
  }
  
  void update() {
    y += velocidad;
  }
}

Rectangulo naranjaCapa;
Rectangulo azulCapa;
Rectangulo rojoCapa;
Rectangulo verdeCapa;


void setup() {
  size(1280, 720);
  
  // Crear objetos de la clase Rectangulo
  naranjaCapa = new Rectangulo(0, 0, width, height, color(255, 165, 0), 4.0);
  azulCapa = new Rectangulo(0, 0, width, height, color(0, 0, 255), -6.0);
  rojoCapa = new Rectangulo(0, 0, width, height, color(255, 0, 0), 9.0);
  verdeCapa = new Rectangulo(0, 0, width, height, color(0, 255, 0), 12.0);
  
}

void draw() {
  background(0);
  
  naranjaCapa.update();
  azulCapa.update();
  rojoCapa.update();
  verdeCapa.update();
   
  naranjaCapa.draw();
  azulCapa.draw();
  rojoCapa.draw();
  verdeCapa.draw();
 
}

Lo que hicimos fue agregar una nueva variable llamada velocidad. Luego la variable de posición “Y” se modifica cada vez que un nuevo cuadro es dibujado:

Esto quiere decir que si la variable velocidad es “2.0”, cada vez que un nuevo plano es dibujado el rectángulo se mueve dos puntos hacia abajo en el eje “Y”. Por otra parte si la velocidad fuera “-2.0” el rectángulo se va a mover dos posiciones hacia arriba cada vez que es dibujado.

Con esto conseguimos crear un efecto de cortina que “sube y baja” que puede servirnos para presentar un proyecto.

Más objetos en movimiento

Por supuesto también podemos crear una nueva serie de objetos en base a la clase que creamos antes. Esta nueva serie de rectángulos va a ser llamada luego, para cerrar la introducción.

El código nos queda de esta forma:

// Este código de Processing sube y baja rectángulos de colores en forma de cortina 

class Rectangulo {
  int x;
  int y;
  int width;
  int height;
  color fillColor;
  float velocidad;

  Rectangulo(int _x, int _y, int _width, int _height, color _fillColor, float _velocidad) {
    x = _x;
    y = _y;
    width = _width;
    height = _height;
    fillColor = _fillColor;
    velocidad = _velocidad;
  }

  void draw() {
    fill(fillColor);
    rect(x, y, width, height);
  }

  void update() {
    y += velocidad;
  }
}

Rectangulo naranjaCapa;
Rectangulo azulCapa;
Rectangulo rojoCapa;
Rectangulo verdeCapa;

Rectangulo amarilloCapa;
Rectangulo cyanCapa;
Rectangulo extra_naranjaCapa;


int startTime; // Variable para el tiempo de comienzo

void setup() {
  size(1280, 720);

  naranjaCapa = new Rectangulo(0, 0, width, height, color(255, 165, 0), 4.0);
  azulCapa = new Rectangulo(0, 0, width, height, color(0, 0, 255), -6.0);
  rojoCapa = new Rectangulo(0, 0, width, height, color(255, 0, 0), 9.0);
  verdeCapa = new Rectangulo(0, 0, width, height, color(0, 255, 0), 12.0);
  
  amarilloCapa = new Rectangulo(0, 720, width, height, color(255, 255, 50), -11.0);
  cyanCapa = new Rectangulo(0, 720, width, height, color(0, 255, 255), -8.0);
  extra_naranjaCapa = new Rectangulo(0, 720, width, height, color(245, 145, 0), -6.0);

  
  // Comenzar a guardar la variable de tiempo
  startTime = millis();
}

void draw() {
  background(0);

  naranjaCapa.update();
  azulCapa.update();
  rojoCapa.update();
  verdeCapa.update();


  if (millis() - startTime >= 7000) {
 
    amarilloCapa.update();
    amarilloCapa.draw();  
    cyanCapa.update(); 
    cyanCapa.draw();   
    extra_naranjaCapa.update(); 
    extra_naranjaCapa.draw();   

  }

  naranjaCapa.draw();
  azulCapa.draw();
  rojoCapa.draw();
  verdeCapa.draw();
}

Haciendo esto le sacamos un poco más de jugo a nuestra clase de Rectángulos, agregando nuevos objetos.

Solamente nos falta agregarle algo de texto al sketch para cumplir nuestro objetivo de un sencillo separador en video de motion graphics.

Un objeto de texto

Esta es casi la última revisión del código:

// Este código de Processing sube y baja rectángulos de colores en forma de cortina 

class Titulo {
  float x, y;
  String contenido;
  float fade = 255;

  Titulo(float x, float y, String contenido) {
    this.x = x;
    this.y = y;
    this.contenido = contenido;
  }

  void display() {
    fill(255, fade);
    textSize(70);
    textAlign(CENTER, CENTER);
    text(contenido, x, y);
    fade -= 0.5;
    if (fade <= 0) {
      fade = 0;
    }
  }
}

class Rectangulo {
  int x;
  int y;
  int width;
  int height;
  color fillColor;
  float velocidad;

  Rectangulo(int _x, int _y, int _width, int _height, color _fillColor, float _velocidad) {
    x = _x;
    y = _y;
    width = _width;
    height = _height;
    fillColor = _fillColor;
    velocidad = _velocidad;
  }

  void draw() {
    fill(fillColor);
    rect(x, y, width, height);
  }

  void update() {
    y += velocidad;
  }
}

Titulo miFrase;

Rectangulo naranjaCapa;
Rectangulo azulCapa;
Rectangulo rojoCapa;
Rectangulo verdeCapa;

Rectangulo amarilloCapa;
Rectangulo cyanCapa;
Rectangulo extra_naranjaCapa;


int startTime; // Variable para el tiempo de comienzo

void setup() {
  size(1280, 720);
  
  miFrase = new Titulo(width/2, height/2, "TÍTULO DEL PROYECTO");
  
  naranjaCapa = new Rectangulo(0, 0, width, height, color(255, 165, 0), 4.0);
  azulCapa = new Rectangulo(0, 0, width, height, color(0, 0, 255), -6.0);
  rojoCapa = new Rectangulo(0, 0, width, height, color(255, 0, 0), 9.0);
  verdeCapa = new Rectangulo(0, 0, width, height, color(0, 255, 0), 12.0);
  
  amarilloCapa = new Rectangulo(0, 720, width, height, color(255, 255, 50), -11.0);
  cyanCapa = new Rectangulo(0, 720, width, height, color(0, 255, 255), -8.0);
  extra_naranjaCapa = new Rectangulo(0, 720, width, height, color(245, 145, 0), -6.0);
  
  // Comenzar a guardar la variable de tiempo
  startTime = millis();
}

void draw() {
  background(0);
  
  
  miFrase.display();
  
  naranjaCapa.update();
  azulCapa.update();
  rojoCapa.update();
  verdeCapa.update();


  if (millis() - startTime >= 7000) {
 
    amarilloCapa.update();
    amarilloCapa.draw();  
    cyanCapa.update(); 
    cyanCapa.draw();   
    extra_naranjaCapa.update(); 
    extra_naranjaCapa.draw();   

  }

  naranjaCapa.draw();
  azulCapa.draw();
  rojoCapa.draw();
  verdeCapa.draw();
}

De la misma forma que habíamos hecho antes, ahora creamos una nueva clase llamada “Titulo”.

class Titulo {
  float x, y;
  String contenido;
  float fade = 255;

  Titulo(float x, float y, String contenido) {
    this.x = x;
    this.y = y;
    this.contenido = contenido;
  }

  void display() {
    fill(255, fade);
    textSize(70);
    textAlign(CENTER, CENTER);
    text(contenido, x, y);
    fade -= 0.5;
    if (fade <= 0) {
      fade = 0;
    }
  }
}

Entre las características que le dimos a esa clase está la habilidad de desvanecer el texto poco a poco, registrando el cambio de forma similar a la forma en que fuimos cambiando la posición de los rectángulos en el espacio.

Con el título agregado el separador de video toma una forma. Pero lo importante es poder seguir utilizando esta herramienta de clases y objetos para nuevos proyectos de programación.

Una clase de una clase: herencias en los objetos

Otra de las características importantes de la programación orientada a objetos es la posibilidad de reutilizar las clases por medio de hacer una inheritance o una “herencia”.

En nuestro código tenemos la siguiente clase llamada “Rectángulo:”

class Rectangulo {
  int x;
  int y;
  int width;
  int height;
  color fillColor;
  float velocidad;

  Rectangulo(int _x, int _y, int _width, int _height, color _fillColor, float _velocidad) {
    x = _x;
    y = _y;
    width = _width;
    height = _height;
    fillColor = _fillColor;
    velocidad = _velocidad;
  }

  void draw() {
    fill(fillColor);
    rect(x, y, width, height);
  }

  void update() {
    y += velocidad;
  }
}

Si lo necesitamos podemos crear lo que puede definirse como una clase diferente a partir de una clase ya existente. El resultado va a ser una nueva clase, que referencia datos de la clase a la que hereda.

Las dos clases van a convivir al mismo tiempo, y eso nos permite ahorrarnos repetir información. Los detalles particulares sobre cómo se maneja este asunto de inheritance, y todo el asunto de clases y objetos en general, depende de cada programa. Pero sigamos con la parte práctica, dedicada a Processing y Java.

A partir de la clase “Rectangulo” voy a crear una nueva clase llamada “Cierre”:

class Cierre extends Rectangulo {
  float velocidadY;
  boolean stopYMovement; // Variable que decide si el rectangulo se mueve o no

  Cierre(int _x, int _y, int _width, int _height, color _fillColor, float _velocidadY) {
    super(_x, _y, _width, _height, _fillColor, 2.0);
    velocidadY = _velocidadY;
  }

  void update() {
    if (!stopYMovement) {
        y += velocidadY; // Modifica la velocidad revisando el eje y

        // Revisa si el eje y llega a 0
        if (y <= 0) {
            y = 0;                // Detiene el movimiento en posición cero
                   }
    }

    super.update(); // Actualiza desde la calse heredada. 
}

}

Podemos leer que dice que “la clase “Cierre” extiende la clase “Rectangulo””.

Nuestra nueva clase cierre retiene las mismas características que antes formaban nuestras capas rectangulares de colores. La diferencia es que en
esta nueva versión heredada la forma de revisar la posición en el eje de “Y” es diferente. Siendo que vamos a usar esta imagen de cierre para nuestro video, queremos que la capa deje de moverse al llegar a la posición cero.

De esta forma vamos a tener una capa solida que nos va a permitir generar un video Gif animado que hace un bucle sin que podamos notar dónde empieza o termina.

El código completo es el siguiente:

// Este código de Processing sube y baja rectángulos de colores en forma de cortina 

class Titulo {
  float x, y;
  String contenido;
  float fade = 255;

  Titulo(float x, float y, String contenido) {
    this.x = x;
    this.y = y;
    this.contenido = contenido;
  }

  void display() {
    fill(255, fade);
    textSize(70);
    textAlign(CENTER, CENTER);
    text(contenido, x, y);
    fade -= 0.5;
    if (fade <= 0) {
      fade = 0;
    }
  }
}

class Rectangulo {
  int x;
  int y;
  int width;
  int height;
  color fillColor;
  float velocidad;

  Rectangulo(int _x, int _y, int _width, int _height, color _fillColor, float _velocidad) {
    x = _x;
    y = _y;
    width = _width;
    height = _height;
    fillColor = _fillColor;
    velocidad = _velocidad;
  }

  void draw() {
    fill(fillColor);
    rect(x, y, width, height);
  }

  void update() {
    y += velocidad;
  }
}

class Cierre extends Rectangulo {
  float velocidadY;
  boolean stopYMovement; // Variable que decide si el rectangulo se mueve o no

  Cierre(int _x, int _y, int _width, int _height, color _fillColor, float _velocidadY) {
    super(_x, _y, _width, _height, _fillColor, 2.0);
    velocidadY = _velocidadY;
  }

  void update() {
    if (!stopYMovement) {
        y += velocidadY; // Modifica la velocidad revisando el eje y

        // Revisa si el eje y llega a 0
        if (y <= 0) {
            y = 0;                // Detiene el movimiento en posición cero
                   }
    }

    super.update(); // Actualiza desde la calse heredada. 
}

}

Titulo miFrase;

Rectangulo naranjaCapa;
Rectangulo azulCapa;
Rectangulo rojoCapa;
Rectangulo verdeCapa;

Rectangulo amarilloCapa;
Rectangulo cyanCapa;
Rectangulo extra_naranjaCapa;

Cierre nuevoVerdeCapa;


void setup() {
  size(1280, 720);
  
  miFrase = new Titulo(width/2, height/2, "TÍTULO DEL PROYECTO");
  
  naranjaCapa = new Rectangulo(0, 0, width, height, color(255, 165, 0), 4.0);
  azulCapa = new Rectangulo(0, 0, width, height, color(0, 0, 255), -6.0);
  rojoCapa = new Rectangulo(0, 0, width, height, color(255, 0, 0), 9.0);
  verdeCapa = new Rectangulo(0, 0, width, height, color(0, 255, 0), 12.0);
  
  amarilloCapa = new Rectangulo(0, 720, width, height, color(255, 255, 50), -12.0);
  cyanCapa = new Rectangulo(0, 720, width, height, color(0, 255, 255), -11.0);
  extra_naranjaCapa = new Rectangulo(0, 720, width, height, color(245, 145, 0), -10.0);
  
  nuevoVerdeCapa = new Cierre(0, 720, width, height, color(0, 255, 0), -9.0);

}

int targetFrames = 300;

void draw() {
  background(0);
  
  miFrase.display();
  
  naranjaCapa.update();
  azulCapa.update();
  rojoCapa.update();
  verdeCapa.update();

  naranjaCapa.draw();
  azulCapa.draw();
  rojoCapa.draw();
  verdeCapa.draw();

  if (frameCount >= targetFrames) {
 
    amarilloCapa.update();
    amarilloCapa.draw();  
    cyanCapa.update(); 
    cyanCapa.draw();   
    extra_naranjaCapa.update(); 
    extra_naranjaCapa.draw();   
    nuevoVerdeCapa.update(); 
    nuevoVerdeCapa.draw();  

  }
  
// saveFrame("frames/frame-####.png");
  
}

Conclusión

Con esto le doy un cierre a este apunte dónde intenté explicar cómo estudiar programación orientada a objetos mientras creamos motion graphics en Processing.


Hice todo lo posible por hacer el texto lo más breve posible, sin omitir detalles de lo que entiendo del tema.

¿Hay algo que pueda mejorar de mi explicación? ¿Encontraste algún error en los ejemplos? Me gustaría conocer tu opinion en los comentarios, o por medio de un correo electrónico en mi dirección que se encuentra en el menú de “contacto”.

La seguimos en el próximo apunte.