I thought I had pretty decent understanding of SVG, including the viewport, viewBox and the user coordinate system.
In the first example below, we use a viewBox with the same aspect ratio as the viewport. As expected, the user coordinate system rotation does not distort any angles.
In example two, we set the viewbox to a different aspect ratio, compared to the viewport. In other words, when mapping the viewBox to the viewport, the shapes' aspect ratios are not maintained. The bottom-right angle is not distorted from this scaling, which makes sense since the coordinate system origin is at (0,0).
When we rotate the user coordinate system in example two, however, the bottom right angle is distorted. This does not happen in example one.
Edit 1: Just to be clear, the issue is with regards to the bottom right angle in the last example. Before rotating, but after stretching with viewBox, the angle is 90%. After rotating however, it is no longer 90%.
Why does a non-uniformly scaled triangle loose its angles when rotating?
Example One (uniform scale)
body {
height: 500px;
}
svg {
width: 400px;
height: 400px;
border: 1px solid red;
}
<svg id="s1" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 200 200" preserveAspectRatio="none">
<style>
polygon {
transform: translate(100px, 0px);
animation: 2s ease-in 1s 1 normal forwards rotate-down;
fill: green;
}
#keyframes rotate-down {
0% {
transform: translate(100px, 0px) rotate(0deg);
}
100% {
transform: translate(100px, 0px) rotate(45deg);
}
}
</style>
<polygon points="100,100 100,0 0,100" />
</svg>
Example Two (non-uniform scale)
body {
height: 500px;
}
svg {
width: 600px;
height: 400px;
border: 1px solid red;
}
<svg id="s1" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 200 400" preserveAspectRatio="none">
<style>
polygon {
transform: translate(100px, 0px);
animation: 2s ease-in 1s 1 normal forwards rotate-down;
fill: green;
}
#keyframes rotate-down {
0% {
transform: translate(100px, 0px) rotate(0deg);
}
100% {
transform: translate(100px, 0px) rotate(45deg);
}
}
</style>
<polygon points="100,100 100,0 0,100" />
</svg>
EDIT 2 (images to clarify):
Below we see the triangle after viewBox has been added (thus scaled and translated), but before rotating. The bottom right angle is 90 degrees.
Below we see the triangle after viewBox has been added (thus scaled and translated), and after rotating. The bottom right angle is no longer 90 degrees.
EDIT 3:
I eventually got to the bottom of this.
Below is an answer explaining the details and linking to relevant resources.
Hopefully this example will show you what's going on.
Hover over the SVG to see why it is the stretching that is changing the angle.
body {
height: 500px;
}
svg {
width: 200px;
height: 400px;
border: 1px solid red;
transition: 1s width;
}
svg:hover {
width: 600px;
}
<svg id="s1" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 200 400" preserveAspectRatio="none">
<style>
polygon {
transform: translate(100px, 0px) rotate(45deg);
fill: green;
}
</style>
<polygon points="100,100 100,0 0,100" />
</svg>
I finally got to the bottom of this.
The following question, which I posted after concluding what the actual problem was, explains why coordinate transformations behave as they do:
SVG rotate after scale: Order of transforms
In an answer to that question, #TemaniAfif shows how the final transformation matrix is calculated and applied to the graphic element's coordinates, in order to map it from the viewport coordinate system to the final user coordinate system.
Long story short, when applying transformations, what we actually do is copying the current user coordinate system, then translating it in relation to the current user coordinate system we copied from. In SVG, the initial user coordinate system (before viewBox or any transforms) is identical to the initial viewport coordinate system.
The chained / nested transforms are applied to the coordinate system left-to-right / outside-in, to reach a final coordinate system within which the graphical elements can be mapped. Note that nesting transforms have the same effect as chaining transforms on one element.
How does this actually work? Well, every transformation has an pre-defined affine transformation matrix, not related to CSS/SVG. There are several Wikipedia articles showing the matrices, like:
https://en.wikipedia.org/wiki/Transformation_matrix#Affine_transformations
https://en.wikipedia.org/wiki/Affine_transformation
To map the coordinates of an element to a final user coordinate system, we multiple the matrices with each other, left-to-right (the order it was written in the source code), to reach the final transformation matrix.
Note that, since we multiply the transform matrices in the order they are written in our source code, and since AxB is different from BxA when multiplying matrices, the order in which the transformations are written in our source code matters.
Finally, we then multiply the x and y coordinates for our element with this final transformation matrix, to see how each coordinate is mapped from the viewport coordinate system to the final user coordinate system.
For those so inclined, it might be easier to not think about the above and instead just mentally imagine that the chained / nested transforms are applied to the element itself (not to user coordinate systems) right-to-left / inside-out (i.e. opposite order of how it was applied to the coordinate systems).
Whether you imagine mentally that you transform the coordinate systems left-to-right and then map in the graphic element, or you transform the element itself by applying the transforms right-to-left, the end result will be the same.
Relevant Specifications
https://www.w3.org/TR/css-transforms-1/#transform-property
https://www.w3.org/TR/css-transforms-1/#transform-rendering
https://www.w3.org/TR/SVG/coords.html
Note
For this question it does not really matter whether the transforms are applied to SVG elements or to HTML elements. The same transformation mechanics apply.
It seems you think that viewBox is some kind of transform method applied, like others, when computing SVG image, which is not true. What you experience here is transformation applied on the whole SVG element. To apply this transformation a browser needs to have SVG object computed, so all in-SVG transformations are already applied.
This works exactly as scaling raster images:
polygon {
fill: transparent;
stroke-width: 4px;
stroke: black;
}
Base raster image:<br>
<img src="https://i.stack.imgur.com/ZA16O.png">
<br>Stretched raster image:<br>
<img style="height: 150px; width: 300px" src="https://i.stack.imgur.com/ZA16O.png">
<br>Base SVG:<br>
<svg viewBox="0,0,160,160" style="height: 120px" preserveAspectRatio="none">
<polygon points="10,10 150,10 80,150"/>
</svg>
<br>Stretched SVG:<br>
<svg viewBox="0,0,160,160" preserveAspectRatio="none" style="height: 120px; width: 300px;">
<polygon points="10,10 150,10 80,150"/>
</svg>
Only SVG's are drawed after they have been transformed, hence they do not lose quality.
SVG spec actually says (here) that all the transforms applied to SVG element work that way.
Related
I have some strange pieces of svg for animate them, the problem is, them no are a 'img-box-style' so, when I put one in another, rotate, translate to another place, etc. know the transform-origin is important. I do it by hand using porcents and try-catch after I get them. But... there aren't one or two. So...
I need to 'see' the transform-origin point of a svg, I trying by using ::after, adding anothers divs or spans, them inherit and works but not for svg. Finally i try using a <circle> but also, don't inherit, i don't know what to do.
Sometimes I need to put the circle in the middle of the two objects, in another time I need to put where the first fig start maybe x: 10% y: 60%.
Lets said I put put transform : 10% 60%; and see what I doing.
body{margin:3rem;}
#svg-id{height: 75vh; background: rgb(216 86 228 / 25%);}
#svg-id g{
/* transform-box: fill-box; */
transform-origin: center; /* how i can 'see' the transform origin??? */
/* animation: rotate 3s infinite linear; */
}
#svg-id g circle{
fill: red;
transform-origin: inherit; /* how inherit the transform origin??? */
}
#keyframes rotate{
from{transform: rotate(0turn);}
to {transform: rotate(1turn);}
}
<div>
<svg xmlns="http://www.w3.org/2000/svg" id="svg-id" viewBox="0 0 237.79 190.08">
<g>
<polygon points="12.21 129.77 40.5 150.42 136.21 115.91 44.66 90.08 12.21 129.77" />
<polygon points="143.89 114.5 152.67 76.72 216.41 26.34 188.17 112.21 143.89 114.5" />
<circle r=16 />
</g>
</svg>
</div>
This question already has answers here:
Maintaining the final state at end of a CSS animation
(5 answers)
Closed 2 years ago.
I am new to CSS animations and not quite understanding what's going on.
Here is my snippet:
.test-container {
position: relative;
height: 200px;
background-color: pink;
}
.test {
fill: black;
position: absolute;
top: 0;
transform: scale(0.0075);
animation: scale 2s ease-out;
animation-iteration-count: 1;
}
#keyframes scale {
0% {
transform: scale(0.0075);
opacity: 0;
}
100% {
transform: scale(1);
opacity: 1;
}
}
<div class="test-container">
<svg class="test" height="2000" width="2000">
<circle cx="1000" cy="1000" r="1000" />
Sorry, your browser does not support inline SVG.
</svg>
</div>
For some reason the snippet doesn't position the circle properly, but that isn't my issue.
What I would like for the animation to do is to start after 4 seconds and increase to the max size and then stay at that size (instead of going back to the small size).
Also, I would like it if the circle would bound the the container so that it doesn't expand over it like this:
Does anyone know how to achieve this?
I should also point out, that i am using an SVG because when I tried animating with a html circle, it lost it's quality, although that was because I was scaling up rather than down.
The snippet doesn't work as it does on my tests. It's supposed to start like this:
Then after 4 seconds I want it to start getting bigger:
Until it gets to it's maximum size:
The red border is to denote its "container". Imagine there is content above and below; I don't want the circle to ever overlap other content items.
I hope that makes more sense.
Update
Someone said this was answered, but it wasn't. It's not just the forwards state I need.....
I solved this issue myself anyway:
https://codepen.io/r3plica/pen/JjKxjPy
To get the animation to stay where it is at the end you use animation-fill-mode: forwards
To get it to start after 4 seconds use animation-delay: 4s
To stop the enlarged circle spilling out of the container set `overflow: hidden;
The code in the question has very large starting circle with a big reduction done through scaling. I have changed this the other way round so you start with a small circle drawn in the svg element and then expand it as that seemed easier than having to calculate reduction ratios.
How much you want the circle to expand and what you want to do about different aspect ratios of your viewport will be up to you, but here's the snippet that shows the basis of what you are looking for. Just play with the parameters to get the exact look you want (e.g. how big the enlarged circle is to be in relation to the height and/or width of the pink container).
.test-container {
position: relative;
height: 200px;
background-color: pink;
overflow-y:hidden;
}
.test {
fill: black;
position: absolute;
top: 90px;
left:0;
animation: scale 2s ease-out;
animation-iteration-count: 1;
animation-fill-mode: forwards;
animation-delay: 4s;
}
#keyframes scale {
0% {
transform: scale(1);
opacity: 0;
}
100% {
transform: scale(15);
opacity: 1;
}
}
<div class="test-container">
<svg class="test" height="20" width="20">
<circle cx="10" cy="10" r="10" />
Sorry, your browser does not support inline SVG.
</svg>
</div>
Im experimenting svg filter to get nice animations and effects. It'a new world that i had just discovered, but im facing a problem.
Im trying to reproduce this kind of effect: https://www.youtube.com/watch?v=GcSU4xH6_Ro
I dont know how to reproduce the circle with the distortion.
I know i could play with filter and svg but actually if i m applying my svg filter to the box all the div have been affect, instead of this circle.
I wonder if it's possible to make svg act like a "lens"
I made a jsFiddle if you want to see the code: https://jsfiddle.net/wekhz7rb/
I would like to do this with CSS only
<div class="box">
<svg class="svg" xmlns="http://www.w3.org/2000/svg" id="effect"
width="275px" height="275px">
<filter id="noise">
<feTurbulence baseFrequency="0.05" numOctaves="2" result="noise">
</feTurbulence>
<feComposite operator="in" in2="SourceGraphic"></feComposite>
<feDisplacementMap in="SourceGraphic" in2="noise" scale="50">
</feDisplacementMap>
</filter>
<!-- <circle cx="137" cy="137" r="137" fill="red" filter="url(#noise)">
</circle> -->
</svg>
<div class="text">DREAMS</div>
<div class="text">DREAMS</div>
<div class="text">DREAMS</div>
<div class="text">DREAMS</div>
<div class="text">DREAMS</div>
<div class="text">DREAMS</div>
<div class="text">DREAMS</div>
<div class="text">DREAMS</div>
<div class="text">DREAMS</div>
<div class="text">DREAMS</div>
<div class="text">DREAMS</div>
<div class="text">DREAMS</div>
<div class="text">DREAMS</div>
<div class="text">DREAMS</div>
<div class="text">DREAMS</div>
</div>
#import url(//fonts.googleapis.com/css?family=Archivo+Black);
body {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
background-color: #7427FF;
height: 100vh;
width: 100vw;
}
.box {
height: 550px;
width: 550px;
position: relative;
overflow: hidden;
text-align: center;
filter: url(#noise);
// mask: url(#effect);
}
.svg {
position: absolute;
top: 50%;
left: 50%;
transform: translateY(-50%) translateX(-50%);
z-index: 10;
}
.text {
font-family: 'Archivo Black', sans-serif;
display: inline-block;
font-size: 116px;
line-height: 90px;
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background: linear-gradient(0deg, rgba(117,62,255,1) 0%,
rgba(249,37,166,1) 40%, rgba(246,154,180,1) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation-name: translate;
animation-timing-function: linear;
animation-duration: 10s;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
#for $i from 1 through 16 {
&:nth-child(#{$i}) {
animation-delay: #{$i * 1}s
}
}
}
#keyframes translate {
from {
transform: translateY(-90px) translateZ(0)
}
to {
transform: translateY(555px) translateZ(0)
}
}
Yes this is possible. Read up on how feDisplacementMap works.
https://www.w3.org/TR/SVG11/single-page.html#filters-feDisplacementMapElement
All you have to do is make sure the pixels outside the circle cause a displacement of 0. For that the colour channel value has to be 0.5 (128).
So if you X channel is the red channel and the Y channel is the green channel, then making the area outside the circle 50% yellow (ie. #808000), should result in a zero displacement.
Update: How feDisplacementMap works
How feDisplacement works is that it takes a pixel value from the "displacement map" (in2) and uses the colour channels of that pixel determine a location elsewhere in the in image top copy a pixel from.
So, for example, say we were currently looking at pixel (100,200).
Look at the pixel at (100,200) in in2
Determine the displacement offset. Lets say it is (-0.4, 0.5). We'll see how we get these values later.
Multiply the offset by the scale. If scale is 10, the new offset would be (-4, 5)
Get the pixel at (100 - 4, 200 - 5) in in
Write out that pixel value to (100,200) in the output (result).
Working out the offset
Start by looking at the appropriate colour channel (R, G, B, or A) in the in2 pixel. The colour channel that is used for the X and Y offsets is set by using the xChannelSelector and yChannelSelector attributes.
Each of those channels can have a value from 0 to 255. For the R channel 0 means no red, and 255 means full red. For the purpose of out offset calculations, the 0..255 range is converted to a 0..1 range.
+----------------------+------+------------+
| Colour channel value | XC() | XC() - 0.5 |
+----------------------+------+------------+
| 0 (00) | 0 | -0.5 |
| 26 (1A) | 0.1 | -0.4 |
| 128 (80) | 0.5 | 0 |
| 255 (FF) | 1 | 0.5 |
+----------------------+------+------------+
So if xChannelSelector="R" and yChannelSelector="G", then a pixel value of #1AFF00 in our in2 would result in the displacement offset of (-0.4, 0.5) that we used above.
Back to displacement maps of a particular shape
From the above, hopefully you can see that if an in2 pixel channel has a value of 128, that would result in a zero offset. Meaning that the output pixel in result would be copied directly across from the same position in the input (in).
So to create a map with a circular boundary. All you have to do is make all the pixels outside the circle 128.
The problem with feDisplacementMap
Changes in the last couple of years to prevent some security issues related to feDisplacementMap, have meant that it can be quite hard to use right now.
There are still left over bugs from that process. For example: https://bugs.chromium.org/p/chromium/issues/detail?id=798001
Plus there are bugs with feImage: eg. https://bugzilla.mozilla.org/show_bug.cgi?id=455986
You'll likely need to use external images with feImage, and make sure your CORS settings are correct on your web server.
Alternatively using data URLs with feImage seems to work okay. See this CodePen from #enxaneta.
Hope this helped.
I have found this question that's been answered and seems to achieve a radial wipe animation with an SVG.
I am looking to achieve a border: 1px solid green; effect like the following example:
What I would like to know though is if this is possible with pure CSS —that would be ideal.
If it's not achievable with CSS, how would I tackle this type of thing with SVG?
CSS is not the right tool for animations like these. While you can do it with CSS, best is to make use of SVG. For a pure CSS version you could try adapting the snippet provided in my answer here but I wouldn't really be recommending it because as you can see it is very complex.
All you have to do is use a circle element, set its stroke-dasharray equal to the circumference of the circle and then animate the stroke-dashoffset like in the below snippet.
The stroke-dasharray property creates a dotted line stroke for the cirlce (the border) where each of the stroke and the dash between them will have the length as specified for the property.
The stroke-dashoffset property specifies the offset at which the circle's stroke should start. When the offset is at 0, the green colored stroke is visible whereas when the offset is at 314 (equal to the circumference), the dash in between strokes become visible. Thus it ends up producing a wipe effect.
svg {
height: 100px;
width: 100px;
transform: rotate(-90deg);
}
circle {
stroke: green;
fill: none;
stroke-dasharray: 314; /* equal to circumference of circle 2 * 3.14 * 50 */
animation: wipe 2s linear infinite;
}
#keyframes wipe {
0% {
stroke-dashoffset: 0;
}
30%, 50% {
stroke-dashoffset: 314;
}
80%, 100% {
stroke-dashoffset: 0;
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/prefixfree/1.0.7/prefixfree.min.js"></script>
<svg viewBox='0 0 100 100'>
<circle cx='50' cy='50' r='40' />
</svg>
The above sample uses an infinite animation and so the wipe and repaint would run continuously. If it has to be toggled on/off then it would be better to use transition like in the below snippet. I have done this on :hover but you can easily adapt it to click or other events.
svg {
height: 100px;
width: 100px;
transform: rotate(-90deg);
}
circle {
stroke: green;
fill: none;
stroke-dasharray: 314; /* equal to circumference of circle 2 * 3.14 * 50 */
stroke-dashoffset: 0; /* initial setting */
transition: all 2s;
}
svg:hover circle{
stroke-dashoffset: 314;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/prefixfree/1.0.7/prefixfree.min.js"></script>
<svg viewBox='0 0 100 100'>
<circle cx='50' cy='50' r='40' />
</svg>
I have a <div> that has a background-color sitting in front of an image. I'm trying to apply a multiply effect using an SVG so that the background image behind the div comes through:
<svg>
<filter id="multiply">
<feBlend mode="multiply"/>
</filter>
</svg>
Unfortunately, only the solid background color is being changed, and I get no transparency through to the background.
Here's the fiddle: https://jsfiddle.net/0p58bxsp/1/
The effect I'm expecting is something like this:
I know the visual effect could be fudged using an rgba value as the background color, but I'm very specifically looking for the combination of a solid color having a multiply filter applied to it.
Is this a limitation of the current SVG implementation?
Here's the definition of a multiply blend:
Multiply blend mode multiplies the numbers for each pixel of the top layer with the corresponding pixel for the bottom layer. The result is a darker picture. , where a is the base layer value and b is the top layer value.
As such, using opacity or alpha doesn't give me the exact result that I'm looking for.
This was supposed to be possible if you stayed completely within SVG 1.1 by using the BackgroundImage pseudo input - but only IE10 ever supported it for inline SVG (Opera supported it for .svg files). Firefox, Chrome & Safari never supported it, and it's now formally declared "not to be implemented" by folks who work on those browsers.
You can import a copy of the background image using feImage and position it just right to match the actual background exactly. But depending on your design - that could take extensive javascript. And url() filters can behave strangely. https://jsfiddle.net/0p58bxsp/3/ shows how to do this - but it also surfaces a regression bug in Chrome with positioned primitives which will be fixed in next Chrome.
<div id="background">
<div id="effect">
Effect goes here
</div>
</div>
<svg width="100%" height="100%" viewBox="0 0 200 200">
<defs>
<filter id="multiply" x="0%"
y="0%" height="300%" width="300%">
<feImage x="0" width="400" height="400" y="0" preserveAspectRatio="xMaxYMax meet" xlink:href="http://lorempixel.com/400/400/" />
<feOffset dx="-100" dy="-100"/>
<feBlend mode="multiply" in="SourceGraphic"/>
</filter>
</defs>
</svg>
Safari 9(and only Safari 9) has a "backdrop-filter" that will do this, and you can also specify a mix-blend-mode which has broader support, but this is still pretty edge.
If you need a multiply blend, it might be best to just keep everything in SVG.
You should add opacity: 0.4; into effect class. It will work fine.
#effect {
color: #fff;
height: 200px;
margin: 100px 0 0 100px;
width: 200px;
opacity: 0.4;
background-color: #3d3934;
filter: url(#multiply);
-webkit-filter: url(#multiply);
-moz-filter: url(#multiply);
}
As far as I know, svg effects are limited to the elements itself. You can't use it to mix with another element.
You can however get this effect with standard CSS mix-blend-mode
#background {
background-image: url(http://lorempixel.com/400/400/);
height: 400px;
padding: 1px;
width: 400px;
}
#effect {
color: #fff;
height: 200px;
margin: 100px 0 0 100px;
width: 200px;
background-color: gray;
mix-blend-mode: multiply;
}
<div id="background">
<div id="effect">
Effect goes here
</div>
</div>