feat(web/radial): radial menu text scaling (#639)

* radial menu improvements

* addressing comments from Luke

* removing uneeded comments

* style(web/radial): remove icon scaling, increase number of items on page

* style(web/radial): cursor pointer, remove stroke, container overflow

---------

Co-authored-by: Luke <39926192+LukeWasTakenn@users.noreply.github.com>
This commit is contained in:
NotMango
2024-09-12 13:40:59 +01:00
committed by GitHub
parent 9f5879e9e4
commit 80267eb1f0
2 changed files with 94 additions and 58 deletions

View File

@@ -10,9 +10,10 @@ export const debugRadial = () => {
{ icon: 'palette', label: 'Paint' }, { icon: 'palette', label: 'Paint' },
{ iconWidth: 35, iconHeight: 35, icon: 'https://icon-library.com/images/white-icon-png/white-icon-png-18.jpg', label: 'External icon'}, { iconWidth: 35, iconHeight: 35, icon: 'https://icon-library.com/images/white-icon-png/white-icon-png-18.jpg', label: 'External icon'},
{ icon: 'warehouse', label: 'Garage' }, { icon: 'warehouse', label: 'Garage' },
{ icon: 'palette', label: 'Quite long \ntext' }, { icon: 'palette', label: 'Quite Long Text' },
{ icon: 'palette', label: 'Fahrzeuginteraktionen' },
{ icon: 'palette', label: 'Fahrzeuginteraktionen' },
{ icon: 'palette', label: 'Paint' }, { icon: 'palette', label: 'Paint' },
{ icon: 'warehouse', label: 'Garage' },
], ],
}, },
}, },

View File

