Selector de área o polígono
Introducción
Cuando trabajamos en edición gráfica con el elemento <canvas>
o con imágenes podemos necesitar un selector de áreas y/o polígonos. Por ejemplo, para dibujar un rectángulo en un Canvas podemos aplicar strokeRect(x,y,w,h)
, siendo x, y las posiciones izquierda y arriba en el plano mientras que w, h serán el ancho y alto de ese rectángulo. Una forma de indicar esas medidas es usando un selector que resalte una selección de área. Aunque también podemos hacer una selección de puntos de un polígono.
En resumen se trata de desarrollar el módulo JavaScript select-area.js que construye instancias de un objeto JavaScript llamado selectArea
para gestionar un control selector de áreas y polígonos. El objeto construye todo lo necesario para manipular la selección mediante elementos HTML y SVG posicionados absolutamente y con los eventos de ratón mousedown, mousemove
y mouseup
. En el caso de áreas utiliza un elemento <div>
mientras que el selector de polígonos se forma con elementos <div>
para los puntos y con elementos <line>
de SVG para las líneas.
En este tema se exponen dos ejemplos de uso de este objeto selectArea
que han sido probados en los navegadores Chrome 21.0, Safari 5.1, Firefox 15.0 y Opera 12.02. En Internet Explorer 8 funciona a excepción de lo relativo a las líneas del polígono, pues no admite SVG aunque si maneja los puntos o vértices del polígono. Por supuesto, tampoco admite Canvas, pero si encuentra una aplicación de uso en el segundo ejemplo que se expone.
Recuerde que los HTML, JS y CSS están minificados pero puede ver los códigos originales con comentarios en el botón CÓDIGO de la barra de botones de la parte superior de este documento. Además ha de tener en cuenta que uso un espacio de nombres y un cargador de módulos de JavaScript. El módulo select-area.js, igual que otros en este sitio, se apoya en general.js, un JavaScript con recursos de uso común para todas las páginas de este sitio.
Un selector de áreas y polígonos para dibujar en un Canvas
Este es un primer ejemplo de aplicación del selector, donde podemos dibujar formas y polígonos en un elemento <canvas>
.
Diciembre 2018: Mejorado para unir los puntos también con curvas de Bezier cúbicas y cuadráticas así como con arcos. Se muestran las propiedades de cada punto y se puede modificar la configuración. En navegadores que no soporten ES6 no funcionará, usándose la versión anterior del script. Estas mejoras y otras se incorporan para ser utilizadas por la herramienta editor SVG. Las principales mejoras son:
- Unir los puntos también con curvas de Bezier cúbicas y cuadráticas así como con arcos.
- Mejora del constructor de polígonos, usando también curvas de Bezier para unir los vértices. Podemos unirlos con Bezier cuadráticas o cúbicas para aproximarlas a arcos de circunferencia. Puede ver un par de temas con más información sobre las Curvas de Bezier cuadráticas en los SVG y las Curvas de Bezier cubicas en los SVG. Otro tema es acerca de los arcos de circunferencia con curvas Bezier en SVG, exponiendo como aproximar un arco de circunferencia entre dos puntos conocidos.
- Utilidades para importar y exportar en varios formatos, especialmente con el formato del atributo
"d"
de un elemento<path>
lo que lo hace adecuado para un editor SVG. - Modo dibujo que permite dibujar líneas y curvas a "mano alzada".
- Diferenciación por subcaminos, pudiendo eliminar, mover, cerrar, cortar o unir un subcamino.
- Interfaz para mostrar propiedades de cada punto y línea, pudiendo modificarse o eliminarse así como crear nuevos puntos y líneas.
- Interfaz para cambiar la configuración del Selector.
- Se incorporan y mejoran funciones que cambian el tamaño, la posición, el ajuste a rejilla, la rotación, el alineado y el reflejo.
- Se mejoran los shortcuts o teclas de método abreviado.
Ejemplo:
Vemos la superficie de dibujo y un grupo de controles para seleccionar un área o bien un polígono. Con la selección de área nos aparece un rectángulo con borde de rayas y un triángulo en su esquina inferior derecha. Se trata de un rectángulo selector construido con un <div>
donde se incorpora una imagen del triángulo en su background
y posicionándola en esa esquina. Manejando los eventos del ratón mousedown, mousemove
y mouseup
podemos mover el selector y también modificar sus dimensiones. Al mismo tiempo los valores se actualizan en el grupo de controles y viceversa. Con la selección de polígono podemos ir creando puntos mientras se van generando líneas que los unen. Es posible también crear polígonos regulares.
touchstart
, touchmove
y touchend
.Los controles del grupo dibujar en Canvas no pertenecen al objeto y los he puesto para realizar una aplicación práctica del selector selectArea
. En este caso hacemos uso de un <canvas>
como superficie de dibujo, incorporando algunas cosas necesarias para dibujar formas simples y evidenciar el uso del selector. Puede ver una introducción al elemento canvas
en este sitio.
Para construir un selector de área necesitamos tener un contenedor al que se aplica. En principio podría ser cualquier elemento de bloque, pero en este ejemplo es un elemento <div>
declarado en el HTML de esta página (quito lo relativo a los controles para dibujar en canvas que no interesan ahora):
El identificado con id="contenedor"
será sobre el que se aplique el objeto selector. En su interior albergamos un elemento <canvas>
con las mismas dimensiones, de tal forma que el área seleccionada en el <div>
nos servirá para dibujar en el <canvas>
. El estilo necesario del contenedor para construir el selector es el siguiente:
position: relative
pues el rectángulo del área selectora y los elementos del polígono son elementos HTML que están ubicados en su interior y que se posicionan de forma absoluta con respecto a este contenedor. Puede ver más sobre el tema de los posicionamientos, pues es importante entender que este contenedor posicionado relativo no puede a su vez estar dentro de otro posicionado, dado que no obtendríamos los valores (x,y) adecuados con los eventos del ratón.width, height
, que serán los mismos que pondríamos en el elemento interior, en este caso un<canvas>
pero podríamos actuar sobre elbacgroundImage
del propio contenedor, como veremos en un ejemplo más abajo.z-index
es una propiedad que por defecto toma el valor cero, por lo que no haría falta ponerla. Lo hacemos para recordar que los elementos necesarios para construir el selector se acoplan en 4 capas por encima de la del contenedor. Estas capas construidas por el objeto son:- Una capa transparente para ubicar líneas entre puntos del polígono, contenidas en un elemento
<svg>
(más abajo se exponen detalles sobre SVG). - Una capa transparente sobre la anterior para ubicar los puntos del polígono, contenidos en un elemento
<div>
. - Una capa sobre la anterior que será el rectángulo selector, también en otro
<div>
. Le damos cierto grado de transparencia para entrever el fondo. - Una última capa transparente con otro
<div>
sobre la que se mueve el ratón y capta las coordenadas x, y.
- Una capa transparente para ubicar líneas entre puntos del polígono, contenidas en un elemento
overflow: hidden
para ocultar todo lo que se dibuje fuera del contenedor.outline
es para incluir un borde exterior o contorno, de tal forma que el punto (0,0) es interior al área. De la misma forma el área selectora está también bordeada conoutline
.
Necesitamos también un lugar identificado donde ubicar los controles del selector, un <div id="controles">
en este ejemplo. Todo el conjunto está albergado en otro <div class="ejemplo-linea">
que uso para presentar los ejemplos con recuadro y letra de color verde. Le agrego el estilo user-select: none
para impedir seleccionar texto en los alrededores del contenedor cuando se mueve el ratón, pero sobre esto me extenderé un poco en el siguiente apartado. El cuadro de controles tiene un botón para desactivar el selector. En la selección de áreas podemos seleccionar todo y borrar la selección. En el selector de polígono también podemos borrar todos los puntos y construir un polígono regular. La edición de polígono se ejecuta con los movimientos del ratón y es la siguiente:
- Para eliminar un punto hay que moverlo y ponerlo encima de otro consecutivo.
- Parra cerrar un camino del polígono moveremos el último punto encima de otro cualquiera que no sea consecutivo. Sólo puede cerrarse un camino con el último punto. Este cierre vuelve a abrirse si agregamos un nuevo punto o movemos alguno de sitio. Cuando creamos polígonos regulares el camino se cierra entre primer y último punto.
- Para mover el polígono activaremos presentar área para mover, apareciendo el recuadro igual que el selector de área que nos permitirá mover el polígono.
User-select, unselectable y posicionamiento absoluto con eventos mouse
El comportamiento de los navegadores al hacer mousedown
y mientras hacemos mousemove
es radicalmente diferente en los distintos navegadores. Aunque la opción por defecto es que se seleccione texto, el comportamiento no es el mismo. Por lo tanto le agrego el estilo user-select: none
para impedir cualquier selección de texto. Esta propiedad user-select
aún no es estándar pero funciona en Chrome 21.0, Safari 5.1 y Firefox 15.0. En Opera 12.0 o IE8 no funciona y hemos de usar el atributo unselectable="on"
. Dado que user-select
no es estándar hemos de prefijarla. Vea vpForCss sobre este tema de los vendor prefixes. La propiedad se hereda por lo que impide que se seleccione texto en todos los elementos de esa superficie del ejemplo. Sin embargo el atributo unselectable
no se hereda entre elementos de bloque anidados, aunque si lo declaramos para un elemento de bloque si lo heredan todos los elementos de línea (al menos eso creo). En resumen, esta es una de esas cosas que aún no se ha unificado en todos los navegadores y que nos dan un dolor de cabeza.
- En Chrome 21.0 cuando estamos moviendo el selector y si tras salirnos del contenedor selecionamos texto en los alrededores del mismo, se pierden las coordenadas (x, y) del evento y hay que hacer doble click sobre el contenedor para recuperar el control del selector, o bien quitar la selección de texto haciendo click en cualquier otra parte de la pagina. Por lo tanto necesitamos hacerlo no seleccionable con
user-select
tanto al contenedor como a los alrededores. No tiene sentido hacer no seleccionable toda la página por si alquién quiere copiar un trozo de texto, pero esto evitaría el problema. Esto no lo voy a hacer en esta página, pero en todo caso bastaría poneruser-select
al elemento<div id="total">
que alberga toda la página. - Igual para Safari 5.1 pues trabaja con el mismo WebKit, aunque vea lo que se comenta más abajo.
- Con Firefox 15.0 no pasa lo de antes cuando nos salimos del contenedor. El problema ahora es que al movernos por el propio contenedor trata de seleccionarlo como texto. Por lo tanto habrá que hacerlo no seleccionable con
user-select
. - Opera 12.0 e IE8 necesitan
unselectable
. Pero al menos estos funcionan bien, pues tras ponerunselectable
no selecciona texto si nos salimos del contenedor.
Decimos que user-select
se hereda, pero no en el caso de los cuadros de texto <input type="text">
para Chrome y Safari. Para hacerlo no seleccionable habría que aplicar user-select
directamente a cada cuadro de texto. Sería similar a desactivarlo con disabled
. Pero no podemos impedir la selección del texto de un <input>
pues entonces no podríamos modificar manualmente ese texto. Este problema, que se manifiesta en Chrome y Safari, consiste en mover el área selectora, salirnos del contenedor y, sin soltar el botón del ratón, seleccionar texto dentro de un <input type="text">
. Es más, Safari incluso se "cuelga" si tratamos luego de mover el área selectora, con el mensaje WebKit2WebProcess.exe ha detectado un problema y debe cerrarse
.
JavaScript para configurar el selector de áreas y polígonos
En el window.onload
de esta página se encuentra el siguiente código. Se declaran dos objetos selectores, sel
para el ejemplo anterior y sel2
para el siguiente. La declaración del constructor es new selectArea(nombreInstancia, contenedor, controles)
. El primer argumento es una cadena con el mismo nombre que la variable creada. El segundo argumento es una cadena con el identificador id
de un elemento de bloque donde vamos a ubicar el selector. El tercer argumento también es un identificador a otro elemento de bloque donde ubicaremos el cuadro de controles. Este último argumento puede omitirse en cuyo caso el selector se construye sin esos controles. Por defecto el tipo de selección será de área, pero podemos cambiar entre los tipos area o poligono (sin tildes) llamando a la función objeto.cambiarTipoSeleccion(tipo)
.
El selector ejecuta dos eventos. Uno llama a la función objeto.ejecutarEventoMov()
después del evento mouseup
o durante el mousemove
si estamos arrastrando el área selectora. Esto nos sirve para el segundo ejemplo donde vamos actualizando una imagen tomando los datos de posicion y dimensiones del área selectora. El otro evento del selector es objeto.ejecutarEventoTipoSeleccion()
que se dispara cuando se ejecuta un cambio de tipo usando la función objeto.cambiarTipoSeleccion(tipo)
. Este evento se usa en el primer ejemplo para desactivar un control suplementario de dibujo en el canvas.
El área selectora del objeto, es decir, el rectángulo con borde a rayas que podemos mover y dimensionar, es la variable objeto.selector
. Se trata de una referencia a un elemento <div>
y por tanto podemos acceder a su posición con objeto.selector.offsetLeft
y lo mismo para offsetTop
, offsetWidth
y offsetHeight
. De igual forma podríamos posicionarlo, por ejemplo, objeto.selector.style.left = "10px"
o también podemos modificar el color del borde, fondo, etc.
Por último vemos en el window.onload
la carga del vendor prefix para la propiedad user-select
. Como señalé antes, puede ver como funciona esto de los prefijos CSS en ese enlace vpForCss.
Selector de área: un efecto "lupa" sobre una imagen.
Para dar otro ejemplo de uso retomaré uno que hice hace tiempo para crear un efecto "lupa" sobre una imagen. Esto lo expuse como ejemplo de la propiedad CSS {clip}
para recortar un elemento. En esencia se trata de tener dos imágenes de distinto tamaño con el mismo contenido. La pequeña de 150×111 píxeles será la guía sobre la que aplicamos la "lupa" y ese trozo lo veremos ampliado extrayéndolo de la imagen grande de 640×480 píxeles. Ahí usábamos clip
pero en este ejemplo no será necesario sino actuar directamente con la posición y dimensiones del área selectora.
Ejemplo:
El código HTML para declarar el selector de área es similar al anterior. Junto a ese contenedor ponemos otros identificado con id="extraccion"
que es el que va a contener la ampliación de la imagen obtenida desde la de mayor tamaño.
En el código del apartado anterior verá la declaración de sel2.ejecutarEventoMov
que es la encargada del efecto. Al mover el área selectora se dispara este evento por lo que extraemos el ancho y alto con offsetWidth
y offsetHeigth
, dimensionando la ampliación en proporción al zoom, es decir, de forma proporcionada a los tamaños de ambas imágenes. En el contenedor de extracción la imagen está incorporada con el estilo background-image
y posicionamos con background-position
proporcionadamente al zoom.
Elementos SVG para el selector de polígonos
La especificación SVG 1.1 se encuentra ahora (septiembre 2012) en fase de recomendación. Define las características y sintaxis para crear Gráficos Vectoriales Redimensionables (Scalable Vector Graphics, SVG). Con SVG podemos crear objetos geométricos como rectas o curvas así como manejar imágenes o texto. Es parecido a lo que hacemos en Canvas pero ahora cada objeto geométrico es un elemento HTML que podemos manipular en el DOM como cualquier otro elemento. No voy a adentrarme de lleno en esta técnica pues requeriría uno o varios temas específicos, aunque apuntaré algunas cosas relacionadas con las líneas que unen los vértices del polígono.
Para crear el polígono de nuestro selector del primer ejemplo necesitamos los puntos (o vértices) y las líneas que los unen. Los puntos los crea el selector con elementos <div>
. Las líneas rectas horizontales y verticales también podrían ser creadas con elementos HTML fácilmente anulando el ancho o alto del elemento. Pero no es posible poner líneas oblícuas con un elemento HTML. Por eso es necesario hacer uso del elemento <svg>
que creamos en la primera capa del selector como se menciona más arriba. El código siguiente lo obtuve con la herramienta de desarrollo del navegador Chrome 21.0 que nos permite copiar un elemento y su interior en formato HTML y luego pegarlo en un editor de texto. Se trata del código actual después de crear dos puntos de polígono en el primer ejemplo. Tras eliminar con puntos suspensivos algunas cosas que no interesan ahora, así como añadir algunos comentarios, nos queda esto relacionado con las capas de líneas y puntos tras ser construido por el objeto selector:
<path>
en lugar de elementos <line>
.Por cada punto creado en el selector se inserta un nuevo elemento <line>
en el contenedor <svg>
y un elemento <div>
en el contenedor de puntos. La posición del punto queda determinada por sus propiedades left
y top
para (x, y) respectivamente. Así el id="punto0"
está en la posición (42, 60). El punto se dibuja con un cuadrado de 12 píxeles de lado, por lo que su centro será (48, 66). Este punto tiene asociado la línea id="linea0"
que va desde (48, 66) hasta (122, 99), centro del siguiente punto id="punto1"
. Éste es el último del polígono y su línea asociada no tiene longitud pues x1=x2 y y1=y2. Si ponemos un nuevo punto entonces se alargará esta línea para conectarla con ese nuevo punto.