diff --git a/entry_types/scrolled/package/spec/contentElements/counter/useWheelCharacters-spec.js b/entry_types/scrolled/package/spec/contentElements/counter/useWheelCharacters-spec.js index d147e0594..264f0bb2b 100644 --- a/entry_types/scrolled/package/spec/contentElements/counter/useWheelCharacters-spec.js +++ b/entry_types/scrolled/package/spec/contentElements/counter/useWheelCharacters-spec.js @@ -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}); @@ -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}, @@ -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); + }); }); diff --git a/entry_types/scrolled/package/src/contentElements/counter/WheelNumber.module.css b/entry_types/scrolled/package/src/contentElements/counter/WheelNumber.module.css index 8d48e1ee4..5f49fb336 100644 --- a/entry_types/scrolled/package/src/contentElements/counter/WheelNumber.module.css +++ b/entry_types/scrolled/package/src/contentElements/counter/WheelNumber.module.css @@ -29,5 +29,4 @@ .hidden { opacity: 0; - transition: none; } diff --git a/entry_types/scrolled/package/src/contentElements/counter/useWheelCharacters.js b/entry_types/scrolled/package/src/contentElements/counter/useWheelCharacters.js index 7bf8da5a6..284718c18 100644 --- a/entry_types/scrolled/package/src/contentElements/counter/useWheelCharacters.js +++ b/entry_types/scrolled/package/src/contentElements/counter/useWheelCharacters.js @@ -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); @@ -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, @@ -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; }