Ros
Ros

Reputation: 53

Fix text-orientation inside rotated element

I'm trying to build an animated clock with HTML, CSS and JS and I've managed to position the hours in the right place, but now the numbers are not straight.

I've been looking into the text-orientation property and tried using text-orientation: auto and text-orientation: mixed, but none seem to work.

This is how my clock looks so far:

const clock = document.getElementById('clock');
const numbers = clock.children;

for (let i = 0; i < 12; i++) {
  numbers[i].style.transform = 'translate(0, -50%) rotate(' + (90 + i * 30) + 'deg)';
}

const hours = document.getElementById('hours');
const minutes = document.getElementById('minutes');
const seconds = document.getElementById('seconds');

setInterval(function() {
  const time = new Date();
  hours.style.transform = 'translate(0, -50%) rotate(' + (time.getHours() % 12 * 30 - 90) + 'deg)';
  minutes.style.transform = 'translate(0, -50%) rotate(' + (time.getMinutes() * 6 - 90) + 'deg)';
  seconds.style.transform = 'translate(0, -50%) rotate(' + (time.getSeconds() * 6 - 90) + 'deg)';

});
#clock {
  width: 200px;
  height: 200px;
  border: 3px solid black;
  border-radius: 50%;
  position: relative;
}

.hour{
  position: absolute;
  top: 50%;
  left: 0;
  width: 100%;
  text-orientation: mixed;
}

.handle {
  position: absolute;
  top: 50%;
  left: 50%;
  transform-origin: left center;
}

#hours {
  border-bottom: 3px solid black;
  width: 25%;
}

#minutes {
  border-bottom: 2px solid black;
  width: 35%;
}

#seconds {
  border-bottom: 1px solid red;
  width: 45%;
}
<div id="clock">
  <div class="hour">- 12</div>
  <div class="hour">- 1</div>
  <div class="hour">- 2</div>
  <div class="hour">- 3</div>
  <div class="hour">- 4</div>
  <div class="hour">- 5</div>
  <div class="hour">- 6</div>
  <div class="hour">- 7</div>
  <div class="hour">- 8</div>
  <div class="hour">- 9</div>
  <div class="hour">- 10</div>
  <div class="hour">- 11</div>
  <div class="handle" id="hours"></div>
  <div class="handle" id="minutes"></div>
  <div class="handle" id="seconds"></div>
</div>

Upvotes: 5

Views: 401

Answers (2)

Danziger
Danziger

Reputation: 21181

You can't do that using text-orientation. This property is more about reading direction rather than design or layout and is intended to be used only in text in vertical mode, as stated in MDN:

The text-orientation CSS property sets the orientation of the text characters in a line. It only affects text in vertical mode (when writing-mode is not horizontal-tb). It is useful for controlling the display of languages that use vertical script, and also for making vertical table headers.

Instead, you need to wrap the hours in a separated div or span, of a fixed width for a more consistent look, and apply the opposite transform: rotate(...) to that.

Also, you might want to rotate the minutes and hours handles progressively. That is, if it's 12:30, then the hours' handle should be half-way between 12 and 1.

And you should add a delay to setInterval. Right now it is being called way more often than needed (once per second) to update the handles.

const hoursHandle = document.getElementById('hours');
const minutesHandle = document.getElementById('minutes');
const secondsHandle = document.getElementById('seconds');

// Use querySelectorAll and avoid iterating over the handles by mistake:
document.querySelectorAll('.hour').forEach((child, i) => {
  const angle = 90 + i * 30;
  
  child.style.transform = `translate(0, -50%) rotate(${ angle }deg)`;
  child.children[0].style.transform = `rotate(${ -angle }deg)`;
});

function updateHandles() {
  // With requestAnimationFrame we avoid forcing a repaint:
  
  requestAnimationFrame(() => {
    const time = new Date();
    const seconds = time.getSeconds();
    const minutes = time.getMinutes();
    const hours = time.getHours() % 12;
    const secondsAngle = -90 + 6 * seconds;
    const minutesAngle = -90 + 6 * (minutes + seconds/60);
    const hoursAngle = -90 + 30 * (hours + minutes/60 + seconds/3600); 

    hoursHandle.style.transform = `translate(0, -50%) rotate(${ hoursAngle }deg)`;
    minutesHandle.style.transform = `translate(0, -50%) rotate(${ minutesAngle }deg)`;
    secondsHandle.style.transform = `translate(0, -50%) rotate(${ secondsAngle }deg)`;
  });
}

updateHandles();

setInterval(updateHandles, 250);
body {
  font-family: monospace;
  margin: 0;
}

#clock {
  margin: 16px auto;
  width: 256px;
  height: 256px;
  border-radius: 50%;
  position: relative;
  box-shadow: 0 0 64px rgba(0, 0, 0, .125);
}

.hour {
  position: absolute;
  top: 50%;
  left: 0;
  width: 100%;
  font-weight: bold;
}

