Redimensionamiento y recorte de imágenes con Canvas - Parte 2

Seguimos con el tutorial de redimensionamiento y recorte de imágenes con Canvas. Si quieres leer la primera parte puedes leerla aquí.

Dibujar una imagen con Canvas es muy sencillo con la función DrawImage. Establecemos el alto y ancho del canvas primero y usamos siempre la copia original de la imagen a tamaño completo. Despues utilizamos toDataURL en el cnavas para conseguir una versión codificada en Base64 de la imagen redimensionada recientemente y la colocamos en la página.

Puedes encontrar una explicación completa de todos los parámetros que se pueden utilizar con el método DrawImage en la sección de recorte de este artículo.

resizeImage = function(width, height){
    resize_canvas.width = width;
    resize_canvas.height = height;
    resize_canvas.getContext('2d').drawImage(orig_src, 0, 0, width, height);   
    $(image_target).attr('src', resize_canvas.toDataURL("image/png"));  
};

¿Muy simple? No tan rápido: la imagen debe estar en el mismo dominio que la página o en un servidor con el cross-origin resource sharing (CORS) habilitado. Si no cumples estos parámetros, de serguro que tendrás más de un problema para redimensionar la imagen.

Redimensionar de diferentes esquinas

Con todo lo explicado antes deberías tener una demo funcional. Por el momento, no importa en qué esquina de la imagen hagamos la redimensión, todas se comportan igual que si lo hiciesemos desde la esquina superior derecha. El objetivo es ser capaces de cambiar el tamaño de la imagen desde cualquier esquina. Para ello tenemos que entender cómo debe comportarse cada una de elllas.

Al cambiar el tamaño, la esquina que estés arrastrando como sus bordes adyacentes deben moverse, mientras que la esquina de justo enfrente y sus bordes adyacentes deben permancer fijos.

Cuando cambiamos el ancho y alto de una imagen, los bordes derecho e inferior se mueven, mientras que los bordes superior e izquierdo permanecerán igual. Esto significa que, por defecto, la imagen se redimensionará desde la esquina inferior derecha.

No podemos cambiar este comportamiento por defecto, pero al cambiar el tamaño de la imagen desde cualquier esquina que no sea la de la parte inferior derecha, podemos cambiar la posición global de la imagen para que aparezca como si la esquina opuesta y los bordes permaneciesen fijos. Vamos a actualizar nuestra función de redimensión.

resizing = function(e){
  var mouse={},width,height,left,top,offset=$container.offset();
  mouse.x = (e.clientX || e.pageX || e.originalEvent.touches[0].clientX) + $(window).scrollLeft(); 
  mouse.y = (e.clientY || e.pageY || e.originalEvent.touches[0].clientY) + $(window).scrollTop();
  
  // Position image differently depending on the corner dragged and constraints
  if( $(event_state.evnt.target).hasClass('resize-handle-se') ){
    width = mouse.x - event_state.container_left;
    height = mouse.y  - event_state.container_top;
    left = event_state.container_left;
    top = event_state.container_top;
  } else if($(event_state.evnt.target).hasClass('resize-handle-sw') ){
    width = event_state.container_width - (mouse.x - event_state.container_left);
    height = mouse.y  - event_state.container_top;
    left = mouse.x;
    top = event_state.container_top;
  } else if($(event_state.evnt.target).hasClass('resize-handle-nw') ){
    width = event_state.container_width - (mouse.x - event_state.container_left);
    height = event_state.container_height - (mouse.y - event_state.container_top);
    left = mouse.x;
    top = mouse.y;
    if(constrain || e.shiftKey){
      top = mouse.y - ((width / orig_src.width * orig_src.height) - height);
    }
  } else if($(event_state.evnt.target).hasClass('resize-handle-ne') ){
    width = mouse.x - event_state.container_left;
    height = event_state.container_height - (mouse.y - event_state.container_top);
    left = event_state.container_left;
    top = mouse.y;
    if(constrain || e.shiftKey){
      top = mouse.y - ((width / orig_src.width * orig_src.height) - height);
    }
  }

  // Optionally maintain aspect ratio
  if(constrain || e.shiftKey){
    height = width / orig_src.width * orig_src.height;
  }

  if(width > min_width && height > min_height && width < max_width && height < max_height){
    // To improve performance you might limit how often resizeImage() is called
    resizeImage(width, height);  
    // Without this Firefox will not re-calculate the the image dimensions until drag end
    $container.offset({'left': left, 'top': top});
  }
}

