Canonicalize shared toolbar mechanics

This commit is contained in:
DCCONSTRUCTIONS
2026-07-11 11:35:09 +03:00
parent 888fdbeb69
commit 6b42d913ea
10 changed files with 707 additions and 53 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
export * from "./accent.js";
export * from "./floating.js";
export * from "./theme.js";
export * from "./toolbar.js";
+106
View File
@@ -0,0 +1,106 @@
export type ToolbarPlacement = "left" | "right" | "bottom";
export interface ToolbarSettings {
minSize: number;
maxSize: number;
lensCount: number;
gap: number;
}
export interface ToolbarTarget {
size: number;
shift: number;
scale: number;
}
export const NODEDC_TOOLBAR_DEFAULTS = Object.freeze({
background: "#111115",
border: "#111117",
outline: "#1c1c1c",
minSize: 25,
maxSize: 87,
lensCount: 5,
gap: 10,
padding: 6,
hideDelay: 5000,
});
export const NODEDC_TOOLBAR_SPRING = Object.freeze({
mass: 0.1,
stiffness: 170,
damping: 12,
maxStep: 1 / 240,
});
const BASE_SCALE = 2.25;
const BASE_DISTANCE = 110;
const BASE_NUDGE = 40;
const NUDGE_TIGHTNESS = 0.82;
export function normalizeToolbarSettings({
minSize = NODEDC_TOOLBAR_DEFAULTS.minSize,
maxSize = NODEDC_TOOLBAR_DEFAULTS.maxSize,
lensCount = NODEDC_TOOLBAR_DEFAULTS.lensCount,
gap = NODEDC_TOOLBAR_DEFAULTS.gap,
}: Partial<ToolbarSettings> = {}): ToolbarSettings {
const safeMin = Math.max(18, Math.min(42, Number.isFinite(minSize) ? minSize : NODEDC_TOOLBAR_DEFAULTS.minSize));
const safeMax = Math.max(safeMin, Math.min(88, Number.isFinite(maxSize) ? maxSize : NODEDC_TOOLBAR_DEFAULTS.maxSize));
const roundedLens = Math.max(1, Math.min(13, Math.round(Number.isFinite(lensCount) ? lensCount : NODEDC_TOOLBAR_DEFAULTS.lensCount)));
return {
minSize: safeMin,
maxSize: safeMax,
lensCount: roundedLens % 2 === 0 ? Math.min(13, roundedLens + 1) : roundedLens,
gap: Math.max(0, Number.isFinite(gap) ? gap : NODEDC_TOOLBAR_DEFAULTS.gap),
};
}
export function stepToolbarSpring(value: number, velocity: number, target: number, dt: number) {
let nextValue = value;
let nextVelocity = velocity;
const duration = Math.min(0.032, Math.max(0.001, dt));
const steps = Math.max(1, Math.ceil(duration / NODEDC_TOOLBAR_SPRING.maxStep));
const step = duration / steps;
for (let index = 0; index < steps; index += 1) {
const displacement = nextValue - target;
const springForce = -NODEDC_TOOLBAR_SPRING.stiffness * displacement;
const dampingForce = -NODEDC_TOOLBAR_SPRING.damping * nextVelocity;
const acceleration = (springForce + dampingForce) / NODEDC_TOOLBAR_SPRING.mass;
nextVelocity += acceleration * step;
nextValue += nextVelocity * step;
}
return [nextValue, nextVelocity] as const;
}
export function calculateToolbarTargets(
centers: readonly number[],
pointerCoordinate: number,
settings: ToolbarSettings,
): ToolbarTarget[] {
const step = settings.minSize + settings.gap;
const lensRadius = Math.max(
settings.minSize,
Math.floor((settings.lensCount - 1) / 2) * step + settings.gap / 2,
);
const maxScale = settings.minSize > 0 ? settings.maxSize / settings.minSize : 1;
const scaledNudge = BASE_NUDGE
* (lensRadius / BASE_DISTANCE)
* (Math.max(0, maxScale - 1) / (BASE_SCALE - 1))
* NUDGE_TIGHTNESS;
return centers.map((center) => {
const distance = pointerCoordinate - center;
const absoluteDistance = Math.abs(distance);
const scale = absoluteDistance >= lensRadius
? 1
: 1 + (maxScale - 1) * (1 - absoluteDistance / lensRadius);
return {
scale,
size: settings.minSize * scale,
shift: absoluteDistance > lensRadius
? Math.sign(distance || 1) * -scaledNudge
: (-distance / lensRadius) * scaledNudge * scale,
};
});
}