feat: add odometer effect to create-token component

This commit is contained in:
Akuoko Daniel Jnr 2020-07-21 14:58:04 +00:00
parent 8a63467e6f
commit 89933f03de
No known key found for this signature in database
GPG Key ID: 1C95803CACD3E9DC
2 changed files with 208 additions and 4 deletions

View File

@ -4,6 +4,8 @@ import * as Constants from "~/common/constants";
import { css } from "@emotion/react";
import { ButtonPrimaryFull } from "~/components/system/components/Buttons";
import Odometer from "~/vendor/odometer";
const STYLES_CREATE_TOKEN = css`
font-family: ${Constants.font.text};
box-sizing: border-box;
@ -33,14 +35,25 @@ const STYLES_CREATE_TOKEN_BOTTOM = css`
padding: 16px;
`;
// TODO(jim): Lets do a cool odometer effect instead.
export const CreateToken = (props) => {
const [odometer, setOdometer] = React.useState(null);
const odometerNode = React.useRef(null);
if (props.token) {
let hash = props.token.replace(/-/g, "");
odometer.start({ to: hash });
}
React.useEffect(() => {
const newOdometer = new Odometer({ node: odometerNode.current });
setOdometer(newOdometer);
}, []);
return (
<div css={STYLES_CREATE_TOKEN}>
<div css={STYLES_CREATE_TOKEN_TOP}>
{props.token
? props.token
: `XXXXXXXX - XXXX - XXXX - XXXX - XXXXXXXXXXXX`}
<div ref={odometerNode} />
</div>
<div css={STYLES_CREATE_TOKEN_BOTTOM}>
<ButtonPrimaryFull onClick={props.onClick}>

191
vendor/odometer.js vendored Normal file
View File

@ -0,0 +1,191 @@
const iselement = (el) => el instanceof HTMLElement && el.nodeType === 1;
const isobject = (ob) => ob !== null && typeof ob === "object";
const isstring = (st) => typeof st === "string" || st instanceof String;
// (NOTE: daniel) helper function to create dom elements with classnames
const g = (attrArg = {}, tagArg = "div") => (...cttArr) => {
let el = document.createElement(tagArg);
let attrObj = isobject(attrArg) ? attrArg : { class: attrArg };
Object.keys(attrObj).forEach((key) => {
const val = attrObj[key];
if (!val) return;
if (/^\$/.test(key)) el.setAttribute("data-" + key.slice(1), val);
else if (/^_/.test(key)) el.addEventListener(key.slice(1), val);
else if (key === "style" && isobject(val)) {
el.setAttribute(
"style",
Object.keys(val)
.map((i) => `${i}:${val[i]}`)
.join(";")
);
} else el.setAttribute(key, val);
});
cttArr.forEach((cttItem) => {
if (iselement(cttItem)) el.appendChild(cttItem);
else if (tagArg.toLowerCase() === "img" && isstring(cttItem))
el.setAttribute("src", cttItem);
else if (cttItem !== undefined) el.innerHTML += cttItem;
});
return el;
};
// (NOTE: daniel) check maximum length of string
const maxLenNum = (aNum, bNum) => (aNum > bNum ? aNum : bNum).toString().length;
// (NOTE: daniel) reverse and convert string to number
const num2PadNumArr = (num, len, chars) => {
const charsArr = chars.map(String);
const padLeftStr = (rawStr, lenNum) =>
rawStr.length < lenNum ? padLeftStr("0" + rawStr, lenNum) : rawStr;
const str2NumArr = (rawStr) =>
rawStr.split("").map((i) => Number(charsArr.indexOf(i)));
return str2NumArr(padLeftStr(num.toString(), len)).reverse();
};
// (NOTE: daniel) helper function to generate numbers in a range
function range(start, stop, step) {
return Array.from(
{ length: (stop - start) / step + 1 },
(_, i) => start + i * step
);
}
const alphabets = range("a".charCodeAt(0), "z".charCodeAt(0), 1).map((x) =>
String.fromCharCode(x)
);
// (NOTE: daniel) odometer class
export default class Odometer {
constructor({
node,
from = 0,
to = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
duration = 0.5,
delay,
easeFn = (pos) =>
(pos /= 0.5) < 1
? 0.5 * Math.pow(pos, 3)
: 0.5 * (Math.pow(pos - 2, 3) + 2),
systemArr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...alphabets],
direct = true,
separator = "-",
seperateOnly = 0,
separateEvery = 3,
}) {
this.beforeArr = [];
this.afterArr = [];
this.ctnrArr = [];
this.duration = duration * 1000;
this.systemArr = systemArr;
this.easeFn = easeFn;
this.from = from;
this.to = to || 0;
this.node = node;
this.direct = direct;
this.separator = separator;
this.seperateOnly = seperateOnly;
this.separateEvery = seperateOnly ? 0 : separateEvery;
this.format = [8, 13, 18, 23];
this.init(maxLenNum(this.from, this.to));
if (to === undefined) return;
if (delay) setTimeout(() => this.start({ to: this.to }), delay * 1000);
else this.start({ to: this.to });
}
init(digits) {
this.node.style.position = "relative";
this.node.style.overflow = "hidden";
for (let i = 0; i < digits; i++) {
const ctnr = g(`digits`)(
...this.systemArr.map((i) => g("digit")(i)),
g("digit")(this.systemArr[0])
);
ctnr.style.position = "relative";
ctnr.style.display = "inline-block";
ctnr.style.verticalAlign = "top";
ctnr.style.textAlign = "center";
this.ctnrArr.unshift(ctnr);
this.node.appendChild(ctnr);
this.beforeArr.push(0);
}
const format = () => {
for (let i = 0; i < this.format.length; i++) {
const sprtr = g("separator")("-");
sprtr.style.display = "inline-block";
this.node.insertBefore(sprtr, this.node.childNodes[this.format[i]]);
}
};
format();
const resize = () => {
this.height = this.ctnrArr[0].clientHeight / (this.systemArr.length + 1);
this.node.style.height = this.height + "px";
if (this.afterArr.length) {
this.frame(1);
} else {
for (let d = 0; d < this.ctnrArr.length; d++) {
this._draw({
digit: d,
per: 1,
alter: ~~(this.from / Math.pow(10, d)),
});
}
}
};
resize();
window.addEventListener("resize", resize);
}
_draw({ per, alter, digit }) {
const newHeight =
this.ctnrArr[0].clientHeight / (this.systemArr.length + 1);
if (newHeight && this.height !== newHeight) this.height = newHeight;
const from = this.beforeArr[digit];
const modNum = (((per * alter + from) % 36) + 36) % 36;
const translateY = `translateY(${-modNum * this.height}px)`;
this.ctnrArr[digit].style.webkitTransform = translateY;
this.ctnrArr[digit].style.transform = translateY;
}
frame(per) {
let temp = 0;
for (let d = this.ctnrArr.length - 1; d >= 0; d--) {
let alter = this.afterArr[d] - this.beforeArr[d];
temp += alter;
this._draw({
digit: d,
per: this.easeFn(per),
alter: this.direct ? alter : temp,
});
temp *= 10;
}
}
start({ to, duration, easeFn, direct }) {
if (easeFn) this.easeFn = easeFn;
if (direct !== undefined) this.direct = direct;
const len = this.ctnrArr.length;
this.beforeArr = num2PadNumArr(this.from, len, this.systemArr);
this.afterArr = num2PadNumArr(to, len, this.systemArr);
const start = Date.now();
const dur = duration * 1000 || this.duration;
const tick = () => {
let elapsed = Date.now() - start;
this.frame(elapsed / dur);
if (elapsed < dur) requestAnimationFrame(tick);
else {
this.from = to;
this.frame(1);
}
};
requestAnimationFrame(tick);
}
}