Ahora comprobamos en qué resize-handle ha sido arrastrada y movemos la imagen mientras la redimensionamos para que aparezca como si la esquina correcta permaneciese fija.

Moviendo la imagen

Ahora que podemos cambiar el tamaño de la imagen desde cualquiera de sus esquinas habrás notado que sin querer podemos cambiar su posición en la página. Tenemos que dar a los usuarios la capacidad de mover la imagen de nuevo en el centro del encuadre. En la función init vamos a añadir otro detector de eventos similar al que hemos hecho antes.

init = function(){

    //...

    $container.on('mousedown', 'img', startMoving);
}

Ahora añadimos las funciones startMoving y endMoving de manera similar a startResize y endResize

startMoving = function(e){
    e.preventDefault();
    e.stopPropagation();
    saveEventState(e);
    $(document).on('mousemove', moving);
    $(document).on('mouseup', endMoving);
};

endMoving = function(e){
    e.preventDefault();
    $(document).off('mouseup', endMoving);
    $(document).off('mousemove', moving);
};

En la función moving necesitaremos calcular la nueva posición del borde superior izquierdo del contenedor. Esta será igual a la posición actual del ratón, compensado por la distancia del ratón a la esquina superior izquierda cuando empezamos a arrastrar la imagen.

moving = function(e){
    var  mouse={};
    e.preventDefault();
    e.stopPropagation();
    mouse.x = (e.clientX || e.pageX) + $(window).scrollLeft();
    mouse.y = (e.clientY || e.pageY) + $(window).scrollTop();
    $container.offset({
        'left': mouse.x - ( event_state.mouse_x - event_state.container_left ),
        'top': mouse.y - ( event_state.mouse_y - event_state.container_top ) 
    });
};

Recortando la imagen

Ahora que podemos cambiar el tamaño de la imagen, lo suyo sería poder recortarla también. En lugar de permitir a los usuarios que puedan recortar la imagen a cualquier tamaño y forma, vamos a crear un frame con las dimensiones exactas que necesitamos y pedir a los usuarios que coloquen la imagen dentro de ese frame. Esto nos otorgará control sobre el zoom y el encuadre, y además, nos aseguramos de que la imagen final siempre tenga el mismo tamaño y la misma forma.

Para ello tenemos que añadir una el siguiente código HTML:

<div class="overlay">
    <div class="overlay-inner">
    </div>
</div>
<button class="btn-crop js-crop">Crop</button>

Los estilos para la caja overlay son importantes, particularmente su posición, anchura y alturason utilizados para determinar qué parte de la imagen se recortará. También es importante recordar que el frame debe ser siempre visible sin importar el color de fondo. Es por eso que he utilizado una linea blanca semitransparente alrededor de la caja principal.

.overlay {
    position: absolute;
    left: 50%;
    top: 50%;
    margin-left: -100px;
    margin-top: -100px;
    z-index: 999;
    width: 200px;
    height: 200px;
    border: solid 2px rgba(222,60,80,.9);
    box-sizing: content-box;
    pointer-events: none;
}

.overlay:after,
.overlay:before {
    content: '';
    position: absolute;
    display: block;
    width: 204px;
    height: 40px;
    border-left: dashed 2px rgba(222,60,80,.9);
    border-right: dashed 2px rgba(222,60,80,.9);
}

.overlay:before {
    top: 0;
    margin-left: -2px;
    margin-top: -40px;
}

.overlay:after {
    bottom: 0;
    margin-left: -2px;
    margin-bottom: -40px;
}

.overlay-inner:after,
.overlay-inner:before {
    content: '';
    position: absolute;
    display: block;
    width: 40px;
    height: 204px;
    border-top: dashed 2px rgba(222,60,80,.9);
    border-bottom: dashed 2px rgba(222,60,80,.9);
}

.overlay-inner:before {
    left: 0;
    margin-left: -40px;
    margin-top: -2px;
}

.overlay-inner:after {
    right: 0;
    margin-right: -40px;
    margin-top: -2px;
}

.btn-crop {
    position: absolute;
    vertical-align: bottom;
    right: 5px;
    bottom: 5px;
    padding: 6px 10px;
    z-index: 999;
    background-color: rgb(222,60,80);
    border: none;
    border-radius: 5px;
    color: #FFF;
}

Modificamos el Javascript con la siguiente función y con el detector de eventos

init = function(){

    //...

    $('.js-crop').on('click', crop);
  
};