@@ -22,12 +22,14 @@ const useStyles = createStyles((theme) => ({
'&:hover': { '&:hover': {
fill: theme.fn.primaryColor(), fill: theme.fn.primaryColor(),
cursor: 'pointer',
'> g > text, > g > svg > path': { '> g > text, > g > svg > path': {
fill: '#fff', fill: '#fff',
}, },
}, },
'> g > text': { '> g > text': {
fill: theme.colors.dark[0], fill: theme.colors.dark[0],
strokeWidth: 0,
}, },
}, },
backgroundCircle: { backgroundCircle: {
@@ -39,6 +41,7 @@ const useStyles = createStyles((theme) => ({
stroke: theme.colors.dark[6], stroke: theme.colors.dark[6],
strokeWidth: 4, strokeWidth: 4,
'&:hover': { '&:hover': {
cursor: 'pointer',
fill: theme.colors[theme.primaryColor][theme.fn.primaryShade() - 1], fill: theme.colors[theme.primaryColor][theme.fn.primaryShade() - 1],
}, },
}, },
@@ -54,14 +57,37 @@ const useStyles = createStyles((theme) => ({
}, },
})); }));
// includes More... button const calculateFontSize = (text: string): number => {
const PAGE_ITEMS = 8; if (text.length > 20) return 10;
if (text.length > 15) return 12;
return 13;
};
const splitTextIntoLines = (text: string, maxCharPerLine: number = 15): string[] => {
const words = text.split(' ');
const lines: string[] = [];
let currentLine = words[0];
for (let i = 1; i < words.length; i++) {
if (currentLine.length + words[i].length + 1 <= maxCharPerLine) {
currentLine += ' ' + words[i];
} else {
lines.push(currentLine);
currentLine = words[i];
}
}
lines.push(currentLine);
return lines;
};
const PAGE_ITEMS = 6;
const degToRad = (deg: number) => deg * (Math.PI / 180); const degToRad = (deg: number) => deg * (Math.PI / 180);
const RadialMenu: React.FC = () => { const RadialMenu: React.FC = () => {
const { classes } = useStyles(); const { classes } = useStyles();
const { locale } = useLocales(); const { locale } = useLocales();
const newDimension = 350 * 1.1025;
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [menuItems, setMenuItems] = useState<RadialMenuItem[]>([]); const [menuItems, setMenuItems] = useState<RadialMenuItem[]>([]);
const [menu, setMenu] = useState<{ items: RadialMenuItem[]; sub?: boolean; page: number }>({ const [menu, setMenu] = useState<{ items: RadialMenuItem[]; sub?: boolean; page: number }>({
@@ -119,74 +145,83 @@ const RadialMenu: React.FC = () => {
}} }}
> >
<ScaleFade visible={visible}> <ScaleFade visible={visible}>
<svg width="350px" height="350px" transform="rotate(90)"> <svg
{/*Fixed issues with background circle extending the circle when there's less than 3 items*/} style={{ overflow: 'visible' }}
width={`${newDimension}px`}
height={`${newDimension}px`}
viewBox="0 0 350 350"
transform="rotate(90)"
>
{/* Fixed issues with background circle extending the circle when there's less than 3 items */}
<g transform="translate(175, 175)"> <g transform="translate(175, 175)">
<circle r={175} className={classes.backgroundCircle} /> <circle r={175} className={classes.backgroundCircle} />
</g> </g>
{menuItems.map((item, index) => { {menuItems.map((item, index) => {
// Always draw full circle to avoid elipse circles with 2 or less items
const pieAngle = 360 / (menuItems.length < 3 ? 3 : menuItems.length); const pieAngle = 360 / (menuItems.length < 3 ? 3 : menuItems.length);
const angle = degToRad(pieAngle / 2 + 90); const angle = degToRad(pieAngle / 2 + 90);
const gap = 0; const gap = 1;
const radius = 175 * 0.65 - gap; const radius = 175 * 0.65 - gap;
const sinAngle = Math.sin(angle); const sinAngle = Math.sin(angle);
const cosAngle = Math.cos(angle); const cosAngle = Math.cos(angle);
const iconYOffset = splitTextIntoLines(item.label, 15).length > 3 ? 3 : 0;
const iconX = 175 + sinAngle * radius; const iconX = 175 + sinAngle * radius;
const iconY = 175 + cosAngle * radius; const iconY = 175 + cosAngle * radius + iconYOffset; // Apply the Y offset to iconY
const iconWidth = Math.min(Math.max(item.iconWidth || 50, 0), 100); const iconWidth = Math.min(Math.max(item.iconWidth || 50, 0), 100);
const iconHeight = Math.min(Math.max(item.iconHeight || 50, 0), 100); const iconHeight = Math.min(Math.max(item.iconHeight || 50, 0), 100);
return ( return (
<> <g
<g transform={`rotate(-${index * pieAngle} 175 175) translate(${sinAngle * gap}, ${cosAngle * gap})`}
transform={`rotate(-${index * pieAngle} 175 175) translate(${sinAngle * gap}, ${cosAngle * gap})`} className={classes.sector}
className={classes.sector} onClick={async () => {
onClick={async () => { const clickIndex = menu.page === 1 ? index : PAGE_ITEMS * (menu.page - 1) - (menu.page - 1) + index;
const clickIndex = if (!item.isMore) fetchNui('radialClick', clickIndex);
menu.page === 1 ? index : PAGE_ITEMS * (menu.page - 1) - (menu.page - 1) + index; else {
if (!item.isMore) fetchNui('radialClick', clickIndex); await changePage(true);
else { }
await changePage(true); }}
} >
}} <path
> d={`M175.01,175.01 l${175 - gap},0 A175.01,175.01 0 0,0 ${
<path 175 + (175 - gap) * Math.cos(-degToRad(pieAngle))
d={`M175.01,175.01 l${175 - gap},0 A175.01,175.01 0 0,0 ${ }, ${175 + (175 - gap) * Math.sin(-degToRad(pieAngle))} z`}
175 + (175 - gap) * Math.cos(-degToRad(pieAngle)) />
}, ${175 + (175 - gap) * Math.sin(-degToRad(pieAngle))} z`} <g transform={`rotate(${index * pieAngle - 90} ${iconX} ${iconY})`} pointerEvents="none">
/> {typeof item.icon === 'string' && isIconUrl(item.icon) ? (
<g transform={`rotate(${index * pieAngle - 90} ${iconX} ${iconY})`} pointerEvents="none"> <image
{typeof item.icon === 'string' && isIconUrl(item.icon) ? ( href={item.icon}
<image width={iconWidth}
href={item.icon} height={iconHeight}
width={iconWidth} x={iconX - iconWidth / 2}
height={iconHeight} y={iconY - iconHeight / 2 - iconHeight / 4}
x={iconX - iconWidth / 2} />
y={iconY - iconHeight / 2 - iconHeight / 4} ) : (
/> <LibIcon
) : ( x={iconX - 14.5}
<LibIcon x={iconX - 12.5} y={iconY - 17.5} icon={item.icon as IconProp} width={25} height={25} fixedWidth/> y={iconY - 17.5}
)} icon={item.icon as IconProp}
<text width={30}
x={iconX} height={30}
y={iconY + (item.label.includes(' \n') ? 7 : 25)} fixedWidth
fill="#fff" />
textAnchor="middle" )}
pointerEvents="none" <text
> x={iconX}
{item.label.includes(' \n') y={iconY + (splitTextIntoLines(item.label, 15).length > 2 ? 15 : 28)}
? item.label.split(' \n').map((value) => ( fill="#fff"
<tspan x={iconX} dy="1.2em"> textAnchor="middle"
{value} fontSize={calculateFontSize(item.label)}
</tspan> pointerEvents="none"
)) lengthAdjust="spacingAndGlyphs"
: item.label} >
</text> {splitTextIntoLines(item.label, 15).map((line, index) => (
</g> <tspan x={iconX} dy={index === 0 ? 0 : '1.2em'} key={index}>
{line}
</tspan>
))}
</text>
</g> </g>
</> </g>
); );
})} })}
<g <g
@@ -202,7 +237,7 @@ const RadialMenu: React.FC = () => {
} }
}} }}
> >
<circle r={32} className={classes.centerCircle} /> <circle r={28} className={classes.centerCircle} />
</g> </g>
</svg> </svg>
<div className={classes.centerIconContainer}> <div className={classes.centerIconContainer}>