/*
  Add the marks with CSS rather than using a dash:
*/

.hour::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 0;
  width: 4px;
  border-top: 1px solid black;
  transform: translate(0, -50%);
}

.hour > div {
  width: 32px;
  padding: 0 8px;
  display: flex;
  justify-content: center;
}

.handle {
  position: absolute;
  top: 50%;
  left: 50%;
  transform-origin: left center;
}

#hours {
  background: rgba(0, 0, 0, .5);
  height: 6px;
  right: 64px;
}

#minutes {
  background: rgba(0, 0, 0, .25);
  height: 4px;
  right: 48px;
}

#seconds {
  background: red;
  height: 2px;
  right: 16px;
}

#seconds::before,
#seconds::after {
  content: '';
  position: absolute;
  background: red;
}

#seconds::before {
  top: 0;
  left: -24px;
  width: 24px;
  height: 100%;
}

#seconds::after {
  top: 50%;
  left: 0;
  width: 8px;
  height: 8px;
  transform: translate(-50%, -50%);
  border-radius: 100%;
}
<div id="clock">
  <div class="hour"><div>12</div></div>
  <div class="hour"><div>1</div></div>
  <div class="hour"><div>2</div></div>
  <div class="hour"><div>3</div></div>
  <div class="hour"><div>4</div></div>
  <div class="hour"><div>5</div></div>
  <div class="hour"><div>6</div></div>
  <div class="hour"><div>7</div></div>
  <div class="hour"><div>8</div></div>
  <div class="hour"><div>9</div></div>
  <div class="hour"><div>10</div></div>
  <div class="hour"><div>11</div></div>
  
  <div class="handle" id="hours"></div>
  <div class="handle" id="minutes"></div>
  <div class="handle" id="seconds"></div>
</div>

And just because today is the release of the last episode of Watchmen...:

const clock = document.getElementById('clock');
const hoursHandle = document.getElementById('hours');
const minutesHandle = document.getElementById('minutes');
const secondsHandle = document.getElementById('seconds');

// Use querySelectorAll and avoid iterating over the handles by mistake:
document.querySelectorAll('.hour').forEach((child, i) => {
  const angle = 90 + i * 30;
  
  child.style.transform = `translate(0, -50%) rotate(${ angle }deg)`;
});

// Add 60 marks around the block:
for (let i = 0; i < 60; ++i) {
  const mark = document.createElement('DIV');
  
  mark.className = 'mark';
  mark.style.transform = `rotate(${ i * 6 }deg)`;
  
  clock.appendChild(mark);
}

function updateHandles() {
  // With requestAnimationFrame we avoid forcing a repaint:
  
  requestAnimationFrame(() => {
    const time = new Date();
    const seconds = time.getSeconds();
    const minutes = time.getMinutes();
    const hours = time.getHours() % 12;
    const secondsAngle = -90 + 6 * seconds;
    const minutesAngle = -90 + 6 * (minutes + seconds/60);
    const hoursAngle = -90 + 30 * (hours + minutes/60 + seconds/3600); 

    hoursHandle.style.transform = `translate(0, -50%) rotate(${ hoursAngle }deg)`;
    minutesHandle.style.transform = `translate(0, -50%) rotate(${ minutesAngle }deg)`;
    secondsHandle.style.transform = `translate(0, -50%) rotate(${ secondsAngle }deg)`;
  });
}

updateHandles();

setInterval(updateHandles, 250);
body {
  font-family: monospace;
  margin: 0;
  background: black;
}

body::before,
body::after {
  content: '';
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  pointer-events: none;
}

body::before {
  background: linear-gradient(-135deg, rgba(0, 0, 255, .125), transparent);
}

body::after {
  background: linear-gradient(45deg, rgba(255, 0, 0, .125), transparent);
  z-index: 1;
}

#clock {
  margin: 16px auto;
  width: 256px;
  height: 256px;
  border-radius: 50%;
  position: relative;
  background: #fbdf27;
  text-shadow: 0 0 1px black;
}

.hour {
  position: absolute;
  top: 50%;
  left: 0;
  width: 50%;
  font-weight: bold;
  font-size: 10px;
  transform-origin: right center;
  background: #fbdf27;
  z-index: 1;
}

.hour > div {
  width: 16px;
  display: flex;
  justify-content: center;
  transform: rotate(-90deg);
}

.handle {
  position: absolute;
  top: 50%;
  left: 50%;
  transform-origin: left center;
  background: blue;
  z-index: 100;
  mix-blend-mode: color-burn;
}

#hours {
  height: 6px;
  right: 64px;
}

#minutes {
  height: 4px;
  right: 48px;
}

#seconds {
  height: 2px;
  right: 16px;
}

#seconds::before,
#seconds::after {
  content: '';
  position: absolute;
  background: blue;
}

#seconds::before {
  top: 0;
  left: -24px;
  width: 24px;
  height: 100%;
  mix-blend-mode: color-burn;
}

