Scrolling transition animation for price action. How?

Viewed 149

I've looked online and only found a version for swift. Online, the only keyword that returns a search result is ScrollCounter. Is this type of animation even possible is CSS HTML?

If anyone can point me to a resource that works, or an example, I will post back my research after creating a working example.

Take a look

ScrollCounter

And Obfuscated CSS and HTML...

Animation

Update 1

I've found the following counter animation but still trying to figure it out how to implement it as a currency counter including optional comma for numbers greater than 999.99.

Although... In real life, I would be updating this value through WebSockets.

const genNumber = () => {
  document.querySelector("div").style.setProperty("--percent", Math.random());
};

setInterval(genNumber, 3000);
setTimeout(genNumber);
@property --percent {
  syntax: "<number>";
  initial-value: 0;
  inherits: false;
}
@property --temp {
  syntax: "<number>";
  initial-value: 0;
  inherits: false;
}
@property --v1 {
  syntax: "<integer>";
  initial-value: 0;
  inherits: false;
}
@property --v2 {
  syntax: "<integer>";
  initial-value: 0;
  inherits: false;
}

div {
  font: 800 40px monospace;
  padding: 2rem;
  transition: --percent 1s;
  --temp: calc(var(--percent) * 100);
  --v1: max(var(--temp) - 0.5, 0);
  --v2: max((var(--temp) - var(--v1)) * 100 - 0.5, 0);
  counter-reset: v1 var(--v1) v2 var(--v2);
}
div::before {
  content: counter(v1) "." counter(v2, decimal-leading-zero) "%";
}
<div></div>

3 Answers

Here is what I managed to come up with, hopefully fits your request better than your own answer. Just creates all possible characters then uses CSS to scroll the correct character into view.

(() => {
  // BUILD VIEW
  const chars = ["$", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ".", ','];
  
  const formatter = new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD'
    });

  const container = document.createElement("div");
  container.setAttribute("class", "spinner-container");

  const cols = [];
  for (let x = 0; x < 12; x++) {
    cols[x] = document.createElement("div");
    cols[x].setAttribute("class", "spinner-col");

    for (let char of chars) {
      const div = document.createElement("div");
      div.innerText = char;
      div.setAttribute("class", "spinner-char");
      cols[x].append(div);
    }

    container.append(cols[x]);
  }
  document.body.append(container);

  // LOGIC

  const setValue = (value) => {
    for (let x = value.length; x < cols.length; x++) {
        cols[x].style.display = "none";
    }
    
    for (let x = 0; x < value.length; x++) {
      const index = chars.indexOf(value[x]);
      cols[x].style.display = "block";
      cols[x].style.transform = `translate(0, -${index * 75}px)`;
    }
  };

  setInterval(() => {
    const value = Math.random() * (999999 - 100000) + 100000;
    setValue(formatter.format(value / 100));
  }, 2000);
})();
.spinner-container {
  height: 75px;
  display: flex;
  overflow: hidden;
}

.spinner-col {
  box-sizing: border-box;
  transform: translate(0, 0);
  transition: transform 400ms ease-in-out;
}

.spinner-char {
  width: 34px;
  text-align: center;
  font-size: 50px;
  height: 75px;
  line-height: 75px;
}

Sorry for the double answer, this is meant as an extension for my other answer and didn't want to add complexity to the original answer. Using custom elements we can make this reusable.

const chars = ["$", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ".", ','];

class RHPriceElement extends HTMLElement {
  static get observedAttributes() {
    return ['value'];
  }

  constructor() {
    super();
    this.formatter = new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'USD'
    });
    this._build();
  }

  attributeChangedCallback(name, _oldValue, newValue) {
    if (name === "value") {
      const value = this.formatter.format(newValue);

      while (this.cols.length < value.length) this._addCol();

      for (let x = value.length; x < this.cols.length; x++) {
        this.cols[x].style.display = "none";
      }

      for (let x = 0; x < value.length; x++) {
        const index = chars.indexOf(value[x]);
        this.cols[x].style.display = "block";
        this.cols[x].style.transform = `translate(0, -${index * 75}px)`;
      }
    }
  }

  _build() {
    this.cols = [];
    for (let x = 0; x < 12; x++) {
      this._addCol();
    }
  }

  _addCol() {
    const x = this.cols.length;
    this.cols[x] = document.createElement("div");
    this.cols[x].setAttribute("class", "spinner-col");

    for (let char of chars) {
      const div = document.createElement("div");
      div.innerText = char;
      div.setAttribute("class", "spinner-char");
      this.cols[x].append(div);
    }

    this.append(this.cols[x]);
  }
}

