learningjavascriptks learningjavascriptks - 1 month ago 9
Javascript Question

CSS Transition doesn't affect transform rotate?

I am trying to use transition on the seconds ticking in a clock code to make it work like an automatic watch like how the seconds tick in this watch, this is the demo and code in codepen.io

function setAttributes(el, attrs) {
for(var key in attrs) {
el.setAttribute(key, attrs[key]);
}
}

function setAttrib(id, val) {
// body...
var v = 'rotate('+val+',220,220)';
if(id==='sec') {
setAttributes(document.getElementById(id), {"transform": v, "transition": "transform 13s ease"});
} else {
document.getElementById(id).setAttribute('transform', v);
}
};


I tried to use milliseconds on the seconds tick' but it doesn't work. I only want to animate the seconds tick' to work like an automatic clock, I tested other transitions in it but I see no effect. I don't know what's wrong with this code? Help?

Answer

Using CSS transitions: (not recommended for the reason mentioned below)

As I had mentioned in my comment, you were using SVG's transform attribute for rotating the line and so transition setting would not have any effect on it. You'd have to use CSS transforms via the style attribute for the transition to work. Below is an extract from the W3C spec on transitions:

The ‘transition-property’ property specifies the name of the CSS property to which the transition is applied.

(emphasis is mine)

You also have to set the transform-origin separately unlike with SVG transform attribute.

The problem with using this approach is that the rotation from 59s to 0s/60s is a change of the rotation angle from 354deg to 0deg which will go in the reverse direction and hence ruin the clock effect.

function clock() {
  // body...
  var d, h, m, s;
  d = new Date;

  h = 30 * ((d.getHours() % 12) + d.getMinutes() / 60);
  m = 6 * d.getMinutes();
  s = 6 * d.getSeconds();

  setAttrib('h', h);
  setAttrib('m', m);
  setAttrib('s', s);
  setAttrib('st', s + 180);

  h = d.getHours();
  m = d.getMinutes();
  s = d.getSeconds();

  (h >= 12) ? setText('suffix', 'PM'): setText('suffix', 'AM');

  if (h != 12) {
    h %= 12;
  }

  setText('hr', h);
  setText('min', m);
  setText('sec', s);

  setTimeout(clock, 1);
};

function setAttributes(el, attrs) {
  for (var key in attrs) {
    el.setAttribute(key, attrs[key]);
  }
}

function setAttrib(id, val) {
  // body...
  var v = 'rotate(' + val + 'deg)';
  if (id === 's') {
    setAttributes(document.getElementById(id), {
      "style": "transform:" + v + "; transform-origin: 220px 220px"
    });
  } else {
    document.getElementById(id).setAttribute('style', "transform:" + v + "; transform-origin: 220px 220px");
  }
};

function setText(id, val) {
  // body...
  if (val < 10) {
    val = "0" + val;
  }

  document.getElementById(id).innerHTML = val;
};

window.onload = clock;
* {
  margin: 0;
  padding: 0;
  font-size: 36px;
}
.analog-clock {
  position: relative;
  display: flex;
  align-items: center;
  text-align: center;
  border: solid green 2px;
  flex-direction: column;
  box-sizing: border-box;
  justify-content: center;
}
#clock {
  stroke: red;
  stroke-width: 2px;
  fill: white;
}
#h,
#m,
#s,
#st {
  stroke: black;
  stroke-linecap: round;
  /**/
}
#h {
  stroke-width: 5.5px;
}
#m {
  stroke-width: 3px;
}
#s {
  stroke-width: 1.5px;
}
#st {
  stroke-width: 1.5px;
}
.time-text {
  text-align: center;
}
#s,
#st {
  transition: transform 1s ease;
}
<div class="analog-clock">
  <svg width='440' height='440'>
    <circle id='clock' cx="220" cy='220' r='218' />

    <line id='h' x1='220' x2='220' y1='220' y2='38' />
    <line id='m' x1='220' x2='220' y1='220' y2='20' />
    <line id='s' x1='220' x2='220' y1='220' y2='12' />
    <line id='st' x1='220' x2='220' y1='220' y2='166' />

    <text x='204' y='30'>12</text>
    <text x='420' y='232'>3</text>
    <text x='210' y='435'>6</text>
    <text x='3' y='232'>9</text>

  </svg>

  <div class="time-text">
    <span id="hr">00</span>
    <span>:</span>
    <span id="min">00</span>
    <span>:</span>
    <span id="sec">00</span>
    <span id="suffix">--</span>
  </div>