#seconds::after {
  top: 50%;
  left: 0;
  width: 8px;
  height: 8px;
  transform: translate(-50%, -50%);
  border-radius: 100%;
}

#title {
  position: absolute;
  top: 192px;
  left: 50%;
  width: 272px;
  color: #fbdf27;
  background: black;
  font-size: 62px;
  line-height: 40px;
  text-align: center;
  transform: translate(-50%, -50%);
  z-index: 50;
}

.mark {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 1px;
  transform: translate(-50%, 0);
}

.mark::before {
  content: '';
  position: absolute;
  top: 6px;
  right: 0;
  left: 0;
  height: 4px;
  background: black;
}
<div id="title">WATCHMEN</div>

<div id="clock">    
  <div class="hour"><div>XII</div></div>
  <div class="hour"><div>I</div></div>
  <div class="hour"><div>II</div></div>
  <div class="hour"><div>III</div></div>
  <div class="hour"><div>IV</div></div>
  <div class="hour"><div>V</div></div>
  <div class="hour"><div>VI</div></div>
  <div class="hour"><div>VII</div></div>
  <div class="hour"><div>VIII</div></div>
  <div class="hour"><div>IX</div></div>
  <div class="hour"><div>X</div></div>
  <div class="hour"><div>XI</div></div>
  
  <div class="handle" id="hours"></div>
  <div class="handle" id="minutes"></div>
  <div class="handle" id="seconds"></div>
</div>

Upvotes: 5

Temani Afif
Temani Afif

Reputation: 273970

You can do the calculation differently by considering only translation and cos/sin formula.

The value .8 will define the distance from the center, adjust it like you want. Don't forget that cos/sin take a radian angle that's why we have to multiply by `Math.PI/180.

I considered a background trick for the little lines:

const clock = document.getElementById('clock');
const numbers = clock.children;

const h = 0.8 * (clock.offsetHeight/2);

for (let i = 0; i < 12; i++) {
  numbers[i].style.transform = 
    'translate(-50%, -50%) translate(' 
     + (-h*Math.cos((90 + i * 30)*Math.PI/180)) + 'px,' 
     + (-h*Math.sin((90 + i * 30)*Math.PI/180)) + 'px)';
}

const hours = document.getElementById('hours');
const minutes = document.getElementById('minutes');
const seconds = document.getElementById('seconds');

setInterval(function() {
  const time = new Date();
  hours.style.transform   = 'rotate(' + (time.getHours() % 12 * 30 - 90) + 'deg)';
  minutes.style.transform = 'rotate(' + (time.getMinutes() * 6 - 90) + 'deg)';
  seconds.style.transform = 'rotate(' + (time.getSeconds() * 6 - 90) + 'deg)';

});
#clock {
  width: 200px;
  height: 200px;
  border: 3px solid black;
  padding:5px; /* Control the length of the lines*/
  box-sizing:border-box;
  border-radius: 50%;
  position: relative;
  
  background:
    linear-gradient(#fff,#fff) content-box,
    
    linear-gradient(#000,#000) center/100% 2px,
    linear-gradient(#000,#000) center/2px 100%,
    linear-gradient(30deg,
      transparent calc(50% - 1px), 
      #000 calc(50% - 1px) calc(50% + 1px),
      transparent calc(50% + 1px)),
    linear-gradient(-30deg,
      transparent calc(50% - 1px), 
      #000 calc(50% - 1px) calc(50% + 1px),
      transparent calc(50% + 1px)),
    linear-gradient(60deg,
      transparent calc(50% - 1px), 
      #000 calc(50% - 1px) calc(50% + 1px),
      transparent calc(50% + 1px)),
   linear-gradient(-60deg,
      transparent calc(50% - 1px), 
      #000 calc(50% - 1px) calc(50% + 1px),
      transparent calc(50% + 1px))  ;
  background-repeat:no-repeat;
    
}

.hour,
.handle{
  position: absolute;
  top: 50%;
  left: 50%;
  transform-origin: left;
}

#hours {
  border-bottom: 4px solid black;
  margin-top:-2px;
  width: 25%;
}

#minutes {
  border-bottom: 2px solid black;
  margin-top:-1px;
  width: 35%;
}

#seconds {
  border-bottom: 1px solid red;
  width: 45%;
}
<div id="clock">
  <div class="hour"> 12</div>
  <div class="hour"> 1</div>
  <div class="hour"> 2</div>
  <div class="hour"> 3</div>
  <div class="hour"> 4</div>
  <div class="hour"> 5</div>
  <div class="hour"> 6</div>
  <div class="hour"> 7</div>
  <div class="hour"> 8</div>
  <div class="hour"> 9</div>
  <div class="hour"> 10</div>
  <div class="hour"> 11</div>
  <div class="handle" id="hours"></div>
  <div class="handle" id="minutes"></div>
  <div class="handle" id="seconds"></div>
</div>

Upvotes: 1

Related Questions