Implementación de Pinceles Creativos para un Lienzo Digital

Este artículo profundiza en la implementación de pinceles creativos para una aplicación de lienzo digital de código abierto. El objetivo es mejorar la experiencia de usuario orfeciendo efectos de pintura únicos tanto en entornos de escritorio como móviles. La aplicación ya incluye una variedad de funciones de asistencia para el dibujo, como deshacer/rehacer, copiar/eliminar, importar/exportar, múltiples lienzos y capas.

Este es el tercer artículo de una serie que detalla la creación de estos pinceles. El código fuente completo está disponible en GitHub.

Pincel de Puntos Múltiples

Este efecto se logra generando puntos aleatorios arlededor de la posición actual del cursor durante el movimiento del ratón. Luego, se dibujan círculos en estas posiciones aleatorias. Cada nuevo conjunto de puntos se conecta al conjunto anterior con líneas, creando un efecto de conexión entre puntos dispersos.


interface Coordinates {
 x: number;
 y: number;
}

let drawingActive = false;
let previousPoints: Coordinates[] = []; // Almacena los puntos del círculo dibujado previamente

/**
* Genera coordenadas aleatorias dentro de un área definida.
* @param centerX - Coordenada X del centro del área.
* @param centerY - Coordenada Y del centro del área.
* @param areaSize - Dimensión del área cuadrada.
* @param numberOfPoints - Cantidad de puntos a generar.
* @returns Un array de objetos Coordinates.
*/
const generateRandomAreaCoordinates = (
 centerX: number,
 centerY: number,
 areaSize: number,
 numberOfPoints: number
): Coordinates[] => {
 const halfSize = areaSize / 2;
 const generatedPoints: Coordinates[] = [];

 for (let i = 0; i < numberOfPoints; i++) {
   const randomX = Math.floor(centerX - halfSize + Math.random() * areaSize);
   const randomY = Math.floor(centerY - halfSize + Math.random() * areaSize);
   generatedPoints.push({ x: randomX, y: randomY });
 }

 return generatedPoints;
};

function CreativeCanvas() {
 const canvasElementRef = useRef<HTMLCanvasElement | null>(null);
 const [renderingContext, setRenderingContext] = useState<CanvasRenderingContext2D | null>(null);

 useEffect(() => {
   if (canvasElementRef?.current) {
     const context = canvasElementRef.current.getContext('2d');
     if (context) {
       context.fillStyle = '#333'; // Color de relleno
       context.strokeStyle = '#333'; // Color del trazo
       context.lineWidth = 1;
       setRenderingContext(context);
     }
   }
 }, [canvasElementRef]);

 const handleMouseDown = () => {
   if (!canvasElementRef?.current || !renderingContext) return;
   drawingActive = true;
 };

 const handleMouseMove = (event: MouseEvent) => {
   if (!canvasElementRef?.current || !renderingContext || !drawingActive) return;

   const { clientX, clientY } = event;
   const currentPoints = generateRandomAreaCoordinates(clientX, clientY, 60, 4); // Genera 4 puntos en un área de 60px
   renderBrushStrokes(currentPoints);
   previousPoints = currentPoints;
 };

 const renderBrushStrokes = (currentPoints: Coordinates[]) => {
   if (!renderingContext) return;

   // Dibuja líneas conectando puntos anteriores con los actuales
   if (previousPoints.length > 0) {
     previousPoints.forEach((prevPoint, index) => {
       if (currentPoints[index]) {
         renderingContext.beginPath();
         renderingContext.save();
         renderingContext.moveTo(prevPoint.x, prevPoint.y);
         renderingContext.lineTo(currentPoints[index].x, currentPoints[index].y);
         renderingContext.stroke();
         renderingContext.restore();
       }
     });
   }

   // Dibuja círculos en las posiciones actuales
   currentPoints.map((point) => {
     renderingContext.beginPath();
     renderingContext.save();
     renderingContext.arc(point.x, point.y, 8, 0, 2 * Math.PI, false); // Círculo con radio 8
     renderingContext.fill();
     renderingContext.restore();
   });
 };

 const handleMouseUp = () => {
   if (!canvasElementRef?.current || !renderingContext) return;
   drawingActive = false;
   previousPoints = []; // Reinicia los puntos al finalizar el trazo
 };

 return (
   <div>
     <canvas
       ref={canvasElementRef}
       onMouseDown={handleMouseDown}
       onMouseMove={handleMouseMove}
       onMouseUp={handleMouseUp}
       width={800} // Ejemplo de tamaño del canvas
       height={600}
     />
   </div>
 );
}

