I'm trying to transform a square div with a background image to a trapezoid.
I would like to make it in 2D, pretty much the same way the "Distort" tool of Photoshop does.
Basically, all I want is to narrow the top side of the square and get the image to be deformed accordingly.
The 3D transformation "seems" to do the trick:
transform: rotateX(30deg);
It works for most use cases but not all of them.
Indeed, It is a 30deg rotation of the square that "looks" like a trapezoid when seen from the front/back side but is remains a 30° rotated square when seen from any other side.
What I want is to get an actual trapezoid. I want the squared image to be distorted in a 2D way so that the shape and image are actually changed, with no rotation involved.
I tried this and it worked in terms of shape (trapezoid):
border-style: solid;
height: 0;
border-color: transparent transparent red transparent;
border-width: 0 100px 100px 100px;
But then I can't replace the red area with a background-image that would follow the distortion. Which defeats my purpose.
Any attempt I tried gets the picture to remain undeformed.
Is there any css/html5/javascript trick that would achieve what I want?
Thanks.
You can get the effect by applying a 3D transform on a pseudo-element (on which you also set the background-image) and making sure it's flattened in its original plane - that of its parent. This means that if you want to rotate something in 3D, you have to rotate the parent.
Step #1: create a square div, add a pseudo (or a child) that has the exact same dimensions and set the background-image on this pseudo.
div {
display: grid; /* makes pseudo stretch all across */
width: 28em; /* whatever edge value we want */
aspect-ratio: 1; /* make it square */
/* just to highlight div boundaries */
box-shadow: 0 0 0 3px;
&::after {
background: url(image.jpg) 50%/ cover;
content: ''
}
}
Step #2: set the transform-origin on the pseudo to the middle of the bottom edge (100% 50%) - this ensures the bottom edge will remain in place after applying the 3D transform.
Step #3: apply a 3D skew along the z axis lengthening the edge along the y axis.
Yes, we don't have 3D skew functions in CSS. But we have matrix3d(), which can be used to express any rotation, scale, skew, translation!
So let's first understand how skewing works.
Skewing happens along an axis.
Here's an interactive demo illustrating how the 2D skew functions work.
Consider this example, where we skew along the x axis and the edge along the y axis gets lengthened as the y axis rotates away from its initial position - this angle is the skew angle. The z axis is perpendicular onto the plane in which we skew (xOy in this example) and is unaffected:
Well, in our case, we do something similar, but the skew happens in the yOz plane, not in the xOy plane, as we skew along the z axis instead of along the x axis.
Since we've anchored the middle of the bottom edge of our pseudo in place with transform-origin and this skew happens along the z axis (perpendicular onto the screen), it results we're basically pulling and stretching our pseudo back, towards the back of the screen, preserving the x and y coordinates of every point, but changing the z coordinates.
Basically, it would look like below if we were to view it in 3D without flattening into the parent's plane (the parent is bounded by the outline).
You can see how the horizontal guidelines at the top show how the top of the skewed pseudo has preserved its x and y coordinates, it just got pulled back along the z axis.
Alright, how do we CSS this?
As mentioned, there's no 3D skew, but we can build our transform matrix ourselves. Since this is a skew along the z axis (3rd axis) stretching the edge along the y axis (2nd axis), the only position in our matrix different from the unit matrix (1 along the main diagonal, 0 elsewhere) is going to be on the 3rd row, 2nd column. And we're going to have the tangent of the skew angle there. On MDN, you can see this for skewX() and skewY() too.
This is because every point along the skew axis gets displaced by its coordinate along the lengthening axis times the tangent of the skew angle - you can see this in the first illustration if you draw parallels to the axes (x axis, y axis pre- and post-skew) through the example point in its original position (in grey) and final position (in black). Drawing these parallels creates a right triangle where the x displacement over the y coordinate is the tangent of the skew angle.
Okay, back to the matrix, it looks like this.
1 0 0
0 1 0
0 tan(a) 1
To get the matrix3d() values, we add one more row and one more column identical to what they'd be in a 4x4 unit matrix and then just list the values column by column (not row by row!). So far, we have:
#use 'sass:math'; // allows us to use trigonometric functions
$a: 60deg; // the skew angle
div {
display: grid;
width: 28em;
aspect-ratio: 1;
perspective: 25em;
box-shadow: 0 0 0 3px;
&::after {
transform-origin: 50% 100%;
transform: matrix3d(1, 0, 0, 0, /* 1st column */
0, 1, math.tan($a), 0, /* 2nd column */
0, 0, 1, 0, /* 3rd column */
0, 0, 0, 1);
background: url(image.jpg) 50%/ cover;
content: ''
}
}
Note we've also added a perspective to get the distorted view (smaller at the top/ further back).
The code so far gives us the flattened version of what we can see in the gif above. And I say the flattened version because, with what we have here, the pseudo always gets flattened in the plane of its parent.
When the parent div has no 3D transform, we look at it from the front and the pseudo obviously looks flattened.
When the parent div does have a 3D transform, its 3D-transformed pseudo gets flattened into its plane because the default transform-style value is flat. This means that any 3D-transformed children/ pseudos of a 3D transformed parent get flattened in the plane of the parent. This can be changed if we set the div's transform-style to preserve-3d. But we don't want that here.
Step 4: fix the top edge!
There's just one more thing that still doesn't look right: the post-transform top edge is now below the original one.
This is because we've set a perspective and how this works. By default, the perspective-origin is dead in the middle of the element we set it on (in this case our div), at 50% horizontally and 50% vertically.
Let's consider just the points behind the plane of the screen because that's where our entire 3D-skewed pseudo is.
With the default perspective-origin (50% 50%), only the points on the line perpendicular onto the plane of the screen in the very middle of our div are going to be projected onto the screen plane at a point with the same x,y coordinates as their own after taking into account perspective. Only the points in the plane perpendicular onto the screen and intersecting the screen along the horizontal midline of the div are going to be projected onto this horizontal midline after taking into account perspective.
Do you see where this is going? If we move the perspective-origin so that it's in the middle of the div's top edge (50% 0), then the points in the plane perpendicular onto the screen along this top edge are going to be projected along this top edge - that is, the top edge of the 3D-skewed pseudo will be along the same line as its parent's top edge.
So our final code is:
#use 'sass:math'; // allows us to use trigonometric functions
$a: 60deg; // the skew angle
div {
display: grid;
width: 28em;
aspect-ratio: 1;
perspective-origin: 50% 0;
perspective: 25em;
box-shadow: 0 0 0 3px;
&::after {
transform-origin: 50% 100%;
transform: matrix3d(1, 0, 0, 0, /* 1st column */
0, 1, math.tan($a), 0, /* 2nd column */
0, 0, 1, 0, /* 3rd column */
0, 0, 0, 1);
background: url(image.jpg) 50%/ cover;
content: ''
}
}
Here is a live comparative view between our result and its pre-transform version as both divs rotate in 3D to show they're flat in the xOy plane.
Don't want to use a preprocessor for the tangent value? Firefox and Safari support trigonometric functions by default already and Chrome 111+ supports them with the Experimental Web Platform features flag enabled in chrome://flags.
Don't want to wait for Chromium support either? You don't even need to use a tangent computation there, you can use any positive number - the bigger this number gets, the smaller the top edge gets. I used the tangent value to illustrate where it comes from, but you don't have to. Our tangent values are computed for angles from 0° to 90°. This gives us tangent values from 0 to +Infinity. So yeah, any positive number will do there in the matrix.
Related
Lets say there's a drop shadow for the following div element.
div {
width: 100px;
height: 100px;
background-color: red;
box-shadow: 60px 60px 50px black;
}
what does 50px mean to that element. That the size of kernel (no of pixels accounted for in calculation of new pixel value) is 50 ?
How does the standard deviation (by default half the blur radius) changes the outcome ?
Is it just the pixels that belong to the area (50px from the edge) that are blurred or the whole shadow?
Why after a certain maximum value of blur radius the effect is unchanged?
In my understanding the blur is generated from the middle of the shadow kernel point. So 25px outward and 25px inward. This is what I see when adding a fourth 'spread' property.
In my experiments I didn't notice a value cap.
Standard definition of the box-shadow blur radius: The blur effect is now defined by css3-background and by HTML to be a Gaussian blur with the standard deviation (σ) equal to half the given blur radius, with allowance for reasonable approximation error.
The pixels nearest the center of the kernel are given more weight than those far away from the center. This averaging is done on a channel-by-channel basis, and the average channel values become the new value for the filtered pixel.
After a definite value the changes are so mere that they are invisible to human eye. That's why it seems like nothing is changing further.
I was in my last years still not able to find a solution, to normalize element positions, after i rotate them.
Here is my example: https://stackblitz.com/edit/typescript-9n8zaq
It's about positioning the upper <div> with transform so that the yellow lower cut edge at each resolution is exactly where the two red lines meet. (Changes the view width.) If the cut edge of the yellow element does not conform to the red line collision point at any width, the formula is wrong)
The width and height I have set to double the value of, otherwise this would not fill the full height and width by the rotate. width and height should therefore remain at these values.
Solely changed, should transform: rotate (-10deg) translate (-25%, calc (-50% - 10%)); in the class .view-area-top
transform: translate(-25%, -50.77%) rotate(-10deg) ;
The horizontal -25% brings the box back to the horizontal midline. The vertical -50.77% positions the lower edge onto the red cross. The number is 50.77% = 50% / cos(10°).
Here is another variant that keeps the center of the lower edge on the cross:
transform: translate(-25%, -50%) translate(0, 50%) rotate(-10deg) translate(0, -50%);
The first translation moves the center of the lower edge onto the cross (without any rotation). The next three transforms are a rotation about the center of the lower edge (translate to the pivot point, rotate, translate back). Of course, you can combine the first two translations, but this will make it a bit harder to read.
I was having an issue where I was attempting to rotate two halves of an image in opposite directions around the y-axis with these two animations:
#-webkit-keyframes first{
0% { -webkit-transform: rotateY(0); }
100% { -webkit-transform: rotateY(-90deg); }
}
#-webkit-keyframes second{
0% { -webkit-transform: rotateY(0);}
100% { -webkit-transform: rotateY(90deg); }
}
Despite the different values for the last keyframe, the two animations rotated in the same direction. Someone rightly pointed out that I needed to apply perspective to my containing space in order to make the effect function (note the checkbox that will apply and remove the perspective from the 3d space to demonstrate):
http://jsfiddle.net/eveQt/12/ - Chrome only
I am curious why this is. From MDN:
The perspective CSS property determines the distance between the z=0 plane and the user in order to give to the 3D-positioned element some perspective. Each 3D element with z>0 becomes larger; each 3D-element with z<0 becomes smaller. The strength of the effect is determined by the value of this property.
From my understanding, perspective moves the viewer along the z plane, making the 3d effect more or less intense. What I don't understand is how moving along the z plane would affect the direction of rotation of an element in such a fashion. I would have thought that perspective would only affect how dramatic the effect is, and not the direction in which the element rotates.
Obviously, application of perspective is a required for the proper rotation direction of each half of the image in my example, but why?
Both halves are rotating correctly around the Y axis, in opposite directions. The trouble is, without perspective, they do not look like they are rotating differently.
Applying perspective in this case basically makes one side of the image larger than the other side, as it rotates around the Y-axis. If there is no perspective, both sides of the image remain the same size, regardless of the direction the image is spinning.
Have a look at the whole image rotating without perspective, and imagine it is rotating in one direction. Then close your eyes, and when you open them again, imagine it is rotating in the other direction. Magic!
For an application with a good lot of icons, I want to make an image sprite.
Since I started my 'adventure' in the land of webdesign/ front-end webdev, I've always wondered: what is the logic behind background-position: (left)<number>px (top)<number>px;
When you compare this to the shorthand property for either padding or margin(when only specifying top and left), these are both property: (top)<number>px (left)<number>px;
So top and left values are reversed.
Also, suppose I have a sprite that is 64px (length) x 16px (height) and contains a total of 4 16x16 icons. To get the second icon in the sprite (|____|_this_|___|____|), you have to type background-image: -16px 0px; instead of 16px 0px (which would be logical, because the second icon starts 16px later than the first one).
If you want an example (I know w3schools is not always correct but it will do for the example): http://www.w3schools.com/css/tryit.asp?filename=trycss_sprites_nav
So my question is: Why are all the values for the background-position property like,... reversed? Is there any logic behind it? Does CSS read the property from right to left?
When using shorthand for margin (or padding) with only two values you are not setting a X/Y position - you are setting four margins, using the same value for top & bottom (vertical margins), as well as right & left (horizontal margins). You can also pass four values and they will start with margin-top and continue clockwise around the box (top -> right -> bottom -> left).
I usually remember this using the word "trouble" without any vowels (TRBL).
Anyway: for positioning there is only two values, and it is common practice to use the vertical axis (x-axis, 0 is top) and then then horizontal (y-axis, 0 is left), so using a negative value for the y-axis on background-position would move a background the same direction you would move the box if you were to give it a negative left margin.
.class1 {
background-position: -20px 0; // move background 20px left
margin-left: -20px; // move box 20px left (margin, following items will also move)
}
.class2 {
position: relative;
left: -20px; // move box 20px left (position, following items will stay put)
}
So I guess what I'm trying to say is that the values are basically coherent, depending on how you look at it ;)
Docs for margin (check the syntax list)
I've run into an issue when using the transform property.
I have 1 simple DIV the fills the screen, and another DIV that sits on top of it.
I'm applying the following transform to the DIV on top:
'transform':'rotateX(Xdeg) rotateY(Ydeg)';
I'm also using preserve-3d
This works perfectly in all browsers except Safari. From what I've read, Safari is the only one that actually gets this correct, and clips the top DIV which the transform is being applied to. The other browsers don't clip the DIV. Basically, when the DIV is rotated, it's going inside/behind the bottom layer DIV.
So, I'm assuming that I need to use translateZ to pull the top DIV forward?
My question is, how would I go about calculating how much I need to translate the div forward along the Z axis, or getting this to work in Safari? The rotate X and Y will be variables, so users might be able to rotate the element along either the X, Y, both or none at all.
Any help on this would be amazing. Thanks!
Are your X and Y values always positive ?
In this case,
transform-origin 0% 0%
should do the trick (making the rotation center be the point that will be the lower in the result)
if not, you only need a little logic:
if your X angle is positive, x origin is 0%, else x origin is 100%
if your Y angle is positive, y origin is 0%, else y origin is 100%
and then, apply
transform-origin x-origin y-origin
I am unable to reproduce your problem without a jsFiddle. You could use a function like element.getBoundingClientRect(). This will give you the current coordinates of the second transformed div. If you compare these with the coordinates of the first div you should find the needed translateZ. You need to compare all 4 corners of the transformed div. A negative DELTA means that that corner is intersecting the first div.