customElements.define('rh-price', RHPriceElement);

(() => {
  setInterval(() => {
    document.querySelectorAll("rh-price").item(0).setAttribute("value", Math.random() * (9999999 - 10000) + 10000);
    document.querySelectorAll("rh-price").item(1).setAttribute("value", Math.random() * (99999 - 1) + 1);
  }, 1000);
})();
rh-price {
  height: 75px;
  display: flex;
  overflow: hidden;
}

rh-price>div {
  transform: translate(0, 0);
  transition: transform 400ms ease-in-out;
}

rh-price>div>div {
  width: 34px;
  text-align: center;
  font-size: 50px;
  height: 75px;
  line-height: 75px;
}
<rh-price value="5000000"></rh-price>
<br>
<rh-price value="600"></rh-price>

I've modified the first answer to be more semantically sound. Added comments and explanations along the way because I found @Phillip Rollins approach pretty clever.

enter image description here

// Usage

// Reference to div element that is going to be used as the spinner
const spinnerContainer = document.getElementById('BTCUSD');
spinnerContainer.setAttribute('class', 'spinner-container');

// INITITAL CONSTRUCT

// Array that contains div with class spinner-col HTML elements
const spinnerColumns = [];

// The View will contain the following possible characters
const spinnerCharacters = ['$', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', ','];

// Assumed total length of 12 digits...
for (let i = 0; i < 12; i++) {

  // Create Anonymous div
  spinnerColumns[i] = document.createElement('div');
  // Set Anonymous div class to spinner-col
  spinnerColumns[i].setAttribute('class', 'spinner-col');

  // For each character in array spinnerCharacters...
  for (let spinnerCharacter of spinnerCharacters) {

    // Create Anonymous div
    const div = document.createElement('div');
    // ..containing this character
    div.innerText = spinnerCharacter;
    // Set Anonymous div class to spinner-char
    div.setAttribute('class', 'spinner-char');

    // Append Anonymous div spinner-char to Anonymous div spinner-col
    spinnerColumns[i].append(div);

  }

  // Append Anonymous div spinner-col to spinner-container
  spinnerContainer.append(spinnerColumns[i]);
}



// RENDER LOGIC

// Set spinner value to number passed in
const setSpinnerValue = (spinnerValue) => {

  // The amount of column.style.display = 'none' attributes to initialize 
  // is equivalent to the amount of digits in the number passed in
  for (let i = spinnerValue.length; i < spinnerColumns.length; i++) {
    // initially NOT visible
    spinnerColumns[i].style.display = 'none';
  }

  // The amount of column.style.display = 'block' attributes to update 
  // is equivalent to the amount of digits in the number passed in
  for (let i = 0; i < spinnerValue.length; i++) {

    const index = spinnerCharacters.indexOf(spinnerValue[i]);
    // NOW visible
    spinnerColumns[i].style.display = 'block';
    // Apply CSS transform style
    spinnerColumns[i].style.transform = `translate(0, -${index * 75}px)`;
  }

};



// A currency formatter for setting the spinner value
const formatCurrency = new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD'
});



// PRICE FEED

// Just a timer setting random prices every 2 seconds
// Easily replaceble with polling or websockets
setInterval(() => {
  const spinnerValue = Math.random() * (999999 - 100000) + 100000;
  // Update Price 
  setSpinnerValue(formatCurrency.format(spinnerValue / 100));
}, 2000);
.spinner-container {
  height: 75px;
  display: flex;
  overflow: hidden;
}

.spinner-col {
  box-sizing: border-box;
  transform: translate(0, 0);
  transition: transform 800ms ease-in-out;
}

.spinner-char {
  width: 34px;
  text-align: center;
  font-size: 50px;
  height: 75px;
  line-height: 75px;
}
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-eOJMYsd53ii+scO/bJGFsiCZc+5NDVN2yr8+0RDqr0Ql0h+rP48ckxlpbzKgwra6" crossorigin="anonymous">

<div id="BTCUSD"></div>
<h4>BTCUSD</h4>

Related