diff --git a/.gitignore b/.gitignore index 676edec..f25839b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ node_modules /coverage /lib/dist .* -_* \ No newline at end of file +/_* \ No newline at end of file diff --git a/docusaurus/docs/1_base_components/_category_.json b/docusaurus/docs/1_base_components/_category_.json new file mode 100644 index 0000000..b38bc40 --- /dev/null +++ b/docusaurus/docs/1_base_components/_category_.json @@ -0,0 +1,9 @@ +{ + "label": "Base components", + "position": 1, + "collapsible": false, + "link": { + "type": "generated-index", + "description": "Provided components" + } +} diff --git a/docusaurus/docs/3_examples/2_custom.mdx b/docusaurus/docs/3_examples/2_custom.mdx index b1a89ea..ac55383 100644 --- a/docusaurus/docs/3_examples/2_custom.mdx +++ b/docusaurus/docs/3_examples/2_custom.mdx @@ -91,7 +91,7 @@ function Example(props) { height={i === active ? tickHeight * 3 : tickHeight} key={i} transform={` - rotate(${angleOffset + stepSize * i} ${center} ${center}) + rotate(${angleOffset + stepSize * i} ${center[0]} ${center[1]}) translate( ${translateX} ${translateY}) `} /> diff --git a/docusaurus/docs/3_examples/_category_.json b/docusaurus/docs/3_examples/_category_.json new file mode 100644 index 0000000..083c7c2 --- /dev/null +++ b/docusaurus/docs/3_examples/_category_.json @@ -0,0 +1,9 @@ +{ + "label": "Examples", + "position": 3, + "collapsible": false, + "link": { + "type": "generated-index", + "description": "Few examples" + } +} diff --git a/docusaurus/src/components/DnlGradientKnob3/index.tsx b/docusaurus/src/components/DnlGradientKnob3/index.tsx index 6e383c8..50da13c 100644 --- a/docusaurus/src/components/DnlGradientKnob3/index.tsx +++ b/docusaurus/src/components/DnlGradientKnob3/index.tsx @@ -27,7 +27,7 @@ function customScaleTick({ className={className} key={i} transform={` - rotate(${angleOffset + stepSize * i} ${center} ${center}) + rotate(${angleOffset + stepSize * i} ${center[0]} ${center[1]}) translate( ${translateX} ${translateY}) `} /> diff --git a/docusaurus/src/components/GradientKnob3/index.tsx b/docusaurus/src/components/GradientKnob3/index.tsx index edd4dd2..1e606b2 100644 --- a/docusaurus/src/components/GradientKnob3/index.tsx +++ b/docusaurus/src/components/GradientKnob3/index.tsx @@ -31,7 +31,7 @@ export default function KnobExample(props: { size: number }) { fill={color} key={i} transform={` - rotate(${angleOffset + stepSize * i} ${center} ${center}) + rotate(${angleOffset + stepSize * i} ${center[0]} ${center[1]}) translate( ${translateX} ${translateY}) `} /> diff --git a/lib/src/Arc.tsx b/lib/src/Arc.tsx index 55ade03..4595e59 100644 --- a/lib/src/Arc.tsx +++ b/lib/src/Arc.tsx @@ -16,6 +16,7 @@ interface Props { export function Arc(props: Props) { const state = useKnobContext('Arc'); + const { geometry } = state; const { percentage } = state; const { className, @@ -23,7 +24,7 @@ export function Arc(props: Props) { arcWidth, color, background, - radius = state.radius, + radius = geometry.radius, } = props; return ( diff --git a/lib/src/Knob.tsx b/lib/src/Knob.tsx index 1ee21f0..7a76093 100644 --- a/lib/src/Knob.tsx +++ b/lib/src/Knob.tsx @@ -1,6 +1,6 @@ -import React, { PropsWithChildren, useEffect, useState } from 'react'; +import React, { PropsWithChildren, useMemo } from 'react'; import useUpdate from './useUpdate'; -import type { InteractiveHook, KnobState } from 'types'; +import type { InteractiveHook, KnobGeometry, KnobState } from 'types'; import { KnobContext } from './context'; interface Props { @@ -79,41 +79,27 @@ export function Knob(props: PropsWithChildren) { onStart, onEnd, }); - const radius = size / 2; - const center = size / 2; - const [knobState, setKnobState] = useState({ - value, - percentage, - size, - angleOffset, - angleRange, - radius, - center, - steps, - }); - - useEffect(() => { - setKnobState({ - value, - percentage, + const geometry = useMemo(() => { + const radius = size / 2; + const cx = size / 2; + return { size, angleOffset, angleRange, radius, - center, + center: [cx, cx], + }; + }, [size, angleOffset, angleRange]); + + const knobState = useMemo(() => { + return { + value, + percentage, + geometry, steps, - }); - }, [ - value, - percentage, - size, - angleOffset, - angleRange, - radius, - center, - steps, - ]); + }; + }, [value, percentage, geometry, steps]); return (
; } return ( - + ) { const state = useKnobContext('Pointer'); - const { angleRange, angleOffset, center } = state; + const { geometry } = state; + const { angleRange, angleOffset, center } = geometry; const { children, width, @@ -65,7 +66,7 @@ export function Pointer(props: React.PropsWithChildren) { useRotation = true, type, percentage = state.percentage, - radius = state.radius, + radius = geometry.radius, color = 'black', className, } = props; @@ -75,15 +76,15 @@ export function Pointer(props: React.PropsWithChildren) { return null; } if (useRotation) { - return `rotate(${ - angleOffset + angleRange * percentage - } ${center} ${center}) - translate( ${center} ${center - radius - (height ?? 0)})`; + return `rotate(${angleOffset + angleRange * percentage} ${ + center[0] + } ${center[1]}) + translate( ${center[0]} ${center[1] - radius - (height ?? 0)})`; } else { const angle = ((angleOffset + angleRange * percentage - 90) * Math.PI) / 180; - const x = center + radius * Math.cos(angle); - const y = center + radius * Math.sin(angle); + const x = center[0] + radius * Math.cos(angle); + const y = center[1] + radius * Math.sin(angle); return `translate(${x} ${y})`; } }, [percentage, angleOffset, angleRange, center, radius, height]); diff --git a/lib/src/Power.tsx b/lib/src/Power.tsx index 25db085..1242d9d 100644 --- a/lib/src/Power.tsx +++ b/lib/src/Power.tsx @@ -1,10 +1,15 @@ +import { KnobGeometry } from 'types'; import { useKnobContext } from './context'; import React, { useMemo } from 'react'; -function pointOnCircle(center: number, radius: number, angle: number) { +function pointOnCircle( + center: [number, number], + radius: number, + angle: number, +) { return { - x: center + radius * Math.cos(angle), - y: center + radius * Math.sin(angle), + x: center[0] + radius * Math.cos(angle), + y: center[1] + radius * Math.sin(angle), }; } @@ -12,12 +17,6 @@ function degTorad(deg: number) { return (Math.PI * deg) / 180; } -interface Geometry { - angleOffset: number; - angleRange: number; - center: number; -} - interface Position { percentage: number; radius: number; @@ -25,7 +24,7 @@ interface Position { } function calcPath( - geometry: Geometry, + geometry: KnobGeometry, positionFrom: Position, positionTo: Position | null, ): string | null { @@ -97,23 +96,19 @@ interface Props { export function Power(props: Props) { const state = useKnobContext('Power'); - const { percentage, angleOffset, angleRange, center } = state; + const { percentage, geometry } = state; const { interpolateTo, color, percentageFrom, - radiusFrom = state.radius, + radiusFrom = geometry.radius, percentageTo, - radiusTo = state.radius, + radiusTo = geometry.radius, widthFrom, widthTo, className, } = props; - const geometry: Geometry = useMemo(() => { - return { angleOffset, angleRange, center }; - }, [angleOffset, angleRange, center]); - const positionFrom: Position = useMemo( () => ({ percentage: percentageFrom, diff --git a/lib/src/Range.tsx b/lib/src/Range.tsx index 4a73d75..d2127d3 100644 --- a/lib/src/Range.tsx +++ b/lib/src/Range.tsx @@ -1,32 +1,31 @@ import React, { useMemo } from 'react'; import { useKnobContext } from './context'; +import { KnobGeometry } from 'types'; + +function pointOnCircle( + center: [number, number], + radius: number, + angle: number, +) { + return { + x: center[0] + radius * Math.cos(angle), + y: center[1] + radius * Math.sin(angle), + }; +} -const pointOnCircle = (center: number, radius: number, angle: number) => ({ - x: center + radius * Math.cos(angle), - y: center + radius * Math.sin(angle), -}); const degTorad = (deg: number) => (Math.PI * deg) / 180; const clampDeg = (deg: number) => deg >= 360 ? 359.999 : deg <= -360 ? -359.999 : deg; -const calcPath = ({ - percentageFrom, - percentageTo, - angleOffset, - angleRange, - arcWidth, - radius: outerRadius, - center, -}: { - percentageFrom: number; - percentageTo: number; - angleOffset: number; - angleRange: number; - arcWidth: number; - radius: number; - center: number; -}) => { +function calcPath( + geometry: KnobGeometry, + arcWidth: number, + outerRadius: number, + percentageFrom: number, + percentageTo: number, +) { + const { angleRange, angleOffset, center } = geometry; const angle = angleRange * (percentageTo - percentageFrom); const clampedAngle = clampDeg(angle); const angleFrom = angleOffset - 90 + angleRange * percentageFrom; @@ -48,7 +47,7 @@ const calcPath = ({ }L${p3.x},${p3.y} A${innerRadius},${innerRadius} 0 ${largeArcFlag} ${ 1 - direction } ${p4.x},${p4.y} L${p1.x},${p1.y}`; -}; +} interface Props { color?: string; @@ -65,12 +64,12 @@ interface Props { export function Range(props: Props) { const state = useKnobContext('Range'); - const { percentage } = state; + const { percentage, geometry } = state; const { color, percentageFrom = null, percentageTo = null, - radius = state.radius, + radius = geometry.radius, outerRadius, arcWidth, className, @@ -94,13 +93,13 @@ export function Range(props: Props) { if (pFrom === null || pTo === null) { return null; } - return calcPath({ + return calcPath( + geometry, arcWidth, - ...state, - radius: outerRadius ?? radius ?? state.radius, - percentageFrom: pFrom, - percentageTo: pTo, - }); + outerRadius ?? radius ?? geometry.radius, + pFrom, + pTo, + ); }, [pFrom, pTo, outerRadius, radius, state]); if (d === null) { diff --git a/lib/src/Scale.tsx b/lib/src/Scale.tsx index 0dff644..c896547 100644 --- a/lib/src/Scale.tsx +++ b/lib/src/Scale.tsx @@ -6,7 +6,7 @@ interface RenderProps { translateY: number; angleOffset: number; stepSize: number; - center: number; + center: [number, number]; color?: string; className?: string; active: number; @@ -35,7 +35,7 @@ function renderCircle({ fill={i === active ? activeColor : color} stroke="none" transform={` - rotate(${angleOffset + stepSize * i} ${center} ${center}) + rotate(${angleOffset + stepSize * i} ${center[0]} ${center[1]}) translate(${translateX} ${translateY}) `} /> @@ -67,7 +67,7 @@ function renderRect({ height={tickHeight} key={i} transform={` - rotate(${angleOffset + stepSize * i} ${center} ${center}) + rotate(${angleOffset + stepSize * i} ${center[0]} ${center[1]}) translate(${translateX} ${translateY}) `} /> @@ -118,11 +118,12 @@ interface Props { export function Scale(props: Props) { const state = useKnobContext('Scale'); - const { angleRange, angleOffset, percentage, center } = state; + const { percentage, geometry } = state; + const { angleRange, angleOffset, center } = geometry; const { steps = state.steps, type = 'rect', - radius = state.radius, + radius = geometry.radius, tickWidth, tickHeight, color, @@ -136,8 +137,8 @@ export function Scale(props: Props) { } const stepSize = angleRange / steps; const length = steps + (angleRange === 360 ? 0 : 1); - const translateX = center - tickWidth / 2; - const translateY = center - radius; + const translateX = center[0] - tickWidth / 2; + const translateY = center[1] - radius; if (percentage === null) { return <>; } diff --git a/lib/src/Spiral.tsx b/lib/src/Spiral.tsx index e7a5946..674a553 100644 --- a/lib/src/Spiral.tsx +++ b/lib/src/Spiral.tsx @@ -1,10 +1,14 @@ import { useKnobContext } from './context'; import React, { useMemo } from 'react'; -function pointOnCircle(center: number, radius: number, angle: number) { +function pointOnCircle( + center: [number, number], + radius: number, + angle: number, +) { return { - x: center + radius * Math.cos(angle), - y: center + radius * Math.sin(angle), + x: center[0] + radius * Math.cos(angle), + y: center[1] + radius * Math.sin(angle), }; } @@ -28,7 +32,7 @@ function calcPath(props: { arcWidth: number; outerRadiusFrom: number; outerRadiusTo: number; - center: number; + center: [number, number]; }) { const { percentageFrom, @@ -89,7 +93,8 @@ interface Props { export function Spiral(props: Props) { const state = useKnobContext('Spiral'); - const { percentage, angleOffset, angleRange, center } = state; + const { percentage, geometry } = state; + const { angleOffset, angleRange, center } = geometry; const { color, percentageFrom = null, diff --git a/lib/src/Value.tsx b/lib/src/Value.tsx index 1f713a2..70fa901 100644 --- a/lib/src/Value.tsx +++ b/lib/src/Value.tsx @@ -13,7 +13,8 @@ interface Props { export function Value(props: Props) { const state = useKnobContext('Value'); - const { size } = state; + const { geometry } = state; + const { size } = geometry; const { decimalPlace = 0, className, diff --git a/lib/src/types.ts b/lib/src/types.ts index b723b44..ae07a4f 100644 --- a/lib/src/types.ts +++ b/lib/src/types.ts @@ -1,3 +1,17 @@ +/** + * Part of the Knob state which is usually unchanged. + */ +export interface KnobGeometry { + size: number; + angleOffset: number; + angleRange: number; + radius: number; + /** + * X and Y position of the center related to the upper left corner. + */ + center: [number, number]; +} + /** * Knob state as exposed to the sub components */ @@ -12,11 +26,11 @@ export interface KnobState { */ percentage: number | null; - size: number; - angleOffset: number; - angleRange: number; - radius: number; - center: number; + /** + * Part of the state which is usually unchanged. + */ + geometry: KnobGeometry; + steps?: number; } diff --git a/lib/test/Power.spec.tsx b/lib/test/Power.spec.tsx new file mode 100644 index 0000000..9c4d28d --- /dev/null +++ b/lib/test/Power.spec.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { describe, it, expect } from 'vitest'; +import { render } from '@testing-library/react'; +import { Knob, Power } from '../src'; + +describe('Power', () => { + it('renders correctlty', () => { + const { container } = render( + + + , + ); + expect(container.children[0].children[0].children[0]).toMatchSnapshot(); + }); +}); diff --git a/lib/test/__snapshots__/Power.spec.tsx.snap b/lib/test/__snapshots__/Power.spec.tsx.snap new file mode 100644 index 0000000..3f3d1c9 --- /dev/null +++ b/lib/test/__snapshots__/Power.spec.tsx.snap @@ -0,0 +1,9 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Power > renders correctlty 1`] = ` + + + +`;