<body>
  <div class="row">
    <form id="translate-form">
      <input type="number" id="numeral" name="numeral" value="2025" min="1" max="3999">
      <input type="submit" id="translate-button" value="Translate" />
    </form>
  </div>
  <div class="row">
    <div id="marble-block">
      <p id="roman-numeral">MMXXV</p>
      <div id="numeral-reveal-bar"></div>
      <div id="dust-cloud">
        <svg class="dust" id="dust-1" width="10" height="10" overflow="visible">
          <circle cx="5" cy="5" r="5" />
        </svg>
        <svg class="dust" id="dust-2" width="10" height="10" overflow="visible">
          <circle cx="5" cy="5" r="5" />
        </svg>
        <svg class="dust" id="dust-3" width="10" height="10" overflow="visible">
          <circle cx="5" cy="5" r="5" />
        </svg>
        <svg class="dust" id="dust-4" width="10" height="10" overflow="visible">
          <circle cx="5" cy="5" r="5" />
        </svg>
        <svg class="dust" id="dust-5" width="10" height="10" overflow="visible">
          <circle cx="5" cy="5" r="5" />
        </svg>
        <svg class="dust" id="dust-6" width="10" height="10" overflow="visible">
          <circle cx="5" cy="5" r="5" />
        </svg>
        <svg class="dust" id="dust-7" width="10" height="10" overflow="visible">
          <circle cx="5" cy="5" r="5" />
        </svg>
        <svg class="dust" id="dust-8" width="10" height="10" overflow="visible">
          <circle cx="5" cy="5" r="5" />
        </svg>
      </div>
      <div id="hammer-and-chisel">
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 229 226" overflow="visible">
          <g fill="none" fill-rule="nonzero">
            <g id="chisel" transform="translate(13 109)" fill="#555">
              <rect x="6" y="9" width="18" height="108" rx="8"/>
              <rect width="30" height="19" rx="8"/>
            </g>
            <g id="hammer" transform="rotate(-90 54.5 54.5)" stroke="#979797">
              <rect fill="#D6B588" x="42.5" y="49.5" width="24" height="179" rx="8"/>
              <rect fill="#D8D8D8" x=".5" y=".5" width="108" height="56" rx="8"/>
            </g>
          </g>
        </svg>
      </div>
    </div>
  </div>
</body>
body {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-direction: column;
    row-gap: 60px;
    font-family: "Times New Roman", serif;
    background-color: #1b1b1b;
    min-height: 100vh;
}

form {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-direction: row;
}

input {
    display: block;
    font-weight: bold;
    border: none;
    padding: 10px 20px;
    margin-right: 20px;
    font-size: 42px;
    color: #666;
    border-radius: 10px;
}

#translate-button {
    cursor: pointer;
    font-size: 16px;
    border: 2px solid gold;
    transition: background-color 0.3s ease, color 0.3s ease;
}

#translate-button:hover {
    background-color: #fff;
}

#translate-button:disabled {
    cursor: not-allowed;
    background-color: #aaa;
}

#marble-block {
    position: relative;
    padding: 80px;
    border-radius: 10px;
    box-shadow: 10px 10px #888888;
    background: #F1F2FA;
}

#hammer-and-chisel {
    position: absolute;
    width: 164px;
    display: none;
    left: 20;
    top: 0;
}

#dust-cloud {
    position: absolute;
    top: 0;
    left: 20;
    width: 200px;
    display: none;
}

.dust {
    position: absolute;
    top: 0;
    left: 0;
    width: 10px;
}

#roman-numeral {
    padding: 0;
    margin: 0;
    font-size: 108px;
    font-family: "Times New Roman", serif;
    background: #666;
    background-clip: text;
    -webkit-background-clip: text;
    color: transparent;
    text-shadow: 3px 5px 1px rgba(245, 245, 245, 0.5);
}

#numeral-reveal-bar {
    position: absolute;
    right: 0;
    top: 0;
    margin: 0;
    padding: 0;
    border-radius: 10px;
    background: #F1F2FA;
}

@media only screen and (max-width: 600px) {
    #hammer-and-chisel {
        width: 112px;
    }

    #roman-numeral {
        font-size: 54px;
    }
}
let numberDisplay = document.getElementById("roman-numeral");
let inputEl = document.getElementById('numeral');
let translateButton = document.getElementById('translate-button');
let translateForm = document.getElementById('translate-form');

let hammerAndChisel = document.getElementById('hammer-and-chisel');
let numeralRevealBar = document.getElementById('numeral-reveal-bar');
let marbleBlock = document.getElementById('marble-block');

let dustCloud = document.getElementById('dust-cloud');
let dustParticles = document.getElementsByClassName("dust");