export default CreativeCanvas;
 

Pincel de Ondas

Este pincel simula un efecto de ondas dibujando semicírculos entre puntos consecutivos del movimiento del ratón. La orientación de cada semicírculo se alterna para crear la apariencia de una onda. El tamaño del semicírculo se ajusta dinámicamente con la velocidad del movimiento: un movimiento más rápido resulta en un semicírculo más grande.


interface PointCoords {
 x: number;
 y: number;
}

let isMousePressed = false;
let lastMovePoint: PointCoords = { x: 0, y: 0 }; // Guarda la última posición del cursor
let flipState = 1; // Controla la dirección de volteo del semicírculo

/**
* Calcula la distancia euclidiana entre dos puntos.
* @param startPoint - El primer punto.
* @param endPoint - El segundo punto.
* @returns La distancia calculada.
*/
const calculateDistance = (startPoint: PointCoords, endPoint: PointCoords): number => {
 return Math.sqrt(Math.pow(startPoint.x - endPoint.x, 2) + Math.pow(startPoint.y - endPoint.y, 2));
};

function WaveCanvasBrush() {
 const canvasRef = useRef<HTMLCanvasElement | null>(null);
 const [canvasContext, setCanvasContext] = useState<CanvasRenderingContext2D | null>(null);

 useEffect(() => {
   if (canvasRef?.current) {
     const context = canvasRef.current.getContext('2d');
     if (context) {
       context.strokeStyle = '#007bff'; // Color azul
       context.lineJoin = 'round'; // Bordes redondeados para las uniones de línea
       context.lineCap = 'round'; // Extremos redondeados de las líneas
       setCanvasContext(context);
     }
   }
 }, [canvasRef]);

 const handlePressDown = (event: MouseEvent) => {
   if (!canvasRef?.current || !canvasContext) return;
   isMousePressed = true;
   const { clientX, clientY } = event;
   lastMovePoint = { x: clientX, y: clientY };
 };

 const handleMovement = (event: MouseEvent) => {
   if (!canvasRef?.current || !canvasContext || !isMousePressed) return;

   const { clientX, clientY } = event;
   const currentPoint: PointCoords = { x: clientX, y: clientY };

   const segmentDistance = calculateDistance(lastMovePoint, currentPoint);
   const centerX = (lastMovePoint.x + clientX) / 2;
   const centerY = (lastMovePoint.y + clientY) / 2;

   canvasContext.beginPath();
   canvasContext.save();

   // Calcula el ángulo entre los dos puntos
   const angle = Math.atan2(clientY - lastMovePoint.y, clientX - lastMovePoint.x);

   // Determina la orientación del semicírculo (volteo)
   const flipOffset = (flipState % 2) * Math.PI;

   // Dibuja el semicírculo
   canvasContext.arc(
     centerX,
     centerY,
     segmentDistance / 2, // Radio basado en la distancia
     angle + flipOffset, // Ángulo de inicio
     angle + flipOffset + Math.PI // Ángulo de fin (medio círculo)
   );
   canvasContext.stroke();
   canvasContext.restore();

   // Actualiza el estado para el próximo segmento
   flipState++;
   lastMovePoint = currentPoint; // Actualiza el último punto de movimiento
 };

 const handleReleaseUp = () => {
   if (!canvasRef?.current || !canvasContext) return;
   isMousePressed = false;
   lastMovePoint = { x: 0, y: 0 }; // Resetea el punto de movimiento
 };

 return (
   <div>
     <canvas
       ref={canvasRef}
       onMouseDown={handlePressDown}
       onMouseMove={handleMovement}
       onMouseUp={handleReleaseUp}
       width={800}
       height={600}
     />
   </div>
 );
}

