Skip to content

Commit 1ac5205

Browse files
committedJul 26, 2020
Add pause and resume
1 parent e4d7a69 commit 1ac5205

File tree

6 files changed

+207
-102
lines changed

6 files changed

+207
-102
lines changed
 

‎.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
node_modules
22
.idea/
33
package-lock.json
4+
.DS_Store

‎README.md

+35-9
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,31 @@
66
[![npm version](https://badge.fury.io/js/bounty.svg)](https://www.npmjs.com/package/bounty)
77
[![Dependencies](https://david-dm.org/coderitual/bounty.svg)](https://david-dm.org/coderitual/bounty)
88
[![npm downloads](https://img.shields.io/npm/dt/bounty.svg?maxAge=2592000)](https://www.npmjs.com/package/bounty)
9+
910
> JavaScript odometer or slot machine effect library for smoothly transitioning numbers with motion blur. Library uses functional approach and ES7 Function Bind Syntax. Internally based on SVG.
1011
1112
<p align="center"><img src ="docs/logo.gif"/></p>
1213

1314
See the **[live version](https://coderitual.github.io/bounty/examples/)**.
1415

1516
## Installation
17+
1618
To install the stable version:
1719

1820
`npm install --save bounty`
1921

2022
## Examples
23+
2124
The API is really simple and straigthforward:
25+
2226
```js
2327
import bounty from `bounty`;
2428

2529
bounty({ el: '.js-bounty', value: '£42,000,000' });
2630
```
2731

2832
You can use it with other **options**:
33+
2934
```js
3035
import bounty from `bounty`;
3136

@@ -37,14 +42,29 @@ bounty({
3742
letterSpacing: 1,
3843
animationDelay: 100,
3944
letterAnimationDelay: 100
45+
duration = 3000
4046
});
4147
```
42-
If you want to **cancel** the ongoing animation just call returned function:
48+
49+
If you want to **control** ongoing animation just use methods from returned object:
50+
4351
```js
4452
import bounty from `bounty`;
4553

46-
const cancel = bounty({ el: '.js-bounty', value: '£42,000,000' });
47-
cancel();
54+
const { cancel, pause, resume } = bounty({ el: '.js-bounty', value: '£42,000,000' });
55+
56+
const wait = (delay) => new Promise((resolve) => setTimeout(resolve, delay));
57+
58+
const pasueAndRun = async () => {
59+
await wait(1500);
60+
pause();
61+
await wait(2000);
62+
resume();
63+
await wait(2000);
64+
cancel();
65+
};
66+
67+
pasueAndRun();
4868
```
4969

5070
Library is built using UMD thus the following usage in HTML is possible.
@@ -53,7 +73,7 @@ Library is built using UMD thus the following usage in HTML is possible.
5373
<div class="js-bounty"></div>
5474
<script src="/bounty.js"></script>
5575
<script>
56-
bounty.default({ el: '.js-bounty', value: '£42,000,000' })
76+
bounty.default({ el: ".js-bounty", value: "£42,000,000" });
5777
</script>
5878
```
5979

@@ -62,10 +82,13 @@ The UMD build is also available on unpkg:
6282
```html
6383
<script src="https://unpkg.com/bounty@1.1.6/lib/bounty.js"></script>
6484
```
85+
6586
You can find the library on `window.bounty`.
6687

6788
## That's it?
89+
6890
Yea! That's it. Other options like `font-family` and `font-size` are taken from **computed styles** so you can just style it like the other layers.
91+
6992
```css
7093
.js-bounty {
7194
font-size: 60px;
@@ -77,16 +100,19 @@ Yea! That's it. Other options like `font-family` and `font-size` are taken from
77100

78101
## How?
79102

80-
If you're interested how it's made, see the **[presentation](http://slides.com/coderitual/odoo-js)**.
103+
If you're interested how it's made, see the **[presentation](http://slides.com/coderitual/odoo-js)**.
81104

82105
## Roadmap
106+
83107
There is a work in progress to implement additional features:
84-
* [ ] `from` `to` API.
85-
* [ ] Full ASCII transition support.
86-
* [ ] Control animation.
87-
* [ ] Introduce Webcomponents API `<svg-bounty>`
108+
109+
- [ ] `from` `to` API.
110+
- [ ] Full ASCII transition support.
111+
- [ ] Control animation.
112+
- [ ] Introduce Webcomponents API `<svg-bounty>`
88113

89114
<p align="center"><img src ="docs/example2.gif"/></p>
90115

91116
## License
117+
92118
The library is available under the MIT license. For more info, see the [LICENSE](LICENSE) file.

‎examples/pause.html

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<title>bounty</title>
6+
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
7+
<meta name="author" content="Coderitual" />
8+
<meta name="description" content="Canvas 2d odometer effect" />
9+
<meta
10+
name="viewport"
11+
content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"
12+
/>
13+
<style type="text/css">
14+
@font-face {
15+
font-family: "ITV Reem";
16+
src: url(assets/itvreem.woff) format("woff");
17+
}
18+
19+
body {
20+
background-color: #999;
21+
font-family: "ITV Reem";
22+
font-size: 82px;
23+
text-shadow: 1px 1px 5px rgba(0, 0, 0, 0.5);
24+
fill: #fff;
25+
}
26+
</style>
27+
</head>
28+
29+
<body>
30+
<div class="js-bounty"></div>
31+
<script src="../lib/bounty.js"></script>
32+
<script>
33+
const animation = bounty.default({
34+
el: ".js-bounty",
35+
value: "£42,000,000",
36+
initialValue: "£123,456",
37+
});
38+
39+
const wait = (delay) =>
40+
new Promise((resolve) => setTimeout(resolve, delay));
41+
42+
const pasueAndRun = async () => {
43+
await wait(1500);
44+
animation.pause();
45+
await wait(2000);
46+
animation.resume();
47+
};
48+
49+
pasueAndRun();
50+
</script>
51+
</body>
52+
</html>

‎lib/bounty.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/bounty.js

+91-85
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,85 @@
1-
import loop from './loop';
2-
import { select, append, attr, style, text } from './selection';
3-
import transition from './transition';
1+
import loop from "./loop";
2+
import { select, append, attr, style, text } from "./selection";
3+
import transition from "./transition";
44

55
const DIGITS_COUNT = 10;
66
const ROTATIONS = 3;
77

88
const createDigitRoulette = (svg, fontSize, lineHeight, id) => {
99
const digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
1010
const roulette = svg
11-
::append('g')
12-
::attr('id', `digit-${id}`)
13-
::style('filter', `url(#motionFilter-${id})`);
11+
::append("g")
12+
::attr("id", `digit-${id}`)
13+
::style("filter", `url(#motionFilter-${id})`);
1414

1515
digits.forEach((el, i) => {
1616
roulette
17-
::append('text')
18-
::attr('y', -i * fontSize * lineHeight)
17+
::append("text")
18+
::attr("y", -i * fontSize * lineHeight)
1919
::text(el);
2020
});
2121

2222
return roulette;
2323
};
2424

2525
const createCharacter = (svg, el, fontSize) =>
26-
svg
27-
::append('g')
28-
::append('text')
29-
::text(el);
26+
svg::append("g")::append("text")::text(el);
3027

3128
const createFilter = (defs, id) =>
3229
defs
33-
::append('filter')
34-
::attr('id', `motionFilter-${id}`)
35-
::attr('width', '300%')
36-
::attr('x', '-100%')
37-
::append('feGaussianBlur')
38-
::attr('class', 'blurValues')
39-
::attr('in', 'SourceGraphic')
40-
::attr('stdDeviation', '0 0');
30+
::append("filter")
31+
::attr("id", `motionFilter-${id}`)
32+
::attr("width", "300%")
33+
::attr("x", "-100%")
34+
::append("feGaussianBlur")
35+
::attr("class", "blurValues")
36+
::attr("in", "SourceGraphic")
37+
::attr("stdDeviation", "0 0");
4138

4239
const createGradient = (defs, id) =>
4340
defs
44-
::append('linearGradient')
45-
::attr('id', `gradient-${id}`)
46-
::attr('x1', '0%')
47-
::attr('y1', '0%')
48-
::attr('x2', '0%')
49-
::attr('y2', '100%')
50-
::append('stop')
51-
::attr('offset', '0')
52-
::attr('stop-color', 'white')
53-
::attr('stop-opacity', '0')
41+
::append("linearGradient")
42+
::attr("id", `gradient-${id}`)
43+
::attr("x1", "0%")
44+
::attr("y1", "0%")
45+
::attr("x2", "0%")
46+
::attr("y2", "100%")
47+
::append("stop")
48+
::attr("offset", "0")
49+
::attr("stop-color", "white")
50+
::attr("stop-opacity", "0")
5451
::select(`#gradient-${id}`)
55-
::append('stop')
56-
::attr('offset', '0.2')
57-
::attr('stop-color', 'white')
58-
::attr('stop-opacity', '1')
52+
::append("stop")
53+
::attr("offset", "0.2")
54+
::attr("stop-color", "white")
55+
::attr("stop-opacity", "1")
5956
::select(`#gradient-${id}`)
60-
::append('stop')
61-
::attr('offset', '0.8')
62-
::attr('stop-color', 'white')
63-
::attr('stop-opacity', '1')
57+
::append("stop")
58+
::attr("offset", "0.8")
59+
::attr("stop-color", "white")
60+
::attr("stop-opacity", "1")
6461
::select(`#gradient-${id}`)
65-
::append('stop')
66-
::attr('offset', '1')
67-
::attr('stop-color', 'white')
68-
::attr('stop-opacity', '0');
62+
::append("stop")
63+
::attr("offset", "1")
64+
::attr("stop-color", "white")
65+
::attr("stop-opacity", "0");
6966

7067
const createMask = (defs, id) =>
7168
defs
72-
::append('mask')
73-
::attr('id', `mask-${id}`)
74-
::append('rect')
75-
::attr('x', 0)
76-
::attr('y', 0)
77-
::attr('width', '100%')
78-
::attr('height', '100%')
79-
::attr('fill', `url(#gradient-${id})`);
69+
::append("mask")
70+
::attr("id", `mask-${id}`)
71+
::append("rect")
72+
::attr("x", 0)
73+
::attr("y", 0)
74+
::attr("width", "100%")
75+
::attr("height", "100%")
76+
::attr("fill", `url(#gradient-${id})`);
8077

8178
const setViewBox = (svg, width, height) => {
82-
svg::attr('width', width);
83-
svg::attr('height', height);
84-
svg::attr('viewBox', `0 0 ${width} ${height}`);
85-
svg::style('overflow', 'hidden');
79+
svg::attr("width", width);
80+
svg::attr("height", height);
81+
svg::attr("viewBox", `0 0 ${width} ${height}`);
82+
svg::style("overflow", "hidden");
8683
};
8784

8885
export default ({
@@ -93,7 +90,7 @@ export default ({
9390
letterSpacing = 1,
9491
animationDelay = 100,
9592
letterAnimationDelay = 100,
96-
duration = 3000
93+
duration = 3000,
9794
}) => {
9895
const element = select(el);
9996
const computedStyle = window.getComputedStyle(element);
@@ -105,28 +102,26 @@ export default ({
105102
let canvasWidth = 0;
106103
const canvasHeight = fontSize * lineHeight + marginBottom;
107104

108-
element.innerHTML = '';
109-
const root = element::append('svg');
110-
const svg = root::append('svg')::attr('mask', `url(#mask-${salt})`);
111-
const defs = root::append('defs');
105+
element.innerHTML = "";
106+
const root = element::append("svg");
107+
const svg = root::append("svg")::attr("mask", `url(#mask-${salt})`);
108+
const defs = root::append("defs");
112109
createGradient(defs, salt);
113110
createMask(defs, salt);
114111

115112
const prepareValues = (value, secondValue) => {
116-
const values = String(value)
117-
.replace(/ /g, '\u00a0')
118-
.split('');
113+
const values = String(value).replace(/ /g, "\u00a0").split("");
119114

120115
const digitIndex = String(value).search(/\d/);
121116
while (secondValue.length > values.length) {
122117
const char =
123118
secondValue[secondValue.length - values.length - 1 + digitIndex];
124-
values.splice(digitIndex, 0, isNaN(parseInt(char, 10)) ? char : '0');
119+
values.splice(digitIndex, 0, isNaN(parseInt(char, 10)) ? char : "0");
125120
}
126121
return values;
127122
};
128123

129-
const initialString = String(initialValue || '0');
124+
const initialString = String(initialValue || "0");
130125
const values = prepareValues(String(value), initialString);
131126
const initial = prepareValues(initialString, String(value));
132127

@@ -137,7 +132,7 @@ export default ({
137132
isDigit: false,
138133
node: createCharacter(svg, char, fontSize),
139134
value: char,
140-
offset: { x: 0, y: offset }
135+
offset: { x: 0, y: offset },
141136
};
142137
} else {
143138
return {
@@ -149,14 +144,14 @@ export default ({
149144
initial: Number(initial[i]),
150145
offset: {
151146
x: 0,
152-
y: offset + Number(initial[i]) * (fontSize * lineHeight)
153-
}
147+
y: offset + Number(initial[i]) * (fontSize * lineHeight),
148+
},
154149
};
155150
}
156151
});
157152

158153
const transitions = [];
159-
const digits = chars.filter(char => char.isDigit);
154+
const digits = chars.filter((char) => char.isDigit);
160155
digits.forEach((digit, i) => {
161156
const sourceDistance = digit.initial * (fontSize * lineHeight);
162157
const targetDistance =
@@ -170,7 +165,7 @@ export default ({
170165
digit.offset.y =
171166
offset + (value % (fontSize * lineHeight * DIGITS_COUNT));
172167
digit.node::attr(
173-
'transform',
168+
"transform",
174169
`translate(${digit.offset.x}, ${digit.offset.y})`
175170
);
176171
const filterOrigin = (sourceDistance + targetDistance) / 2;
@@ -180,48 +175,59 @@ export default ({
180175
sourceDistance
181176
) / 100
182177
).toFixed(1);
183-
digit.filter::attr('stdDeviation', `0 ${motionValue}`);
178+
digit.filter::attr("stdDeviation", `0 ${motionValue}`);
184179
},
185-
end: i === 0 ? () => {
186-
element.querySelectorAll('[style*="filter"]').forEach(ele => {
187-
ele.style.filter = ''
188-
});
189-
cancelAnimation();
190-
} : e => e
180+
end:
181+
i === 0
182+
? () => {
183+
element.querySelectorAll('[style*="filter"]').forEach((ele) => {
184+
ele.style.filter = "";
185+
});
186+
cancel();
187+
}
188+
: (e) => e,
191189
});
192190
transitions.push(digitTransition);
193191
});
194192

195-
const update = timestamp => {
193+
const update = (timestamp) => {
196194
canvasWidth = 0;
197-
chars.forEach(char => {
195+
chars.forEach((char) => {
198196
const { width } = char.node.getBBox();
199197

200198
char.offset.x = canvasWidth;
201199
// set proper kerning for proportional fonts
202200
if (char.isDigit) {
203-
[...char.node.childNodes].forEach(element => {
201+
[...char.node.childNodes].forEach((element) => {
204202
const { width: letterWidth } = element.getBBox();
205203
const offset = (width - letterWidth) / 2;
206-
element.setAttribute('x', offset);
204+
element.setAttribute("x", offset);
207205
});
208206
}
209207

210208
canvasWidth += width + letterSpacing;
211209
});
212210
canvasWidth -= letterSpacing;
213211

214-
chars.forEach(char => {
212+
chars.forEach((char) => {
215213
char.node::attr(
216-
'transform',
214+
"transform",
217215
`translate(${char.offset.x}, ${char.offset.y})`
218216
);
219217
});
220218

221219
setViewBox(root, canvasWidth, canvasHeight);
222-
transitions.forEach(transition => transition.update(timestamp));
220+
transitions.forEach((transition) => transition.update(timestamp));
221+
};
222+
223+
const cancel = loop(update);
224+
225+
const pause = () => {
226+
transitions.forEach((transition) => transition.pause());
227+
};
228+
const resume = () => {
229+
transitions.forEach((transition) => transition.resume());
223230
};
224231

225-
const cancelAnimation = loop(update);
226-
return cancelAnimation;
232+
return { cancel, pause, resume };
227233
};

‎src/transition.js

+27-7
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,53 @@
1-
const cubicInOut = t => ((t *= 2) <= 1 ? t * t * t : (t -= 2) * t * t + 2) / 2;
1+
const cubicInOut = (t) =>
2+
((t *= 2) <= 1 ? t * t * t : (t -= 2) * t * t + 2) / 2;
23

34
export default ({
45
from,
56
to,
67
duration = 3000,
78
delay = 0,
89
easing = cubicInOut,
9-
start = v => v,
10-
step = v => v,
11-
end = v => v
10+
start = (v) => v,
11+
step = (v) => v,
12+
end = (v) => v,
1213
}) => {
1314
let value = from;
1415
let startTime = 0;
16+
let paused = false;
17+
let prevTime = 0;
1518
let finished = false;
16-
const update = timestamp => {
19+
const update = (timestamp) => {
1720
if (finished) {
1821
return;
1922
}
2023
if (!startTime) {
2124
startTime = timestamp;
25+
prevTime = timestamp;
2226
start(value);
2327
}
24-
const t = Math.min(Math.max(timestamp - startTime - delay, 0), duration) / duration;
28+
29+
if (paused) {
30+
startTime += timestamp - prevTime;
31+
}
32+
33+
const t =
34+
Math.min(Math.max(timestamp - startTime - delay, 0), duration) / duration;
2535
value = easing(t) * (to - from) + from;
2636
step(value);
2737
if (t === 1) {
2838
finished = true;
2939
end(value);
3040
}
41+
42+
prevTime = timestamp;
3143
};
32-
return { update };
44+
45+
const pause = () => {
46+
paused = true;
47+
};
48+
const resume = () => {
49+
paused = false;
50+
};
51+
52+
return { update, pause, resume };
3353
};

0 commit comments

Comments
 (0)
Please sign in to comment.