var intToRoman = function(num) {
    const symbolVals = { 'M': 1000, 'CM': 900, 'D': 500, 'CD': 400,
        'C': 100, 'XC': 90, 'L': 50, 'XL': 40, 'X': 10, 'IX': 9,
        'V': 5, 'IV': 4, 'I': 1 }
    const symbols = Object.keys(symbolVals)
    let roman = ""
    let currentSymbolIdx = 0
    while (num > 0) {
      let currentVal = symbolVals[symbols[currentSymbolIdx]]
      if (num - currentVal >= 0) {
        roman += symbols[currentSymbolIdx]
        num -= currentVal
      } else {
        currentSymbolIdx += 1
      }
    }
    return roman
};

translateForm.addEventListener('submit', function(event) {
  event.preventDefault();
  let inputVal = parseInt(inputEl.value, 10);
  if (Number.isInteger(inputVal)) {
    if (inputVal >= 1 && inputVal <= 3999) {
      let romanNum = intToRoman(inputVal);
      numberDisplay.innerHTML = romanNum;
      hammerAndChisel.style.display = 'block';
      dustCloud.style.display = 'block';
      translateButton.disabled = true;
      runLoadingAnim();
    }
  }
});

function runLoadingAnim() {
  let tl = gsap.timeline({defaults: {ease: 'none', repeat: -1, duration: 0.25, yoyo: true }})

  tl.set("#chisel", {
    rotate: 15,
  })

  tl.set("#hammer", {
    y: numberDisplay.offsetHeight + 80,
    transformOrigin: '50% 100%',
    rotate: -60,
  })

  tl.to('#hammer', {
    y: numberDisplay.offsetHeight + 50,
    transformOrigin: '50% 100%',
    rotate: 0
  })

  let leftToRight = gsap.timeline({defaults: { ease: 'none', duration: 2 }})
  leftToRight.add('start')

  leftToRight.set('#dust-cloud', {
    y: marbleBlock.offsetHeight / 2 + 15,
  })

  leftToRight.set('#numeral-reveal-bar', {
    width: marbleBlock.offsetWidth,
    height: marbleBlock.offsetHeight,
  })

  runDustCloudAnimation(leftToRight)

  leftToRight.to('#numeral-reveal-bar', {
    width: 0,
    delay: 0.1
  }, 'start')

  leftToRight.to('#hammer-and-chisel', {
    x: numberDisplay.offsetWidth,
    onComplete: () => {
      hammerAndChisel.style.display = 'none';
      gsap.set("#hammer-and-chisel", { x: 20 });
    }
  }, 'start')

  leftToRight.to('#dust-cloud', {
    x: numberDisplay.offsetWidth,
    onComplete: () => {
      dustCloud.style.display = 'none';
      gsap.set("#dust-cloud", { x: 20 });
      translateButton.disabled = false;
    }
  }, 'start')
}

window.onload = function() {
  hammerAndChisel.style.display = 'block';
  dustCloud.style.display = 'block';
  translateButton.disabled = true;
  runLoadingAnim();
}

function runDustCloudAnimation(timeline) {
  const commonProps = { ease: 'power1.out', opacity: 0, duration: 0.25, repeat: 4, repeatDelay: 0.25 }
  const dustResetProps = { x: 0, y: 0, opacity: 1 }

  timeline.to('#dust-1', {
    y: -marbleBlock.offsetHeight / 2,
    ...commonProps,
    onComplete: () => {
      gsap.set('#dust-1', dustResetProps)
    }
  }, 'start')

  timeline.to('#dust-2', {
    x: -marbleBlock.offsetHeight / 4,
    y: -marbleBlock.offsetHeight / 4,
    ...commonProps,
    onComplete: () => {
      gsap.set('#dust-2', dustResetProps)
    }
  }, 'start')

  timeline.to('#dust-3', {
    y: marbleBlock.offsetHeight / 2,
    ...commonProps,
    onComplete: () => {
      gsap.set('#dust-3', dustResetProps)
    }
  }, 'start')

  timeline.to('#dust-4', {
    x: marbleBlock.offsetHeight / 4,
    y: marbleBlock.offsetHeight / 4,
    ...commonProps,
    onComplete: () => {
      gsap.set('#dust-4', dustResetProps)
    }
  }, 'start')

  timeline.to('#dust-5', {
    x: -marbleBlock.offsetHeight / 2,
    ...commonProps,
    onComplete: () => {
      gsap.set('#dust-5', dustResetProps)
    }
  }, 'start')

  timeline.to('#dust-6', {
    x: -marbleBlock.offsetHeight / 4,
    y: marbleBlock.offsetHeight / 4,
    ...commonProps,
    onComplete: () => {
      gsap.set('#dust-6', dustResetProps)
    }
  }, 'start')

  timeline.to('#dust-7', {
    x: marbleBlock.offsetHeight / 2,
    ...commonProps,
    onComplete: () => {
      gsap.set('#dust-7', dustResetProps)
    }
  }, 'start')

  timeline.to('#dust-8', {
    x: marbleBlock.offsetHeight / 4,
    y: -marbleBlock.offsetHeight / 4,
    ...commonProps,
    onComplete: () => {
      gsap.set('#dust-8', dustResetProps)
    }
  }, 'start')
}
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://unpkg.com/gsap@3/dist/gsap.min.js