export default WaveCanvasBrush;
 

Pincel de Espinas

Este pincel genera un efecto de "espinas" dibujando elipses delgadas y alargadas. La longitud de la elipse se basa en la distancia recorrida por el cursor, y su grosor es fijo y pequeño. Esto crea una apariencia puntiaguda y afilada.


interface Position {
 x: number;
 y: number;
}

let isDrawing = false;
let previousPosition: Position = { x: 0, y: 0 }; // Almacena la última posición del ratón
const minEllipseWidth = 4; // Ancho mínimo de la elipse

/**
* Calcula la distancia entre dos puntos.
* @param start - El punto de inicio.
* @param end - El punto final.
* @returns La distancia calculada.
*/
const getPathDistance = (start: Position, end: Position): number => {
 return Math.sqrt(Math.pow(start.x - end.x, 2) + Math.pow(start.y - end.y, 2));
};

function ThornBrushCanvas() {
 const canvasRef = useRef<HTMLCanvasElement | null>(null);
 const [ctx, setCtx] = useState<CanvasRenderingContext2D | null>(null);

 useEffect(() => {
   if (canvasRef?.current) {
     const renderingContext = canvasRef.current.getContext('2d');
     if (renderingContext) {
       renderingContext.fillStyle = '#d9534f'; // Color rojo
       renderingContext.lineJoin = 'round';
       renderingContext.lineCap = 'round';
       setCtx(renderingContext);
     }
   }
 }, [canvasRef]);

 const handleInteractionStart = (event: MouseEvent) => {
   if (!canvasRef?.current || !ctx) return;
   isDrawing = true;
   const { clientX, clientY } = event;
   previousPosition = { x: clientX, y: clientY };
 };

 const handleInteractionMove = (event: MouseEvent) => {
   if (!canvasRef?.current || !ctx || !isDrawing) return;

   const { clientX, clientY } = event;
   const currentPosition: Position = { x: clientX, y: clientY };

   const movementDistance = getPathDistance(previousPosition, currentPosition);

   // Calcula el centro de la elipse
   const ellipseCenterX = (previousPosition.x + clientX) / 2;
   const ellipseCenterY = (previousPosition.y + clientY) / 2;

   ctx.beginPath();
   ctx.save();

   // Calcula el ángulo de rotación de la elipse
   const rotationAngle = Math.atan2(clientY - previousPosition.y, clientX - previousPosition.x);

   /**
    * Dibuja la elipse:
    * - Radio X (ancho) es proporcional a la distancia del movimiento.
    * - Radio Y (alto) es fijo (minEllipseWidth).
    */
   ctx.ellipse(
     ellipseCenterX,
     ellipseCenterY,
     movementDistance * 6 + minEllipseWidth, // Ancho de la elipse
     minEllipseWidth, // Alto de la elipse
     rotationAngle, // Rotación
     0, // Ángulo de inicio
     2 * Math.PI // Ángulo de fin
   );
   ctx.fill();
   ctx.restore();

   previousPosition = currentPosition; // Actualiza la posición anterior
 };

 const handleInteractionEnd = () => {
   if (!canvasRef?.current || !ctx) return;
   isDrawing = false;
   previousPosition = { x: 0, y: 0 }; // Resetea la posición
 };

 return (
   <div>
     <canvas
       ref={canvasRef}
       onMouseDown={handleInteractionStart}
       onMouseMove={handleInteractionMove}
       onMouseUp={handleInteractionEnd}
       width={800}
       height={600}
     />
   </div>
 );
}

export default ThornBrushCanvas;
 

Etiquetas: canvas JavaScript React HTML5 WebGL

Publicado el 6-11 18:40