Canonicalize shared toolbar mechanics
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
export * from "./accent.js";
|
||||
export * from "./floating.js";
|
||||
export * from "./theme.js";
|
||||
|
||||
export * from "./toolbar.js";
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
}
|
||||
+69
-14
@@ -2104,20 +2104,39 @@ textarea.nodedc-field__control {
|
||||
.nodedc-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.42rem;
|
||||
border: 1px solid var(--nodedc-toolbar-outline);
|
||||
border-radius: calc(var(--nodedc-toolbar-radius) + 0.42rem);
|
||||
background: color-mix(in srgb, var(--nodedc-toolbar-bg) 88%, transparent);
|
||||
box-shadow: 0 10px 28px color-mix(in srgb, var(--nodedc-toolbar-outline) 36%, transparent);
|
||||
backdrop-filter: blur(24px) saturate(1.12);
|
||||
-webkit-backdrop-filter: blur(24px) saturate(1.12);
|
||||
gap: var(--nodedc-toolbar-gap, 10px);
|
||||
background: transparent;
|
||||
transition: opacity 180ms ease, transform 220ms ease;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
.nodedc-toolbar-wrap[data-placement="left"] .nodedc-toolbar,
|
||||
.nodedc-toolbar-wrap[data-placement="right"] .nodedc-toolbar {
|
||||
.nodedc-toolbar-wrap[data-placement="left"] .nodedc-toolbar {
|
||||
width: var(--nodedc-toolbar-min-size);
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: var(--nodedc-toolbar-padding, 6px) 0;
|
||||
}
|
||||
|
||||
.nodedc-toolbar-wrap[data-placement="right"] .nodedc-toolbar {
|
||||
width: var(--nodedc-toolbar-min-size);
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
padding: var(--nodedc-toolbar-padding, 6px) 0;
|
||||
}
|
||||
|
||||
.nodedc-toolbar-wrap[data-placement="bottom"] .nodedc-toolbar {
|
||||
height: var(--nodedc-toolbar-min-size);
|
||||
align-items: flex-end;
|
||||
padding: 0 var(--nodedc-toolbar-padding, 6px);
|
||||
}
|
||||
|
||||
.nodedc-toolbar-wrap[data-placement="left"][data-active="true"] .nodedc-toolbar,
|
||||
.nodedc-toolbar-wrap[data-placement="right"][data-active="true"] .nodedc-toolbar {
|
||||
width: var(--nodedc-toolbar-max-size);
|
||||
}
|
||||
|
||||
.nodedc-toolbar-wrap[data-placement="bottom"][data-active="true"] .nodedc-toolbar {
|
||||
height: var(--nodedc-toolbar-max-size);
|
||||
}
|
||||
|
||||
.nodedc-toolbar-wrap[data-autohide="true"]:not([data-visible="true"]) .nodedc-toolbar {
|
||||
@@ -2125,6 +2144,10 @@ textarea.nodedc-field__control {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nodedc-toolbar-wrap[data-autohide="true"]:not([data-visible="true"]) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nodedc-toolbar-wrap[data-placement="left"][data-autohide="true"]:not([data-visible="true"]) .nodedc-toolbar {
|
||||
transform: translateX(calc(-100% - 1rem));
|
||||
}
|
||||
@@ -2139,6 +2162,7 @@ textarea.nodedc-field__control {
|
||||
|
||||
.nodedc-toolbar__hotzone {
|
||||
position: fixed;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.nodedc-toolbar-wrap[data-placement="left"] .nodedc-toolbar__hotzone {
|
||||
@@ -2160,9 +2184,9 @@ textarea.nodedc-field__control {
|
||||
|
||||
.nodedc-toolbar__button {
|
||||
display: grid;
|
||||
width: var(--nodedc-toolbar-item-size);
|
||||
height: var(--nodedc-toolbar-item-size);
|
||||
flex: 0 0 var(--nodedc-toolbar-item-size);
|
||||
width: var(--nodedc-toolbar-min-size);
|
||||
height: var(--nodedc-toolbar-min-size);
|
||||
flex: 0 0 var(--nodedc-toolbar-min-size);
|
||||
place-items: center;
|
||||
border: 1px solid var(--nodedc-toolbar-border);
|
||||
border-radius: var(--nodedc-toolbar-radius);
|
||||
@@ -2170,7 +2194,32 @@ textarea.nodedc-field__control {
|
||||
background: var(--nodedc-toolbar-bg);
|
||||
color: var(--nodedc-toolbar-accent);
|
||||
padding: 0;
|
||||
transition: width 120ms ease, height 120ms ease, flex-basis 120ms ease, background 120ms ease, color 120ms ease;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
will-change: transform;
|
||||
transition: border-color 120ms ease, box-shadow 120ms ease, background 120ms ease, color 120ms ease;
|
||||
}
|
||||
|
||||
.nodedc-toolbar-wrap[data-placement="left"] .nodedc-toolbar__button {
|
||||
transform: translateY(var(--nodedc-toolbar-shift, 0px)) scale(var(--nodedc-toolbar-scale, 1));
|
||||
transform-origin: left center;
|
||||
}
|
||||
|
||||
.nodedc-toolbar-wrap[data-placement="right"] .nodedc-toolbar__button {
|
||||
transform: translateY(var(--nodedc-toolbar-shift, 0px)) scale(var(--nodedc-toolbar-scale, 1));
|
||||
transform-origin: right center;
|
||||
}
|
||||
|
||||
.nodedc-toolbar-wrap[data-placement="bottom"] .nodedc-toolbar__button {
|
||||
transform: translateX(var(--nodedc-toolbar-shift, 0px)) scale(var(--nodedc-toolbar-scale, 1));
|
||||
transform-origin: center bottom;
|
||||
}
|
||||
|
||||
.nodedc-toolbar__button:hover,
|
||||
.nodedc-toolbar__button:focus-visible {
|
||||
border-color: var(--nodedc-toolbar-outline);
|
||||
box-shadow: 0 10px 20px color-mix(in srgb, var(--nodedc-toolbar-outline) 30%, transparent);
|
||||
}
|
||||
|
||||
.nodedc-toolbar__button[data-active="true"] {
|
||||
@@ -2179,6 +2228,12 @@ textarea.nodedc-field__control {
|
||||
color: rgb(var(--nodedc-on-accent-rgb));
|
||||
}
|
||||
|
||||
.nodedc-toolbar__button:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.45;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.nodedc-window.nodedc-share-access-window {
|
||||
width: min(35rem, calc(100vw - 2rem));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user