Skip to content

Commit ae7a855

Browse files
authored
Merge pull request #2356 from tf/wheel-fixes
Fix wheel animation for zero-crossing and countdown
2 parents 9bdd245 + 987917f commit ae7a855

3 files changed

Lines changed: 155 additions & 36 deletions

File tree

entry_types/scrolled/package/spec/contentElements/counter/useWheelCharacters-spec.js

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,23 @@ describe('createWheelCharacterFunctions', () => {
6565
]);
6666
});
6767

68-
it('hides minus sign when transitioning to negative', () => {
68+
it('hides minus sign when transitioning to negative integer', () => {
6969
const result = getRotationValues({value: -0.4, startValue: 0, targetValue: -1});
7070

7171
expect(result[0]).toEqual({text: '-', hide: true});
7272
});
7373

74+
it('shows minus sign for small negative values with decimal places', () => {
75+
const result = getRotationValues({
76+
value: -0.1,
77+
startValue: 0,
78+
targetValue: -0.5,
79+
decimalPlaces: 1
80+
});
81+
82+
expect(result[0]).toEqual({text: '-', hide: false});
83+
});
84+
7485
it('includes hidden minus sign at start when counting to negative', () => {
7586
const result = getRotationValues({value: 10, startValue: 10, targetValue: -10});
7687

@@ -127,7 +138,7 @@ describe('createWheelCharacterFunctions', () => {
127138
});
128139

129140
expect(result).toEqual([
130-
{value: 1, hideZero: false},
141+
{value: 1, hideZero: true},
131142
{text: ',', hide: false},
132143
{value: 2, hideZero: false},
133144
{value: 3, hideZero: false},
@@ -161,4 +172,94 @@ describe('createWheelCharacterFunctions', () => {
161172
expect(values[2]).toBe(0.5); // tens: 1 → 0 (99 rotations), at halfway = 0.5
162173
expect(values[3]).toBe(5); // ones: 0 → 0 (99 rotations), at halfway = 5
163174
});
175+
176+
it('animates digits when counting down from 10 to 9', () => {
177+
const result = getRotationValues({value: 9.5, startValue: 10, targetValue: 9});
178+
179+
expect(result.map(r => r.value)).toEqual([0.5, 9.5]);
180+
});
181+
182+
it('keeps hideZero true while digit value is below 1 when counting up past threshold', () => {
183+
const result = getRotationValues({value: 10, startValue: 9, targetValue: 15});
184+
185+
expect(result[0].hideZero).toBe(true);
186+
expect(result[0].value).toBeCloseTo(1 / 6);
187+
});
188+
189+
it('does not hide middle zero at end when digit completes full rotation', () => {
190+
const result = getRotationValues({value: 100, startValue: 0, targetValue: 100});
191+
192+
expect(result[1].hideZero).toBe(false);
193+
expect(result[1].value).toBe(0);
194+
});
195+
196+
it('does not hide middle zero at end when start was not a leading zero', () => {
197+
const result = getRotationValues({value: 100, startValue: 10, targetValue: 100});
198+
199+
expect(result[1].hideZero).toBe(false);
200+
expect(result[1].value).toBe(0);
201+
});
202+
203+
it('shows correct final digit when crossing zero from negative to positive', () => {
204+
const result = getRotationValues({value: 9, startValue: -10, targetValue: 9});
205+
206+
// tens digit should be 0, not 0.9
207+
expect(result[1].value).toBe(0);
208+
expect(result[2].value).toBe(9);
209+
});
210+
211+
it('shows correct final digit when crossing zero from positive to negative', () => {
212+
const result = getRotationValues({value: -9, startValue: 10, targetValue: -9});
213+
214+
expect(result[1].value).toBe(0);
215+
expect(result[2].value).toBe(9);
216+
});
217+
218+
it('works with decimal places when counting from 0 to 0.5', () => {
219+
const result = getRotationValues({
220+
value: 0.5,
221+
startValue: 0,
222+
targetValue: 0.5,
223+
decimalPlaces: 1,
224+
locale: 'en'
225+
});
226+
227+
expect(result).toEqual([
228+
{value: 0, hideZero: false},
229+
{text: '.'},
230+
{value: 5, hideZero: false}
231+
]);
232+
});
233+
234+
it('handles floating point precision when counting to 0.7', () => {
235+
const result = getRotationValues({
236+
value: 0.7,
237+
startValue: 0,
238+
targetValue: 0.7,
239+
decimalPlaces: 1,
240+
locale: 'en'
241+
});
242+
243+
expect(result).toEqual([
244+
{value: 0, hideZero: false},
245+
{text: '.'},
246+
{value: 7, hideZero: false}
247+
]);
248+
});
249+
250+
it('does not hide leading digit at start when counting down from 1900 to 0', () => {
251+
const result = getRotationValues({value: 1900, startValue: 1900, targetValue: 0});
252+
253+
// thousands digit should not have hideZero at value 1900
254+
expect(result[0].hideZero).toBe(false);
255+
expect(result[0].value).toBe(1);
256+
});
257+
258+
it('hides thousands digit at value 1000 when counting down from 1900', () => {
259+
const result = getRotationValues({value: 1000, startValue: 1900, targetValue: 0});
260+
261+
// at 1000, the "0" coming in on the thousands wheel should be hidden
262+
// since it will become a leading zero
263+
expect(result[0].hideZero).toBe(true);
264+
});
164265
});

entry_types/scrolled/package/src/contentElements/counter/WheelNumber.module.css

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,4 @@
2929

3030
.hidden {
3131
opacity: 0;
32-
transition: none;
3332
}
Lines changed: 52 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
import {useMemo} from 'react';
22

3-
export function createWheelCharacterFunctions({startValue, targetValue, decimalPlaces = 0, locale = 'en', useGrouping = false}) {
3+
export function useWheelCharacters({
4+
startValue, targetValue, decimalPlaces = 0, locale = 'en', useGrouping = false
5+
}) {
6+
return useMemo(
7+
() => createWheelCharacterFunctions({startValue, targetValue, decimalPlaces, locale, useGrouping}),
8+
[startValue, targetValue, decimalPlaces, locale, useGrouping]
9+
);
10+
}
11+
12+
export function createWheelCharacterFunctions({
13+
startValue, targetValue, decimalPlaces = 0, locale = 'en', useGrouping = false
14+
}) {
415
const hasNegative = startValue < 0 || targetValue < 0;
516
const crossesZero = (startValue > 0 && targetValue < 0) || (startValue < 0 && targetValue > 0);
617
const absStartValue = Math.abs(startValue);
@@ -9,8 +20,6 @@ export function createWheelCharacterFunctions({startValue, targetValue, decimalP
920
String(Math.round(absTargetValue)).length,
1021
String(Math.round(absStartValue)).length
1122
);
12-
const delta = absTargetValue - absStartValue;
13-
const range = targetValue - startValue;
1423

1524
const formatted = absTargetValue.toLocaleString(locale, {
1625
minimumIntegerDigits: integerDigitCount,
@@ -19,55 +28,65 @@ export function createWheelCharacterFunctions({startValue, targetValue, decimalP
1928
useGrouping
2029
});
2130

22-
const charFunctions = [];
2331
let digitIndex = 0;
2432

25-
for (const char of formatted) {
33+
const charFunctions = [...formatted].map((char) => {
2634
if (/\d/.test(char)) {
27-
const position = integerDigitCount - 1 - digitIndex;
35+
const position = integerDigitCount - digitIndex++ - 1;
2836
const divisor = Math.pow(10, position);
2937

3038
if (crossesZero) {
31-
charFunctions.push((absValue) => ({
32-
value: (absValue / divisor) % 10,
33-
hideZero: position > 0 && absValue < divisor
34-
}));
35-
} else {
36-
const startDigit = Math.floor(absStartValue / divisor) % 10;
37-
const endDigit = Math.floor(absTargetValue / divisor) % 10;
38-
const fullRotations = Math.floor(absTargetValue / (divisor * 10)) -
39-
Math.floor(absStartValue / (divisor * 10));
40-
let distance = endDigit - startDigit + fullRotations * 10;
41-
if (delta < 0 && endDigit > startDigit) distance -= 10;
39+
const toZero = createDigitCharFunction(position, divisor, absStartValue, 0);
40+
const fromZero = createDigitCharFunction(position, divisor, 0, absTargetValue);
41+
const inFirstSegment = (value) => startValue < 0 ? value < 0 : value > 0;
4242

43-
charFunctions.push((absValue, progress) => ({
44-
value: ((startDigit + progress * distance) % 10 + 10) % 10,
45-
hideZero: position > 0 && absValue < divisor
46-
}));
43+
return (value, progress) =>
44+
inFirstSegment(value) ?
45+
toZero(value, (value - startValue) / -startValue) :
46+
fromZero(value, value / targetValue);
47+
} else {
48+
return createDigitCharFunction(position, divisor, absStartValue, absTargetValue);
4749
}
48-
digitIndex++;
4950
} else if (digitIndex < integerDigitCount) {
5051
const threshold = Math.pow(10, integerDigitCount - digitIndex);
51-
charFunctions.push((absValue) => ({text: char, hide: absValue < threshold}));
52+
return (value) => ({text: char, hide: Math.abs(value) < threshold});
5253
} else {
53-
charFunctions.push(() => ({text: char}));
54+
return () => ({text: char});
5455
}
55-
}
56+
});
5657

5758
if (hasNegative) {
58-
charFunctions.unshift((absValue, progress, value) => ({text: '-', hide: value > -1}));
59+
const minusThreshold = -Math.pow(10, -decimalPlaces);
60+
charFunctions.unshift((value) => ({text: '-', hide: value > minusThreshold}));
5961
}
6062

63+
const range = targetValue - startValue;
64+
6165
return (value) => {
62-
const absValue = Math.abs(value);
6366
const progress = range === 0 ? 0 : (value - startValue) / range;
64-
return charFunctions.map(fn => fn(absValue, progress, value));
67+
return charFunctions.map(fn => fn(value, progress));
6568
};
6669
}
6770

68-
export function useWheelCharacters({startValue, targetValue, decimalPlaces = 0, locale = 'en', useGrouping = false}) {
69-
return useMemo(
70-
() => createWheelCharacterFunctions({startValue, targetValue, decimalPlaces, locale, useGrouping}),
71-
[startValue, targetValue, decimalPlaces, locale, useGrouping]
72-
);
71+
function createDigitCharFunction(position, divisor, segmentStart, segmentEnd) {
72+
const startDigit = getDigitAtPosition(segmentStart, divisor);
73+
const endDigit = getDigitAtPosition(segmentEnd, divisor);
74+
const fullRotations = Math.floor(segmentEnd / (divisor * 10)) -
75+
Math.floor(segmentStart / (divisor * 10));
76+
const distance = endDigit - startDigit + fullRotations * 10;
77+
78+
return (value, progress) => ({
79+
value: ((startDigit + progress * distance) % 10 + 10) % 10,
80+
hideZero: position > 0 && Math.abs(value) < divisor * 1.9
81+
});
82+
}
83+
84+
function getDigitAtPosition(value, divisor) {
85+
// Multiply by integer instead of dividing by fraction to avoid floating point errors
86+
// (e.g., 0.7 / 0.1 = 6.999... but 0.7 * 10 = 7)
87+
if (divisor < 1) {
88+
const multiplier = Math.round(1 / divisor);
89+
return Math.floor(value * multiplier) % 10;
90+
}
91+
return Math.floor(value / divisor) % 10;
7392
}

0 commit comments

Comments
 (0)