crop = function(){
    var crop_canvas,
        left = $('.overlay').offset().left - $container.offset().left,
        top =  $('.overlay').offset().top - $container.offset().top,
        width = $('.overlay').width(),
        height = $('.overlay').height();
        
    crop_canvas = document.createElement('canvas');
    crop_canvas.width = width;
    crop_canvas.height = height;
    
    crop_canvas.getContext('2d').drawImage(image_target, left, top, width, height, 0, 0, width, height);
    window.open(crop_canvas.toDataURL("image/png"));
}

La función crop es similar a la función resizeImage, sin embargo, en lugar de pasarle los valores de ancho y alto, obtenemos el alto y ancho del elemento overlay.

Para recortar, el método del canvas drawImage requiere nueve parámetros. El primer parámetro es la imagen. Los próximos cuatro parámetros indican qué parte de la imagen vammos a utilizar (es decir, el cuadro de recorte). Los últimos cuatro parámetros indican donde debe empezar a dibujar el canvas y en qué tamaño.

Añadiendo control táctil y detección de gestos

Ya que hemos añadido eventos para ratón, vamos a insertar soporte para dispositivos móviles también.

Para mousedown y mouseup hay un equivalente en táctil, touchstart y touchend, y para mousemove, el equivalente es touchmove. Vamos a añadir los eventos touchstart y touchend donde haya un detector de eventos mousedown y mouseup, y touchmove donde haya un mousemove.

// In init()...
$container.on('mousedown touchstart', '.resize-handle', startResize);
$container.on('mousedown touchstart', 'img', startMoving);

//In startResize() ...
$(document).on('mousemove touchmove', moving);
$(document).on('mouseup touchend', endMoving);

//In endResize()...
$(document).off('mouseup touchend', endMoving);
$(document).off('mousemove touchmove', moving);

//In  startMoving()...
$(document).on('mousemove touchmove', moving);
$(document).on('mouseup touchend', endMoving);

//In endMoving()...
$(document).off('mouseup touchend', endMoving);
$(document).off('mousemove touchmove', moving);

Ya que estamos redimensionando una imagen, lo suyo sería que al pellizcar la pantalla se hiciese zoom. Existe una librería llamada Hammer que nos proporciona una gran ayuda a la hora de trabajar con gestos. Pero como solo necesitamos el pellizco para hacer zoom, tal vez nos venga grande eso de insertar esta librería. A continuación te voy a mostrar lo sencillo que es detectar un pellizco en la pantalla sin ninguna librería.

En primer lugar, comprobamos si el evento tiene dos "toques" y medimos la distancia entre ellos. Observamos esto como la distancia inicial y luego medimos constantemente los cambios en la distancia mientras se mueve. Vamos a actualizar la función de movimiento:

moving = function(e){
  var  mouse={}, touches;
  e.preventDefault();
  e.stopPropagation();
  
  touches = e.originalEvent.touches;
  mouse.x = (e.clientX || e.pageX || touches[0].clientX) + $(window).scrollLeft(); 
  mouse.y = (e.clientY || e.pageY || touches[0].clientY) + $(window).scrollTop();
  $container.offset({
    'left': mouse.x - ( event_state.mouse_x - event_state.container_left ),
    'top': mouse.y - ( event_state.mouse_y - event_state.container_top ) 
  });
  // Watch for pinch zoom gesture while moving
  if(event_state.touches && event_state.touches.length > 1 && touches.length > 1){
    var width = event_state.container_width, height = event_state.container_height;
    var a = event_state.touches[0].clientX - event_state.touches[1].clientX;
    a = a * a; 
    var b = event_state.touches[0].clientY - event_state.touches[1].clientY;
    b = b * b; 
    var dist1 = Math.sqrt( a + b );
    
    a = e.originalEvent.touches[0].clientX - touches[1].clientX;
    a = a * a; 
    b = e.originalEvent.touches[0].clientY - touches[1].clientY;
    b = b * b; 
    var dist2 = Math.sqrt( a + b );

    var ratio = dist2 /dist1;

    width = width * ratio;
    height = height * ratio;
    // To improve performance you might limit how often resizeImage() is called
    resizeImage(width, height);
  }
};

Dividimos la distancia actual por la distancia inicial para obtener la relación y cuánto debe escalar la imagen. Llevamos a cabo la nueva anchura y altura y luego redimensionamos la imagen.

Y eso sería todo, espero que os haya gustado.

Fuente: tympanus.net

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP