javascript - img - ¿Cómo puedo sujetar un rectángulo dentro de los límites de un rectángulo girado exterior?
option html selected (3)
Estoy trabajando en un WebGL (con un editor de fotos en lienzo 2d canvas).
Decidí incorporar la rotación directamente en la herramienta de recorte, de manera similar a la recortadora de fotos iOS 8. es decir, el tamaño y la posición de la fotografía cambian dinámicamente a medida que gira la foto, para que el área de recorte esté siempre contenida dentro de la fotografía.
Sin embargo, estoy luchando con algunas de las matemáticas.
Tengo dos rectángulos, la foto y el área de recorte.
Ambos se definen como:
var rect = {
x : x,
y : y,
w : width,
h : height
}
El rectángulo que define la foto en sí también tiene una propiedad de rotation
en radianes, que por supuesto describe el ángulo de la foto (en radianes).
Debe tenerse en cuenta que las coordenadas x
e y
del rectángulo de la foto ( no el rectángulo de recorte) realmente definen el centro de la foto, y no el punto superior izquierdo. Si bien esto puede parecer extraño, nos facilita los cálculos en otros lugares y no debería afectar esta pregunta. En aras de la exhaustividad lo menciono.
El origen de transformación de la foto siempre se establece como el centro del área de cultivo.
Cuando el ángulo de la foto es 0
y el área de recorte es del mismo tamaño que la foto, los rectángulos se alinean así:
Cuando roto la foto, se aplica un factor de escala al rectángulo de la foto para garantizar que el área de cultivo se mantenga dentro de los límites de las fotos:
Esto se logra calculando el cuadro delimitador de la zona de cultivo y, a partir de esto, calculando el factor de escala requerido para aplicar al rectángulo de la photo
:
TC.UI.PhotoCropper.prototype._GetBoundingBox = function(w,h,rads){
var c = Math.abs(Math.cos(rads));
var s = Math.abs(Math.sin(rads));
return({ w: h * s + w * c, h: h * c + w * s });
}
var bbox = this._GetBoundingBox(crop.w, crop.h, photo.rotation);
var scale = Math.max(1, Math.max(bbox.w/photo.w, bbox.h/photo.h));
Actualmente, esto funciona exactamente como se espera y el ancho del rectángulo de la foto se escala correctamente en cualquier lugar donde se encuentre el área de cultivo (y el origen de la rotación resultante).
Además, como se trata de una herramienta de recorte de fotografías, la posición del área de cultivo puede, por supuesto, modificarse. Tenga en cuenta que al modificar la posición de la zona de cultivo, estamos moviendo el rectángulo de la photo
. (La zona de cultivo siempre permanece centrada, ya que creemos que esto es mucho más natural con los dispositivos táctiles, y aún así es aceptable cuando se usa con un mouse ... Esto es lo mismo en iOS y Windows 8).
Por lo tanto, actualmente fijamos las coordenadas x
e y
del rectángulo de la photo
en los bordes del área recortada de la siguiente manera:
var maxX = crop.x + (crop.w/2) - (photo.w/2);
var maxY = crop.y + (crop.h/2) - (photo.w/2);
var minX = crop.x - (crop.w/2) + (photo.w/2);
var minY = crop.y - (crop.h/2) + (photo.h/2);
photo.x = Math.min(minX, Math.max(maxX, photo.x));
photo.y = Math.min(minY, Math.max(maxY, photo.y));
Y aquí es donde estoy teniendo problemas.
Cuando el área de cultivo no está centrada dentro de la foto, terminamos con el área de cultivo moviéndose fuera de los límites de la foto, de esta manera:
Recuerde, el origen de la rotación es siempre exactamente el centro del área de cultivo.
En la foto de arriba puede ver que el ancho requerido se calcula correctamente y la foto se escala correctamente para abarcar el área de cultivo.
Sin embargo, dado que nuestra función de sujeción solo verifica los bordes del área de cultivo, terminamos con nuestro área de cultivo fuera de los límites de las fotos.
Nos gustaría modificar nuestra función de sujeción para que tenga en cuenta la rotación del rectángulo de la photo
. Por lo tanto, las coordenadas x
e y
(en la captura de pantalla de arriba) deben ajustarse correctamente para garantizar que el resultado final se vea así:
Desafortunadamente, no tengo idea de por dónde empezar con las matemáticas y realmente podría necesitar ayuda.
Actualmente utilizamos la misma función de sujeción mientras giramos y movemos el rectángulo de la photo
, y nos gustaría continuar haciéndolo si es posible.
Que pasa
En lugar de fijar las coordenadas de la foto en el sistema de coordenadas original, debe sujetarlas en el sistema de coordenadas girado. El uso de las coordenadas de las esquinas del área de recorte (al calcular minX
, maxX
, minY
y maxY
) en el sistema de coordenadas girado no tiene sentido.
Sujetando las coordenadas de la foto
Debe construir un cuadro delimitador alineado con el eje del área de recorte en el sistema de coordenadas rotado que puede usar para la fijación. Al igual que:
Como estamos girando alrededor del centro del área de cultivo, las coordenadas del centro permanecen igual. Sin embargo el ancho y la altura aumentarán.
Tenga en cuenta que ya ha calculado estas cantidades para escalar la foto de forma adecuada. En resumen, solo puede reemplazar el ancho crop.w
y el alto crop.h
utilizado al escalar por los obtenidos al calcular el cuadro delimitador ( bbox.w
y bbox.h
):
var maxX = crop.x + (bbox.w/2) - (photo.w/2);
var maxY = crop.y + (bbox.h/2) - (photo.w/2);
var minX = crop.x - (bbox.w/2) + (photo.w/2);
var minY = crop.y - (bbox.h/2) + (photo.h/2);
center = {
x: Math.min(minX, Math.max(maxX, photo.x)),
y: Math.min(minY, Math.max(maxY, photo.y))
};
Tenga en cuenta que las coordenadas obtenidas en las dos últimas líneas solo funcionan en el sistema de coordenadas rotado. Si los necesita en el sistema de coordenadas original, todavía tiene que rotarlos de nuevo:
var sin = Math.sin(-rotation);
var cos = Math.cos(-rotation);
photo.x = crop.x + cos*(center.x - crop.x) - sin*(center.y - crop.y);
photo.y = crop.y + sin*(center.x - crop.x) + cos*(center.y - crop.y);
Como dijo DARK_DUCK, hay formas (formas no triviales) de escribir una función de clamp
que resuelva su problema. Sin embargo, cuando dice "abrazadera", asumo automáticamente que su solución debe restringir a los usuarios para que alcancen algunos estados no válidos, por lo que IMHO no es una buena solución. Tal vez un usuario rotará la imagen que pasa a través de algunos estados no válidos solo para alcanzar un estado válido.
Propongo una solución alternativa:
- Almacene el último estado válido con cada cambio (rotación, traducción, escala)
- Cuando el usuario finaliza su cambio, no hace nada si el estado actual es válido o si restablece el último estado válido.
El estado constará de origen de la imagen (o posición), rotación y escala.
Puede animar la transición de un estado no válido al último estado válido. Mientras se encuentre en un estado no válido, puede informar al usuario haciendo que el borde del área de recorte sea rojo:
Un estado válido es cuando las 4 esquinas del área de recorte están dentro del rectángulo de la imagen. Las siguientes funciones te ayudarán a probar esta condición:
//rotate a point around a origin
function RotatePoint(point, angle, origin)
{
var a = angle * Math.PI / 180.0;
var sina = Math.sin(a);
var cosa = Math.cos(a);
return
{
x : origin.x + cosa * (point.x - origin.x) - sina * (point.y - origin.y),
y : origin.y + sina * (point.x - origin.x) + cosa * (point.y - origin.y)
}
}
//position of point relative to line defined by a and b
function PointPos(a, b, point)
{
return Math.sign((b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x));
}
Tienes que:
- Calcule la posición de las esquinas del rectángulo de la imagen después de aplicar rotaciones y traslaciones.
Verifique si las esquinas del rectángulo de recorte están dentro del rectángulo de la imagen usando la segunda función proporcionada anteriormente. Un punto x está dentro del rectángulo si
PointPos (a, b, x) == -1 && PointPos (b, c, x) == -1 && PointPos (c, d, x) == -1 && PointPos (d, a, x) == - 1;
Edición: Aquí hay un ejemplo: JSFiddle
Los dos rectángulos tienen diferentes orígenes. El origen de rotación del rectángulo externo ( pictureArea
) es el origen del interno ( cropArea
).
¡Buena suerte!
En realidad, no puede "sujetar" los valores x e y porque x e y son interdependientes. Lo que quiero decir es que puede resolver un problema (imagen fuera de límite) cambiando el valor x de la imagen O el valor y O una combinación lineal infinita de valores x e y.
Por lo tanto, la función de sujeción devolvería un conjunto infinito de valores correctos (o ninguno si no hay solución). Si desea una solución a su problema, quizás deba agregar más condiciones a la especificación de la función de sujeción. Se podría decir, por ejemplo, que la función de sujeción debe minimizar el max(displacementX, displacementY)
. Si agrega esta condición, la función de sujeción devolverá una solución única o ninguna.