简体   繁体   中英

JavaScript continiously changing colour of SVG element - flickers

I have an element and I'm changing its colour with timer:

r = 0.05;
delta = 0.05;
function changecol(){
  if (r < 0.05 || r > 0.95) delta = delta * (-1);
  r += delta;
  col = '#'+(Math.floor(r*255)).toString(16)+(Math.floor(r*255)).toString(16)+(Math.floor(r*255)).toString(16);
  svg_elem.style.fill = col;
  setTimeout("changecol()", 50);
}

So colour changes from white to black and back, r*255 keeps it between 00 and FF, everything goes smoothly but when it goes to Black, it flickers to White and starts ascending up basically. Have I missed some error in calculations?

here's a jFiddle with a demo: https://jsfiddle.net/b4f58bz2/

I ran a console.debug on your jsfiddle and I found that the result of (Math.floor(r*255)).toString(16) when it flickers is 'C' in hex, just before '0'. The problem is that when translated to css color it's: #ccc which equals to #cccccc which is a very light color instead of a dark one. You have to pad the result of (Math.floor(r*255)).toString(16) with a leading 0 if its length is less than 2, like:

color = (Math.floor(r*255)).toString(16);
if (color.length < 2) {
  color = "0" + color;
}

then just make:

col = '#' + color + color + color;

I hope this gets you on the right track.

This took a bit.

The problem is your conversion of toString(16), you aren't considering the possibility that your hex code is too short.

 R = hex.length == 1 ? "0" + hex : hex;

Please see:

 r = 0.05; delta = 0.05; function changecol() { if (r < 0.05 || r > 0.95) delta = delta * (-1); r += delta; hex = (Math.floor(r * 255)).toString(16); R = hex.length == 1 ? "0" + hex : hex; col = '#' + R + R + R; document.getElementById('test').style.backgroundColor = col; setTimeout("changecol()", 300); } document.addEventListener('DOMContentLoaded', changecol, false); 
 <div id="test"> ADFASDF </div> <div id="test1"> ADFASDF </div> 

The other poster already mentioned your bug in encoding small values, so that #0C0C0C for example is encoded as #CCC wich actually represents #CCCCCC .

Besides that your code has some more bad practices in it.

First: use local variables! some may be "needed" global, but col for example has no reason at all, to be made global. That's just environmental pollution.

Next: pass the function to setTimeout, not a string that has to be parsed.

setTimeout(changecol, 50);

It's faster, it's cleaner, it can be encapsulated, it can be minified, ... only benefits.

Then, with a little trick, you can rewrite your color-encoding to be way nicer:

var c = 255 * r;
var col = "#" + (0x1000000 | c << 16 | c << 8 | c).toString(16).substr(1);

the 0x1000000 is padding, so that the values always exceed 6 digits, followed by the three color channels (r, g, b).

This value is converted to a hex-value, having a 1 in front of it from the padding, wich then is removed by the substr(1). Therefore we have always exactly 6 hexadecimal digits, 2 per color-channel, no worrying about leading zeroes and stuff.

And the bit-operations also strip all decimal-places that c might have.

And don't use body-onload to start this thing. If you really have to, use jQuery ($(changecol)) or search for a DOMContentLoaded-implementation, ... or just put the script at the end of the body and know therefore that is executed after all the html is parsed, and the dom is built.


But imo. we can do even better than that. We can make this thing a function of time, instead of incrementing and decrementing the value, and giving it a padding of 5% so you don't go over the borders, ...

var startTime = Date.now();
var speed = 255 / 1000;   //255 shades of gray / 1000ms

//this means every 2 seconds this animation completes 
//a full cycle from black to white to black again.
//or you slow the speed down to `2*255 / 30000` for example
//1cycle per 30seconds

function changecol(){
    //first we calculate the passed time since the start (in ms) 
    //and then we multiply it by the speed by wich the value should change
    //the amount of color-steps per ms
    var t = (Date.now() - startTime) * speed;

    //now we get the fraction of 256 wich defines the shade
    /*
        var c = Math.floor(t % 256);
    */
    //since we're dealing with powers of 2 we can use a bit-mask to extract just 
    //the last 8 bit of the value and do basically the same, including the Math.floor()
    var c = t & 0xFF;

    //every other interval we have to change the direction, so that we fade back from white to black
    //and don't start over from 0 to 255 (black to white)
    /*
        var interval = Math.floor(t / 256);  //get the interval
        if(interval % 2 === 1)               //check wether it's odd
            c = 255 - c;  //revert the direction
    */
    //but again we can do easyer by simply checking the 9th bit. 
    //if it's a 1, this is an odd interval. the 9th bit equals 0x100 (256).
    if(t & 0x100) //mask only the 9th bit. returns either 256 (wich is true) or 0 (false)
        c = 0xFF - c;  //revert the direction

    var col = "#" + (0x1000000 | c << 16 | c << 8 | c).toString(16).substr(1);
    test.style.color = col;

    //using animation-frames to stay in sync with the rest of animation in the browser.
    requestAnimationFrame(changecol);
}
changecol();  //start this thing, don't use body-onload

To change from black to white you can simply use the Lightness part of HSL with 0% saturation, giving that as color argument:

Replacing setTimeout() with requestAnimationFrame() will also make the animation much smoother as this will sync to the monitor vblank updates. Just compensate by reducing delta to about half.

Example using a div

 var l = 5; var delta = 2.5; (function changecol() { if (l < 5 || l > 95) delta = -delta; l += delta; d.style.backgroundColor = "hsl(0,0%," + l + "%)"; requestAnimationFrame(changecol); })(); 
 <div id=d>Ping-pong fade</div> 

You could do something like this instead, that way you don't have to try and figure out the correct HEX code.

 var whatColor = true; fadeColor = function() { var color = whatColor ? 'white' : 'black'; $('svg circle').css({fill: color}); whatColor = !whatColor; setTimeout(function() { fadeColor(); }, 1000); } fadeColor(); 
 svg circle { -webkit-transition: fill 1s; transition: fill 1s; fill: black; } 
 <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> <svg class="mySVG" height="100" width="100"> <circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" /> </svg> 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM