Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,23 @@ describe('createWheelCharacterFunctions', () => {
]);
});

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

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

it('shows minus sign for small negative values with decimal places', () => {
const result = getRotationValues({
value: -0.1,
startValue: 0,
targetValue: -0.5,
decimalPlaces: 1
});

expect(result[0]).toEqual({text: '-', hide: false});
});

it('includes hidden minus sign at start when counting to negative', () => {
const result = getRotationValues({value: 10, startValue: 10, targetValue: -10});

Expand Down Expand Up @@ -127,7 +138,7 @@ describe('createWheelCharacterFunctions', () => {
});

expect(result).toEqual([
{value: 1, hideZero: false},
{value: 1, hideZero: true},
{text: ',', hide: false},
{value: 2, hideZero: false},
{value: 3, hideZero: false},
Expand Down Expand Up @@ -161,4 +172,94 @@ describe('createWheelCharacterFunctions', () => {
expect(values[2]).toBe(0.5); // tens: 1 → 0 (99 rotations), at halfway = 0.5
expect(values[3]).toBe(5); // ones: 0 → 0 (99 rotations), at halfway = 5
});

it('animates digits when counting down from 10 to 9', () => {
const result = getRotationValues({value: 9.5, startValue: 10, targetValue: 9});

expect(result.map(r => r.value)).toEqual([0.5, 9.5]);
});

it('keeps hideZero true while digit value is below 1 when counting up past threshold', () => {
const result = getRotationValues({value: 10, startValue: 9, targetValue: 15});

expect(result[0].hideZero).toBe(true);
expect(result[0].value).toBeCloseTo(1 / 6);
});

it('does not hide middle zero at end when digit completes full rotation', () => {
const result = getRotationValues({value: 100, startValue: 0, targetValue: 100});

expect(result[1].hideZero).toBe(false);
expect(result[1].value).toBe(0);
});

it('does not hide middle zero at end when start was not a leading zero', () => {
const result = getRotationValues({value: 100, startValue: 10, targetValue: 100});

expect(result[1].hideZero).toBe(false);
expect(result[1].value).toBe(0);
});

it('shows correct final digit when crossing zero from negative to positive', () => {
const result = getRotationValues({value: 9, startValue: -10, targetValue: 9});

// tens digit should be 0, not 0.9
expect(result[1].value).toBe(0);
expect(result[2].value).toBe(9);
});

it('shows correct final digit when crossing zero from positive to negative', () => {
const result = getRotationValues({value: -9, startValue: 10, targetValue: -9});

expect(result[1].value).toBe(0);
expect(result[2].value).toBe(9);
});

it('works with decimal places when counting from 0 to 0.5', () => {
const result = getRotationValues({
value: 0.5,
startValue: 0,
targetValue: 0.5,
decimalPlaces: 1,
locale: 'en'
});

expect(result).toEqual([
{value: 0, hideZero: false},
{text: '.'},
{value: 5, hideZero: false}
]);
});

it('handles floating point precision when counting to 0.7', () => {
const result = getRotationValues({
value: 0.7,
startValue: 0,
targetValue: 0.7,
decimalPlaces: 1,
locale: 'en'
});

expect(result).toEqual([
{value: 0, hideZero: false},
{text: '.'},
{value: 7, hideZero: false}
]);
});

it('does not hide leading digit at start when counting down from 1900 to 0', () => {
const result = getRotationValues({value: 1900, startValue: 1900, targetValue: 0});

// thousands digit should not have hideZero at value 1900
expect(result[0].hideZero).toBe(false);
expect(result[0].value).toBe(1);
});

it('hides thousands digit at value 1000 when counting down from 1900', () => {
const result = getRotationValues({value: 1000, startValue: 1900, targetValue: 0});

// at 1000, the "0" coming in on the thousands wheel should be hidden
// since it will become a leading zero
expect(result[0].hideZero).toBe(true);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,4 @@

.hidden {
opacity: 0;
transition: none;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import {useMemo} from 'react';

export function createWheelCharacterFunctions({startValue, targetValue, decimalPlaces = 0, locale = 'en', useGrouping = false}) {
export function useWheelCharacters({
startValue, targetValue, decimalPlaces = 0, locale = 'en', useGrouping = false
}) {
return useMemo(
() => createWheelCharacterFunctions({startValue, targetValue, decimalPlaces, locale, useGrouping}),
[startValue, targetValue, decimalPlaces, locale, useGrouping]
);
}

export function createWheelCharacterFunctions({
startValue, targetValue, decimalPlaces = 0, locale = 'en', useGrouping = false
}) {
const hasNegative = startValue < 0 || targetValue < 0;
const crossesZero = (startValue > 0 && targetValue < 0) || (startValue < 0 && targetValue > 0);
const absStartValue = Math.abs(startValue);
Expand All @@ -9,8 +20,6 @@ export function createWheelCharacterFunctions({startValue, targetValue, decimalP
String(Math.round(absTargetValue)).length,
String(Math.round(absStartValue)).length
);
const delta = absTargetValue - absStartValue;
const range = targetValue - startValue;

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

const charFunctions = [];
let digitIndex = 0;

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

if (crossesZero) {
charFunctions.push((absValue) => ({
value: (absValue / divisor) % 10,
hideZero: position > 0 && absValue < divisor
}));
} else {
const startDigit = Math.floor(absStartValue / divisor) % 10;
const endDigit = Math.floor(absTargetValue / divisor) % 10;
const fullRotations = Math.floor(absTargetValue / (divisor * 10)) -
Math.floor(absStartValue / (divisor * 10));
let distance = endDigit - startDigit + fullRotations * 10;
if (delta < 0 && endDigit > startDigit) distance -= 10;
const toZero = createDigitCharFunction(position, divisor, absStartValue, 0);
const fromZero = createDigitCharFunction(position, divisor, 0, absTargetValue);
const inFirstSegment = (value) => startValue < 0 ? value < 0 : value > 0;

charFunctions.push((absValue, progress) => ({
value: ((startDigit + progress * distance) % 10 + 10) % 10,
hideZero: position > 0 && absValue < divisor
}));
return (value, progress) =>
inFirstSegment(value) ?
toZero(value, (value - startValue) / -startValue) :
fromZero(value, value / targetValue);
} else {
return createDigitCharFunction(position, divisor, absStartValue, absTargetValue);
}
digitIndex++;
} else if (digitIndex < integerDigitCount) {
const threshold = Math.pow(10, integerDigitCount - digitIndex);
charFunctions.push((absValue) => ({text: char, hide: absValue < threshold}));
return (value) => ({text: char, hide: Math.abs(value) < threshold});
} else {
charFunctions.push(() => ({text: char}));
return () => ({text: char});
}
}
});

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

const range = targetValue - startValue;

return (value) => {
const absValue = Math.abs(value);
const progress = range === 0 ? 0 : (value - startValue) / range;
return charFunctions.map(fn => fn(absValue, progress, value));
return charFunctions.map(fn => fn(value, progress));
};
}

export function useWheelCharacters({startValue, targetValue, decimalPlaces = 0, locale = 'en', useGrouping = false}) {
return useMemo(
() => createWheelCharacterFunctions({startValue, targetValue, decimalPlaces, locale, useGrouping}),
[startValue, targetValue, decimalPlaces, locale, useGrouping]
);
function createDigitCharFunction(position, divisor, segmentStart, segmentEnd) {
const startDigit = getDigitAtPosition(segmentStart, divisor);
const endDigit = getDigitAtPosition(segmentEnd, divisor);
const fullRotations = Math.floor(segmentEnd / (divisor * 10)) -
Math.floor(segmentStart / (divisor * 10));
const distance = endDigit - startDigit + fullRotations * 10;

return (value, progress) => ({
value: ((startDigit + progress * distance) % 10 + 10) % 10,
hideZero: position > 0 && Math.abs(value) < divisor * 1.9
});
}

function getDigitAtPosition(value, divisor) {
// Multiply by integer instead of dividing by fraction to avoid floating point errors
// (e.g., 0.7 / 0.1 = 6.999... but 0.7 * 10 = 7)
if (divisor < 1) {
const multiplier = Math.round(1 / divisor);
return Math.floor(value * multiplier) % 10;
}
return Math.floor(value / divisor) % 10;
}
Loading