</div>


Using higher refresh rate: (recommended)

To overcome the problem with CSS transition but still have an animation effect, you could increase the refresh rate by reducing the time out duration and using milliseconds also into calculation.

The CSS output would be more pleasing on the eye because of the easing effect but achieving it with JS wouldn't be possible as far as I know.

function clock() {
  // body...
  var d, h, m, s, ms;
  d = new Date;

  h = 30 * ((d.getHours() % 12) + d.getMinutes() / 60);
  m = 6 * d.getMinutes();
  s = 6 * d.getSeconds();
  ms = 6 * d.getMilliseconds();

  setAttrib('h', h);
  setAttrib('m', m);
  setAttrib('s', s + (ms == 0 ? 1 : ms / 1000));
  setAttrib('st', s + (ms == 0 ? 1 : ms / 1000) + 180);

  h = d.getHours();
  m = d.getMinutes();
  s = d.getSeconds();
  ms = d.getMilliseconds();

  (h >= 12) ? setText('suffix', 'PM'): setText('suffix', 'AM');

  if (h != 12) {
    h %= 12;
  }

  setText('hr', h);
  setText('min', m);
  setText('sec', s + '.' + ms);

  setTimeout(clock, 1);
};

function setAttributes(el, attrs) {
  for (var key in attrs) {
    el.setAttribute(key, attrs[key]);
  }
}

function setAttrib(id, val) {
  // body...
  var v = 'rotate(' + val + 'deg)';
  if (id === 's') {
    setAttributes(document.getElementById(id), {
      "style": "transform:" + v + "; transform-origin: 220px 220px"
    });
  } else {
    document.getElementById(id).setAttribute('style', "transform:" + v + "; transform-origin: 220px 220px");
  }
};

function setText(id, val) {
  // body...
  if (val < 10) {
    val = "0" + val;
  }

  document.getElementById(id).innerHTML = val;
};

window.onload = clock;
* {
  margin: 0;
  padding: 0;
  font-size: 36px;
}
.analog-clock {
  position: relative;
  display: flex;
  align-items: center;
  text-align: center;
  border: solid green 2px;
  flex-direction: column;
  box-sizing: border-box;
  justify-content: center;
}
#clock {
  stroke: red;
  stroke-width: 2px;
  fill: white;
}
#h,
#m,
#s,
#st {
  stroke: black;
  stroke-linecap: round;
  /**/
}
#h {
  stroke-width: 5.5px;
}
#m {
  stroke-width: 3px;
}
#s {
  stroke-width: 1.5px;
}
#st {
  stroke-width: 1.5px;
}
.time-text {
  text-align: center;
}
<div class="analog-clock">
  <svg width='440' height='440'>
    <circle id='clock' cx="220" cy='220' r='218' />

    <line id='h' x1='220' x2='220' y1='220' y2='38' />
    <line id='m' x1='220' x2='220' y1='220' y2='20' />
    <line id='s' x1='220' x2='220' y1='220' y2='12' />
    <line id='st' x1='220' x2='220' y1='220' y2='166' />

    <text x='204' y='30'>12</text>
    <text x='420' y='232'>3</text>
    <text x='210' y='435'>6</text>
    <text x='3' y='232'>9</text>

  </svg>

  <div class="time-text">
    <span id="hr">00</span>
    <span>:</span>
    <span id="min">00</span>
    <span>:</span>
    <span id="sec">00</span>
    <span id="suffix">--</span>
  </div>
</div>

Maybe using SMIL (SVG) animations could produce the easing effect also without having that reverse rotation problem but it won't work atleast in Chrome because they've deprecated SMIL animations.