Pulling my hair out on this one. I have a 5 star rating system for products in my app and I am having a heck of a time trying to get the right CSS animations to apply to the one star that shows the partial coloring.
Example: rating is 3.3, three fully colored stars, then the 4th star would only be 30% colored
I am trying to apply partial color to the 4th star using CSS -webkit animations but every technique I am using doesn't work or has some issue.
Star Icons:
<div class="" style="font-size:14px;">
<i class="ion-ios-star" id="Rating_{{prod.prodID}}_1" ></i>
<i class="ion-ios-star" id="Rating_{{prod.prodID}}_2" ></i>
<i class="ion-ios-star" id="Rating_{{prod.prodID}}_3" ></i>
<i class="ion-ios-star" id="Rating_{{prod.prodID}}_4" ></i>
<i class="ion-ios-star" id="Rating_{{prod.prodID}}_5" ></i>
{{prod.Rating}} out of {{prod.RatingCount}} reviews
</div>
Statically, applying the ratingStarA class to my element works, all of the attributes are there, even if they are crossed out leaving only the valid ones to make the animation work:
.ratingStarA {
background:-moz-linear-gradient(top, #e72c83 30%, #a742c6 70%);
background: -webkit-linear-gradient(top, #e72c83 30%,#a742c6 70%);
background: linear-gradient(to bottom, #e72c83 30%,#a742c6 70%);
-webkit-background-clip: text;
-moz-background-clip: text;
background-clip: text;
-webkit-text-fill-color:transparent;
}
.ratingStarB {
-webkit-background-clip: text;
-moz-background-clip: text;
background-clip: text;
-webkit-text-fill-color:transparent;
}
But I need to dynamically apply the background because I need to pass in the percent values and this where things get wonky. The -webkit-background-clip style is the one that seems to be causing all the problems. So far, any method I have tried to use to apply it dynamically fails.
METHOD A: this seemingly works, but the 'background' is crossed out and the '-webkit-background-clip' is missing when I inspect the element...BUT...the animation is working - I just don't trust it or understand why this is happening.
function colorStars(ID,rating) {
// for the partial star, rating is passed in as ".3"
var colorPer = rating * 100 ;
var whitePer = 100 - colorPer ;
var star = document.getElementById(ID) ;
star.style.background = "-webkit-linear-gradient(right," +eColors.orange+ " " +colorPer+ "%, lightgray " +whitePer+ "%)" ;
star.style['webkit-background-clip'] = "text" ;
star.style['webkit-text-fill-color'] = "transparent" ;
}
METHOD B: just setting the background and then apply a class, but all the '-xxx-background-clip' are missing from the element after the class is applied and thus the partial star is not visible.
function colorStars(ID,rating) {
// for the partial star, rating is passed in as ".3"
var colorPer = rating * 100 ;
var whitePer = 100 - colorPer ;
var star = document.getElementById(ID) ;
star.style.background = "-webkit-linear-gradient(right," +eColors.orange+ " " +colorPer+ "%, lightgray " +whitePer+ "%)" ;
start.classList.add('ratingStarB') ;
}
METHOD C: How to apply multiple backgrounds for cross browser support? I can't apply multiple backgrounds dynamically, like in a class, because the last one overwrites the first two
function colorStars(ID,rating) {
// for the partial star, rating is passed in as ".3"
var colorPer = rating * 100 ;
var whitePer = 100 - colorPer ;
var star = document.getElementById(ID) ;
star.style.background = "-moz-linear-gradient(right," +eColors.orange+ " " +colorPer+ "%, lightgray " +whitePer+ "%)" ;
star.style.background = "-webkit-linear-gradient(right," +eColors.orange+ " " +colorPer+ "%, lightgray " +whitePer+ "%)" ;
// because this is the last 'background' applied, it overwrites the first two
star.style.background = "linear-gradient(to right," +eColors.orange+ " " +colorPer+ "%, lightgray " +whitePer+ "%)" ;
//as well, only the '-webkit-text-fill-color' is applied, the others are not there.
star.style['-moz-background-clip'] = "text" ;
star.style['-webkit-background-clip'] = "text" ;
star.style['background-clip'] = "text" ;
star.style['-webkit-text-fill-color'] = "transparent" ;
}
Again, Method A seems to work, but viewing the elements shows the styles crossed out or missing - and it doesn't support cross browser support.
Any help is appreciated.
This can be achieved using masking inside a SVG element and a little javascript found here
function setProgress(amt) {
amt = (amt < 0) ? 0 : (amt > 1) ? 1 : amt;
document.getElementById("stop1").setAttribute("offset", amt);
document.getElementById("stop2").setAttribute("offset", amt);
}
let val = 0;
function colorStars() {
const rating = 2.65, // usually dynamic value
flatten = rating / 5; // to fit within 0-1 range
setProgress(val);
val += 0.01;
if (val <= flatten) {
setTimeout(colorStars, 30);
}
}
body { display: flex; align-items: center; gap: 1rem }
<svg viewBox="0 0 120 24" width="120" height="24">
<g mask="url(#mask)">
<path id="star" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
<use xlink:href="#star" x="24" y="0" />
<use xlink:href="#star" x="48" y="0" />
<use xlink:href="#star" x="72" y="0" />
<use xlink:href="#star" x="96" y="0" />
</g>
<defs>
<linearGradient id="progress" x1="0" y1="0" x2="1" y2="0">
<stop id="stop1" offset="0" stop-color="lightblue" />
<stop id="stop2" offset="0" stop-color="green" />
</linearGradient>
<mask id="mask" x="0" y="0" width="1" height="1">
<rect x="0" y="0" width="120" height="100" fill="url(#progress)" />
</mask>
</defs>
</svg>
<button onclick="colorStars()">Color Stars</button>
Related
I'm making a progressive line bar using SVG elements, my logic is that for every number of inputs , the line gets divided on even parts and each part will get colored by percentage, depending of the number of valid interactions.
In the console I can see the logic is working as it should, however the coloring of the line is not.
I'm not fully sure how can I make that the progress gets colored from left to right progressively, right now its behaviour doesn't follow the desired path.
function updateBar() {
//Path that will be painted
var myProgress = document.getElementById("myProgress");
//Reference the number of inputs
var numberOfInputs =document.getElementById("totalInput").value;
//number that we will use to divide the valid interaction with the inputs
var interactionTimes = document.getElementById("validInput").value;
//getting the # to get the right percent to paint the line
var lineDivision = [(100 / numberOfInputs)];
console.log("this is lineDivision:" + lineDivision);
//and then with this lineDivision variable, replace the 20 from the previous code. But here is where I have the main problem, because as result of every click I do in a checkbox the var percent always get 100 and as result the ring is already fully colored.
var percent = (interactionTimes) * (lineDivision);
console.log("this is the percent:" + percent);
myProgress.style.strokeDashoffset = 100 - percent;
if (interactionTimes === numberOfInputs) {
myProgress.style.stroke = "#1DEBA1";
} else if (interactionTimes < numberOfInputs) {
myProgress.style.stroke = "purple";
}
return true;
}
checks = document.querySelectorAll("input[type='number']");
checks.forEach(function(paint) {
paint.addEventListener("change", function() {
updateBar()
});
});
#mySvg {
transform: rotate(-180deg);
}
#myRect {
width: 0;
height: 30px;
stroke: #E3E5E7;
stroke-width: 3px;
}
#myProgress {
margin: auto;
width: 50%;
/*stroke: red;*/
stroke-width: 3px;
stroke-dasharray: 100;
stroke-dashoffset: 100;
stroke-linecap: square;
}
<svg id="line-progress" height="4" width="300">
<g >
<line class="myRect" id="myRect" x1="0" y1="50%" x2="100%" y2="50%" stroke-width="4" fill="transparent" />
</g>
<g >
<line class="myProgress" id="myProgress" x1="0" y1="50%" x2="100%" y2="50%" pathLength="100" fill="transparent"/>
</g>
</svg>
<form>
<label> Total inputs to interact with
<input id="totalInput" type="number" min="0" max="100">
</label>
<br>
<label> Number of valid inputs
<input id="validInput" type="number" min="0" max="100">
</label>
</form>
<script src="script.js"></script>
I have a usecase where designers supply us with a SVG, and we use certain elements in that SVG to position our dynamically created elements.
In the snippet below I try to overlap the rect#overlayTarget with the div#overlay using getBoundingClientRect: it doesn't take the scaling of the parent element into account, and the elements don't overlap.
The answers from this question is not applicable here as it uses element.offsetLeft and element.offsetTop, which aren't available for SVG: How to compute getBoundingClientRect() without considering transforms?
How do I make the #overlay and #overlayTarget overlap?
const target = document.querySelector("#overlayTarget");
const position = target.getBoundingClientRect();
const overlay = document.querySelector("#overlay");
overlay.style.top = `${position.y}px`;
overlay.style.left = `${position.x}px`;
overlay.style.width = `${position.width}px`;
overlay.style.height = `${position.height}px`;
#overlay {
position: absolute;
background: hotpink;
opacity: 0.3;
width: 100px;
height: 100px;
}
<div id="app" style="transform: scale(0.875);">
Test
<div id="overlay"></div>
<svg xmlns="http://www.w3.org/2000/svg" width="1809" height="826" viewBox="0 0 809 826">
<g
id="Main_overview"
data-name="Main overview"
transform="translate(-49.5 -155)"
>
<g
id="overlayTarget"
data-name="DC-DC converter"
transform="translate(400 512)"
>
<rect
id="Rectangle_29"
data-name="Rectangle 29"
width="74"
height="74"
fill="none"
stroke="#47516c"
stroke-width="2"
/>
</g>
</g>
</svg>
</div>
If you cannot set your overlay element outside of the transformed element, this answer will work, but only for some simple transformations:
translations and
scales with factors > 0
In these cases, the corners of the bounding box aren't moved out of their top/left and bottom/right orientation. Rotations or skews, and most of the 3D transforms won'T work out.
You can then compute the resulting box values for your overlay by transforming the corners of position with the inverse matrix to that set for the #app element. The DOMPoint and DOMMatrix interfaces help with that.
It is important to remember that transform sets an implicit position: relative, so the top and left values of the overlay are not in relation to the viewport.
const app = document.querySelector('#app');
const relative = app.getBoundingClientRect();
const target = document.querySelector("#overlayTarget");
const position = target.getBoundingClientRect();
const matrix = new DOMMatrix(app.style.transform).inverse();
const topleft = new DOMPoint(
position.x - relative.x,
position.y - relative.y
).matrixTransform(matrix);
const bottomright = new DOMPoint(
position.x - relative.x + position.width,
position.y - relative.y + position.height
).matrixTransform(matrix);
const overlay = document.querySelector("#overlay");
overlay.style.top = `${topleft.y}px`;
overlay.style.left = `${topleft.x}px`;
overlay.style.width = `${bottomright.x - topleft.x}px`;
overlay.style.height = `${bottomright.y - topleft.y}px`;
#overlay {
position: absolute;
background: hotpink;
opacity: 0.3;
width: 100px;
height: 100px;
}
<div id="app" style="transform: scale(0.875);">
Test
<div id="overlay"></div>
<svg xmlns="http://www.w3.org/2000/svg" width="1809" height="826" viewBox="0 0 809 826">
<g
id="Main_overview"
data-name="Main overview"
transform="translate(-49.5 -155)"
>
<g
id="overlayTarget"
data-name="DC-DC converter"
transform="translate(400 512)"
>
<rect
id="Rectangle_29"
data-name="Rectangle 29"
width="74"
height="74"
fill="none"
stroke="#47516c"
stroke-width="2"
/>
</g>
</g>
</svg>
</div>
I have a div containing an SVG image of 300x300 px and a viewbox of 1000x1000.
The image describes a blue rectangle on top of a red one.
When I move the mouse a circle is following the mouse position inside the image:
Everything is perfect except that when I apply a transformation changing perspective and rotation, the mouse pointer and circle center are not anymore matching:
Code is here:
$(function() {
$('#image').mousemove(function(event) {
var svg = document.querySelector('svg');
var pt = svg.createSVGPoint();
pt.x = event.clientX;
pt.y = event.clientY;
pt = pt.matrixTransform(svg.getScreenCTM().inverse());
overlay = document.getElementById('overlay');
$('#overlay').html(
"<circle cx='" + pt.x + "' cy='" + pt.y + "' r='50' stroke='#8f00ff' fill='transparent' stroke-width='10' /></svg>"
);
refresh = $("#overlay").html();
$("#overlay").html( refresh )
});
});
function Transform() {
$('#image').css({
transformOrigin: '500px 500px',
transform: 'perspective(100px) rotateX(5deg)'
});
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id='image' tabindex='0' >
<svg id='svgmap' width='300' height='300' xmlns='http://www.w3.org/2000/svg' version='1.1' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 1000 1000'>
<rect x='0' y='0' width='1000' height='1000' fill='red' />
<rect x='250' y='250' width='500' height='500' stroke='yellow' fill='blue' stroke-width='10' />
<g id='overlay'></g>
</svg>
</div>
<button onclick='Transform()'>Transform</button>
My goal is to preserver matching between the purple circle center and the mouse pointer, even when a transformation is applied to the object.
Is there a way to do it?
In your code #image is a div. In order to make it work you need to apply the transformation to the svg element (#svgmap) and the transformation must be an svg transformation.
$(function() {
$('#svgmap').mousemove(function(event) {
var svg = document.querySelector('svg');
var pt = svg.createSVGPoint();
pt.x = event.clientX;
pt.y = event.clientY;
pt = pt.matrixTransform(svg.getScreenCTM().inverse());
overlay = document.getElementById('overlay');
$('#overlay').html(
"<circle cx='" + pt.x + "' cy='" + pt.y + "' r='50' stroke='#8f00ff' fill='transparent' stroke-width='10' /></svg>"
);
refresh = $("#layer_wafer").html();
$("#layer_wafer").html( refresh )
});
});
function Transform() {
svgmap.setAttributeNS(null,"transform", "skewX(-20) translate(100)");
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id='image' tabindex='0' >
<svg id='svgmap' width='300' height='300' xmlns='http://www.w3.org/2000/svg' version='1.1' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 1000 1000' transform="">
<rect x='0' y='0' width='1000' height='1000' fill='red' />
<rect x='250' y='250' width='500' height='500' stroke='yellow' fill='blue' stroke-width='10' />
<g id='overlay'></g>
</svg>
</div>
<button onclick='Transform()'>Transform</button>
I understand that you are needing a 3D css transformation but this (at least for now) doesn't work.
This is an article where you can read more about 3d transforms in svg: https://oreillymedia.github.io/Using_SVG/extras/ch11-3d.html In the article you can read: All the 3D transformation functions described in this section should be considered “future”
I've solved the issue in the ugliest possible way. Simply hiding the mouse cursor over the div.
Ugly. But effective.
$(function() {
$('#image').mousemove(function(event) {
var svg = document.querySelector('svg');
var pt = svg.createSVGPoint();
pt.x = event.clientX;
pt.y = event.clientY;
pt = pt.matrixTransform(svg.getScreenCTM().inverse());
overlay = document.getElementById('overlay');
$('#overlay').html(
"<circle cx='" + pt.x + "' cy='" + pt.y + "' r='50' stroke='#8f00ff' fill='transparent' stroke-width='10' /></svg>"
);
refresh = $("#overlay").html();
$("#overlay").html( refresh )
});
});
function Transform() {
$('#image').css({
transformOrigin: '500px 500px',
transform: 'perspective(100px) rotateX(5deg)'
});
}
div#image {
cursor: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id='image' tabindex='0' >
<svg id='svgmap' width='300' height='300' xmlns='http://www.w3.org/2000/svg' version='1.1' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 1000 1000'>
<rect x='0' y='0' width='1000' height='1000' fill='red' />
<rect x='250' y='250' width='500' height='500' stroke='yellow' fill='blue' stroke-width='10' />
<g id='overlay'></g>
</svg>
</div>
<button onclick='Transform()'>Transform</button>
There's a little bit of optimization that I have to do to reduce the mouse cursor position gap between the DIV over and any other page element, but it's acceptable to me.
So I have a multilingual website (currently only in french and english), and there are some places where there is multi line content (titles) a bit like this one :
.banner-heading{
font-size: 37.2px;
}
.banner-second-heading{
font-size: 46px;
}
.banner-third-heading{
font-size: 78px;
}
<link href="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet"/>
<div class="flex flexCenter flexColumn col-xs-12 col-md-6 title-fr">
<h1 class="banner-heading">Une solution digitale</h1>
<h1 class="banner-second-heading">pour le bien-être</h1>
<h1 class="banner-third-heading">au travail</h1>
</div>
And the designer wants it to be done without the justify property, because it would deform the font they've chosen.
My problem is I currently do exactly the same for english as well (with different values to keep the text well aligned at beginning and end of lines), but it is already annoying to maintain in the current states, and should we decide to add other languages, it would become almost impossible to have one css PER language, especially with different font-size per line and per language.
So my question is : how do I achieve the same result, in different languages, without using the justify property, given it would deform the chosen font, and without having to play with the font-sizeproperty in the different languages.
Thank you very much for your help, and sorry if this question was already answered, I searched it and didn't find any answer.
I think I under stand your problem, you need to change the size of the font/text to fit the container.
Here is a solution that uses svg and a little JavaScript to achieve that.
First we must use svg to write out the text
<svg width="100%" height="auto" viewBox="0 0 100 20" preserveAspectRatio="xMidYMin slice">
<text class="scalableText" x="0" y="15">Une solution digitale</text>
</svg>
then we have to measure the text and change to viewport to fit the text.
var textElements = document.getElementsByClassName('scalableText');
for(var i = 0;i < textElements.length; i++)
{
var textLength = textElements[i].getComputedTextLength();
textElements[i].parentElement.setAttribute("viewBox", "0 0 " + textLength + " 20");
}
var textElements = document.getElementsByClassName('scalableText');
for(var i = 0;i < textElements.length; i++) {
var textLength = textElements[i].getComputedTextLength();
textElements[i].parentElement.setAttribute("viewBox", "0 0 " + textLength + " 20");
}
<svg width="100%" height="auto" viewBox="0 0 100 20" preserveAspectRatio="xMidYMin slice">
<text class="scalableText" x="0" y="15">Une solution digitale</text>
</svg>
<svg width="100%" height="auto" viewBox="0 0 100 20" preserveAspectRatio="xMidYMin slice">
<text class="scalableText" x="0" y="15">pour le bien-être</text>
</svg>
<svg width="100%" height="auto" viewBox="0 0 100 20" preserveAspectRatio="xMidYMin slice">
<text class="scalableText" x="0" y="15">au travail</text>
</svg>
Example on jsbin
Below uses jQuery to calculate the width of the largest line and then use that width to create a ratio and calculate a new font size
var $wrapper = $('.wrapper');
$wrapper.each(function() {
var $spans = $(this).find('.equalise'),
max = 0,
fontsize = 0;
$spans.each(function() {
var $this = $(this),
width = $this.outerWidth();
if (max < width) {
$spans.removeClass('max');
$this.addClass('max');
max = width;
fontsize = parseInt($this.css('font-size'));
}
});
$spans.not('.max').each(function() {
var $this = $(this),
ratio = max / $this.outerWidth();
$this.css('font-size', fontsize * ratio)
});
});
.equalise {
display: inline-block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="wrapper">
<span class="equalise">line 1 text</span><br>
<span class="equalise">big font</span><br>
<span class="equalise">a lot more text for to show how this works</span><br>
</div>
<br>
<div class="wrapper">
<span class="equalise">another example</span><br>
<span class="equalise">a line with some text</span><br>
<span class="equalise">short stuff</span><br>
</div>
I am trying to get a color from an element on my screen after the initial color has been ran through -webkit-filter on the CSS. nd apply that color onto another element.
HTML
<div id="Div1" style="background-color: rgb(166,187,207);"></div>
<br>
<div id="Div2"></div>
CSS
#Div1{
height: 100px;
width: 100px;
-webkit-filter: saturate(5);
}
Javascript
var inDiv1 = document.getElementById("Div1");
var dColorx = inDiv1.style.backgroundColor;
var inDiv2 = document.getElementById("Div2");
inDiv2.innerHTML += '<svg width="400" height="110">'+
'<rect width="300" height="100" '+
'style="fill:'+ dColorx +';'+
'stroke-width:3;stroke:rgb(0,0,0)"></svg>';
I need the rect inside of Div2 to be the same color as Div1 currently.
Div1 default color rgb(166,187,207).
Div1 post filter color rgb(94,199,255). <-- need to get this color
I also used a RGB to HSL converters I've found on stackoverflow, but the colors were off by quite a bit. If I'm not making sense, I do apologize. I've been up all night trying to get this to work.
If anyone can help me it would greatly be appreciated!
Example: http://jsfiddle.net/JfKEL/
Not sure how you would get the colour after applying the filter exactly but you can use the same colour for your svg elements and re-apply the filter. Your svg output will be something like this:
<svg width="400" height="110">
<filter id="saturate">
<feColorMatrix in="SourceGraphic" type="saturate" values="5" />
</filter>
<g filter="url(#saturate)">
<rect width="300" height="100" '+ 'style="fill:'+ dColorx + '; stroke-width:3;stroke:rgb(0,0,0)">
</g>
</svg>
Working jsFiddle
With the help of the functions from this post you should be able to achieve what you want using the following:
var inDiv1 = document.getElementById("Div1");
var dColorx = inDiv1.style.backgroundColor.replace(/[^0-9$,]/g, '').split(',');
var hsl= rgbToHsl(dColorx[0], dColorx[1], dColorx[2]);
hsl[1] = hsl[1] * 5;
var rgb = hslToRgb(hsl[0], hsl[1], hsl[2]);
var newColor = 'rgb(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ');'
var inDiv2 = document.getElementById("Div2");
inDiv2.innerHTML += '<svg width="400" height="110">' +
'<rect width="300" height="100" ' +
'style="fill:' + newColor +
';stroke-width:3;stroke:rgb(0,0,0)"></svg>';
Example