diff --git a/dank-qml-common b/dank-qml-common index ea058153e..5a0a6b1e6 160000 --- a/dank-qml-common +++ b/dank-qml-common @@ -1 +1 @@ -Subproject commit ea058153e7362c13585ec3af72f19b666dfffdc0 +Subproject commit 5a0a6b1e6049bee316e5055d7fa6d9bb255e102e diff --git a/flake.lock b/flake.lock index 3c1c45e68..a046891b0 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "dank-qml-common": { "flake": false, "locked": { - "lastModified": 1791820510, - "narHash": "sha256-aDpJTeAE89k0zN584vponqj99QgiHK9gNoTZo3qiLWM=", + "lastModified": 1791861042, + "narHash": "sha256-Wop1FPbCObjcDAHjo2fWTVBJGKPihhcohN8YTM3Jw5Y=", "owner": "AvengeMedia", "repo": "dank-qml-common", - "rev": "ea058153e7362c13585ec3af72f19b666dfffdc0", + "rev": "5a0a6b1e6049bee316e5055d7fa6d9bb255e102e", "type": "github" }, "original": { diff --git a/quickshell/Common/Anims.qml b/quickshell/Common/Anims.qml index a62ddffd8..d41b6c55b 100644 --- a/quickshell/Common/Anims.qml +++ b/quickshell/Common/Anims.qml @@ -31,12 +31,4 @@ Singleton { readonly property var expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1] readonly property var expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1] readonly property var expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1] - - // Expressive spatial spring presets ([stiffness, damping], unit mass) tuned to a - // 500ms reference transition; runtime values scale via Theme.springPreset(). - readonly property var spatialSprings: { - "expressive": [560, 37], - "fast": [220, 23], - "default": [100, 16] - } } diff --git a/quickshell/Modals/DLauncherV2/Controller.qml b/quickshell/Modals/DLauncherV2/Controller.qml index 704e3823e..bca7372c7 100644 --- a/quickshell/Modals/DLauncherV2/Controller.qml +++ b/quickshell/Modals/DLauncherV2/Controller.qml @@ -27,6 +27,7 @@ Item { property var collapsedSections: ({}) property bool keyboardNavigationActive: false property bool active: false + property bool retainResultsOnClose: false property var _modeSectionsCache: ({}) property bool _queryDrivenSearch: false property bool _diskCacheConsumed: false @@ -183,9 +184,14 @@ Item { } else { SessionData.addLauncherHistory(searchQuery, explicitQuerySession); } + _cancelSearch(); + selectedFlatIndex = -1; + selectedItem = null; + keyboardNavigationActive = false; + if (retainResultsOnClose && searchMode !== "dmenu") + return; sections = []; flatModel = []; - selectedItem = null; _clearModeCache(); } @@ -247,9 +253,9 @@ Item { readonly property int searchCacheVersion: AppSearchService.cacheVersion onSearchCacheVersionChanged: { + _clearModeCache(); if (!active) return; - _clearModeCache(); if (searchMode === "apps") { _loadAppCategories(); performSearch(); @@ -542,6 +548,19 @@ Item { property bool _searchPending: false + function _cancelSearch() { + _searchVersion++; + searchDebounce.stop(); + pluginPhaseTimer.stop(); + fileSearchDebounce.stop(); + _searchPending = false; + _pluginPhasePending = false; + _pluginPhaseForceFirst = false; + _phase1Items = []; + isSearching = false; + isFileSearching = false; + } + // Leading-edge debounce: search immediately when idle, coalesce bursts function requestSearch() { if (searchDebounce.running) { @@ -673,11 +692,11 @@ Item { } function reset() { + _cancelSearch(); searchQuery = ""; searchMode = "all"; previousSearchMode = "all"; autoSwitchedToFiles = false; - isFileSearching = false; fileSearchType = "all"; fileSearchExt = ""; fileSearchFolder = ""; @@ -686,7 +705,6 @@ Item { flatModel = []; selectedFlatIndex = 0; selectedItem = null; - isSearching = false; activePluginId = ""; activePluginName = ""; activePluginCategories = []; @@ -697,12 +715,10 @@ Item { collapsedSections = {}; _clearModeCache(); _queryDrivenSearch = false; - _pluginPhasePending = false; - _pluginPhaseForceFirst = false; - _phase1Items = []; } function openSession(query, explicitQuery, mode, searchWhenEmpty) { + _cancelSearch(); if (mode && mode.indexOf("dmenu:") === 0) { _startDmenuSession(mode.slice("dmenu:".length)); return; @@ -716,7 +732,6 @@ Item { historyIndex = -1; previousSearchMode = "all"; autoSwitchedToFiles = false; - isFileSearching = false; fileSearchType = SessionData.launcherLastFileSearchType || "all"; fileSearchExt = ""; fileSearchFolder = ""; @@ -729,9 +744,6 @@ Item { selectedFlatIndex = 0; selectedItem = null; _queryDrivenSearch = false; - _pluginPhasePending = false; - _pluginPhaseForceFirst = false; - _phase1Items = []; searchMode = mode; if (query) { setSearchQuery(query); @@ -1528,7 +1540,7 @@ Item { function _performPluginPhase() { _pluginPhasePending = false; - if (searchMode !== "all") + if (!active || searchMode !== "all") return; if (searchQuery && searchQuery.length < 2) return; @@ -1671,7 +1683,7 @@ Item { } function performFileSearch() { - if (!DSearchService.dsearchAvailable) + if (!active || !DSearchService.dsearchAvailable) return; var fileQuery = ""; var effectiveType = fileSearchType || "all"; @@ -1706,6 +1718,8 @@ Item { var queryTypes = splitBothTypes ? ["file", "dir"] : [effectiveType]; var pending = queryTypes.length; var aggregatedItems = []; + const currentVersion = _searchVersion; + const currentMode = searchMode; for (var t = 0; t < queryTypes.length; t++) { var queryType = queryTypes[t]; @@ -1727,6 +1741,8 @@ Item { } DSearchService.search(fileQuery, params, function (response) { + if (!root.active || currentVersion !== root._searchVersion || currentMode !== root.searchMode) + return; pending--; if (!response.error) { var hits = response.result?.hits || []; diff --git a/quickshell/Modals/DLauncherV2/ResultsList.qml b/quickshell/Modals/DLauncherV2/ResultsList.qml index acfc176ad..dff222121 100644 --- a/quickshell/Modals/DLauncherV2/ResultsList.qml +++ b/quickshell/Modals/DLauncherV2/ResultsList.qml @@ -492,49 +492,6 @@ Item { } } - Rectangle { - id: bottomShadow - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.bottomMargin: bottomSectionHeader.visible ? bottomSectionHeader.height : 0 - height: Theme.spacingXL - z: 100 - visible: { - if (BlurService.enabled) - return false; - if (mainListView.contentHeight <= mainListView.height) - return false; - var atBottom = mainListView.contentY >= mainListView.contentHeight - mainListView.height + mainListView.originY - Theme.spacingXS; - if (atBottom) - return false; - - var flatModel = root.controller?.flatModel; - if (!flatModel || flatModel.length === 0) - return false; - var lastItemIdx = -1; - for (var i = flatModel.length - 1; i >= 0; i--) { - if (!flatModel[i].isHeader) { - lastItemIdx = i; - break; - } - } - if (lastItemIdx >= 0 && root.controller?.selectedFlatIndex === lastItemIdx) - return false; - return true; - } - gradient: Gradient { - GradientStop { - position: 0.0 - color: "transparent" - } - GradientStop { - position: 1.0 - color: Theme.readableSurface - } - } - } - Rectangle { id: stickyHeader anchors.left: parent.left diff --git a/quickshell/Modals/DLauncherV2/SpotlightLauncherContent.qml b/quickshell/Modals/DLauncherV2/SpotlightLauncherContent.qml index 50c583392..7b7543e98 100644 --- a/quickshell/Modals/DLauncherV2/SpotlightLauncherContent.qml +++ b/quickshell/Modals/DLauncherV2/SpotlightLauncherContent.qml @@ -22,6 +22,8 @@ FocusScope { property bool showResultsWithoutQuery: false property bool suspendSearchUpdates: false property real maxResultsHeight: 0 + // Off for fixed-height hosts, where the resize would only slide the bottom fade. + property bool resizeAnimated: true property real resultsInset: LauncherMetrics.spotlightInset property bool editMode: false @@ -447,7 +449,7 @@ FocusScope { visible: !root.editMode Behavior on height { - enabled: root._animateResize + enabled: root.resizeAnimated && root._animateResize NumberAnimation { duration: root._resizeDuration easing.type: Easing.BezierSpline diff --git a/quickshell/Modules/DIsland/Activities/LauncherExpanded.qml b/quickshell/Modules/DIsland/Activities/LauncherExpanded.qml index fb67101c0..4a7b2449a 100644 --- a/quickshell/Modules/DIsland/Activities/LauncherExpanded.qml +++ b/quickshell/Modules/DIsland/Activities/LauncherExpanded.qml @@ -25,18 +25,22 @@ FocusScope { } function initializeSession() { - const targetQuery = root.controller.launcherPendingQuery || (SettingsData.rememberLastQuery ? (SessionData.launcherLastQuery || "") : ""); + const pendingQuery = root.controller.launcherPendingQuery; + const targetQuery = pendingQuery || (SettingsData.rememberLastQuery ? (SessionData.launcherLastQuery || "") : ""); const targetMode = root.controller.launcherPendingMode || SessionData.getLauncherRestoreMode(); + // Used once: the face stays loaded between opens. + root.controller.launcherPendingQuery = ""; + root.controller.launcherPendingMode = ""; launcherContent.closeTransientUi?.(); launcherContent.suspendSearchUpdates = true; launcherContent.searchField.text = targetQuery; launcherContent.suspendSearchUpdates = false; - launcherController.openSession(targetQuery, !!root.controller.launcherPendingQuery, targetMode, true); + launcherController.openSession(targetQuery, !!pendingQuery, targetMode, true); launcherContent.resetScroll(); root.focusFace(); - if (!root.controller.launcherPendingQuery) { + if (!pendingQuery) { launcherContent.searchField.selectAll(); return; } @@ -73,6 +77,7 @@ FocusScope { controllerOverride: root.launcherController transientSurfaceTracker: root.transientSurfaceTracker showResultsWithoutQuery: true + resizeAnimated: false maxResultsHeight: Math.max(120, root.controller.launcherExpandedTarget.height - Theme.spacingM - root.bottomInset - launcherContent.searchAreaHeight - launcherContent.actionPanelHeight) } @@ -81,9 +86,10 @@ FocusScope { Connections { target: root.controller + // Synchronous so the results are ready before the first morph frame. function onSessionStarted(activityId) { if (activityId === "launcher") - Qt.callLater(root.initializeSession); + root.initializeSession(); } } diff --git a/quickshell/Modules/DIsland/DIslandSurface.qml b/quickshell/Modules/DIsland/DIslandSurface.qml index 4489aed20..2602af2a1 100644 --- a/quickshell/Modules/DIsland/DIslandSurface.qml +++ b/quickshell/Modules/DIsland/DIslandSurface.qml @@ -422,6 +422,7 @@ Item { id: contentHost controller: root.controller + surfaceColor: root.effectiveSurfaceColor freeMode: root.freeMode compactFaceOverride: root.compactFaceOverride resolveTarget: target => root.resolveTarget(target) diff --git a/quickshell/Modules/DIsland/IslandBarHost.qml b/quickshell/Modules/DIsland/IslandBarHost.qml index 73ccd5ca1..79cc75cd5 100644 --- a/quickshell/Modules/DIsland/IslandBarHost.qml +++ b/quickshell/Modules/DIsland/IslandBarHost.qml @@ -445,6 +445,7 @@ Item { id: launcherController active: controller.launcherSessionActive + retainResultsOnClose: true viewModeContext: "spotlight" forceLinearNavigation: true } diff --git a/quickshell/Modules/DIsland/IslandContentHost.qml b/quickshell/Modules/DIsland/IslandContentHost.qml index ccbaf4868..9f30e3c89 100644 --- a/quickshell/Modules/DIsland/IslandContentHost.qml +++ b/quickshell/Modules/DIsland/IslandContentHost.qml @@ -22,6 +22,8 @@ Item { required property bool expanded required property bool pointerInside required property string activityId + // The colour painted behind the faces, so edge fades blend into it. + required property color surfaceColor property bool freeMode: false property Component compactFaceOverride: null // Hosted islands fold the slot anchor into every target; the face must pin to the same resolved slot. @@ -58,12 +60,10 @@ Item { readonly property bool mediaSurfaceActive: root.surfaceActive("media") readonly property bool systemSurfaceActive: root.controller.systemActivities.some(id => root.surfaceActive(id)) - function systemCompactOpacity() { - return Math.max(0, ...root.controller.systemActivities.map(id => root.compactOpacity(id))); - } - - function systemExpandedOpacity() { - return Math.max(0, ...root.controller.systemActivities.map(id => root.expandedOpacity(id))); + function systemOpacity(opacityFor) { + const incoming = root.controller.isSystemActivity(root.renderedActivity) ? opacityFor(root.renderedActivity) : 0; + const outgoing = root.controller.isSystemActivity(root.outgoingActivity) ? opacityFor(root.outgoingActivity) : 0; + return Math.max(incoming, outgoing); } property string renderedActivity: "home" @@ -174,14 +174,25 @@ Item { } } + // Where a resolved target sits at rest, in host coordinates. + function slotAlong(target) { + return root.controller.isVertical ? (root.hostHeight - target.height) / 2 + target.offsetAlong : (root.hostWidth - target.width) / 2 + target.offsetAlong; + } + + function slotCross(target) { + if (!root.controller.farEdge) + return target.offsetCross; + return root.controller.isVertical ? root.hostWidth - target.offsetCross - target.width : root.hostHeight - target.offsetCross - target.height; + } + // Pinned to the compact target's own screen slot so the face holds still while the island morphs. component CompactFace: Loader { required property string activity required property Component face readonly property var target: root.resolveTarget(root.controller.compactTargetFor(activity)) readonly property bool isVertical: root.controller.isVertical - readonly property real alongPos: isVertical ? Math.round((root.hostHeight - target.height) / 2 + target.offsetAlong) - Math.round(root.islandY) : Math.round((root.hostWidth - target.width) / 2 + target.offsetAlong) - Math.round(root.islandX) - readonly property real crossPos: isVertical ? Math.round((parent.width - width) / 2) : Math.round((parent.height - height) / 2) + readonly property real alongPos: !active ? 0 : Math.round(root.slotAlong(target)) - Math.round(isVertical ? root.islandY : root.islandX) + readonly property real crossPos: !active ? 0 : Math.round(root.slotCross(target)) - Math.round(isVertical ? root.islandX : root.islandY) x: root.freeMode ? Math.round((parent.width - width) / 2) : (isVertical ? crossPos : alongPos) y: root.freeMode ? Math.round((parent.height - height) / 2) : (isVertical ? alongPos : crossPos) @@ -189,19 +200,26 @@ Item { height: target.height asynchronous: false sourceComponent: root.compactFaceOverride && root.controller.usesDotFace(activity) ? root.compactFaceOverride : face + opacity: !active ? 0 : root.controller.isSystemActivity(activity) ? root.systemOpacity(root.compactOpacity) : root.compactOpacity(activity) visible: opacity > 0.001 enabled: opacity >= 0.5 } + // Pinned to the sheet's settled slot so the content holds still while the island morphs. component ExpandedFace: Loader { required property string activity readonly property var target: root.controller.expandedTargetFor(activity) + readonly property var slot: root.resolveTarget(target) readonly property bool isVertical: root.controller.isVertical + readonly property real inset: root.controller.farEdge ? 0 : root.expandedInset + readonly property real alongPos: !active ? 0 : Math.round(root.slotAlong(slot)) - Math.round(isVertical ? root.islandY : root.islandX) + readonly property real crossPos: !active ? 0 : Math.round(root.slotCross(slot)) - Math.round(isVertical ? root.islandX : root.islandY) + inset - x: isVertical && !root.controller.farEdge ? root.expandedInset : 0 - y: !isVertical && !root.controller.farEdge ? root.expandedInset : 0 + x: isVertical ? crossPos : alongPos + y: isVertical ? alongPos : crossPos width: target.width height: target.height + opacity: !active ? 0 : root.controller.isSystemActivity(activity) ? root.systemOpacity(root.expandedOpacity) : root.expandedOpacity(activity) visible: opacity > 0.001 enabled: opacity >= 0.5 } @@ -210,7 +228,6 @@ Item { active: true activity: "home" face: root.homeCompactComponent - opacity: root.compactOpacity("home") } ExpandedFace { @@ -220,14 +237,12 @@ Item { active: root.homeExpandedTouched asynchronous: true sourceComponent: root.homeExpandedComponent - opacity: root.expandedOpacity("home") } CompactFace { active: root.mediaSurfaceActive activity: "media" face: root.mediaCompactComponent - opacity: root.compactOpacity("media") } ExpandedFace { @@ -237,14 +252,12 @@ Item { active: root.mediaSurfaceActive && (root.expanded || root.expandedFade > 0) asynchronous: false sourceComponent: root.mediaExpandedComponent - opacity: root.expandedOpacity("media") } CompactFace { active: root.surfaceActive("launcher") activity: "launcher" face: root.launcherCompactComponent - opacity: root.compactOpacity("launcher") } ExpandedFace { @@ -254,7 +267,6 @@ Item { active: root.controller.visualsRequested("launcher") asynchronous: true sourceComponent: root.launcherExpandedComponent - opacity: root.expandedOpacity("launcher") onEnabledChanged: { if (enabled) root.refocusLauncher(); @@ -269,7 +281,6 @@ Item { active: root.surfaceActive("controlcenter") activity: "controlcenter" face: root.controlCenterCompactComponent - opacity: root.compactOpacity("controlcenter") } ExpandedFace { @@ -278,14 +289,12 @@ Item { active: root.controller.visualsRequested("controlcenter") asynchronous: false sourceComponent: root.controlCenterExpandedComponent - opacity: root.expandedOpacity("controlcenter") } CompactFace { active: root.surfaceActive("wallpaper") activity: "wallpaper" face: root.wallpaperCompactComponent - opacity: root.compactOpacity("wallpaper") } ExpandedFace { @@ -295,14 +304,12 @@ Item { active: root.controller.visualsRequested("wallpaper") asynchronous: true sourceComponent: root.wallpaperExpandedComponent - opacity: root.expandedOpacity("wallpaper") } CompactFace { active: root.surfaceActive("weather") activity: "weather" face: root.weatherCompactComponent - opacity: root.compactOpacity("weather") } ExpandedFace { @@ -312,14 +319,12 @@ Item { active: root.controller.visualsRequested("weather") asynchronous: true sourceComponent: root.weatherExpandedComponent - opacity: root.expandedOpacity("weather") } CompactFace { active: root.surfaceActive("notificationcenter") activity: "notificationcenter" face: root.notificationCenterCompactComponent - opacity: root.compactOpacity("notificationcenter") } ExpandedFace { @@ -329,14 +334,12 @@ Item { active: root.controller.visualsRequested("notificationcenter") asynchronous: true sourceComponent: root.notificationCenterExpandedComponent - opacity: root.expandedOpacity("notificationcenter") } CompactFace { active: root.surfaceActive("clipboard") activity: "clipboard" face: root.clipboardCompactComponent - opacity: root.compactOpacity("clipboard") } ExpandedFace { @@ -346,14 +349,12 @@ Item { active: root.controller.visualsRequested("clipboard") asynchronous: true sourceComponent: root.clipboardExpandedComponent - opacity: root.expandedOpacity("clipboard") } CompactFace { active: root.surfaceActive("powermenu") activity: "powermenu" face: root.powerMenuCompactComponent - opacity: root.compactOpacity("powermenu") } ExpandedFace { @@ -363,14 +364,12 @@ Item { active: root.controller.visualsRequested("powermenu") asynchronous: true sourceComponent: root.powerMenuExpandedComponent - opacity: root.expandedOpacity("powermenu") } CompactFace { active: root.systemSurfaceActive activity: "volume" face: root.systemCompactComponent - opacity: root.systemCompactOpacity() } ExpandedFace { @@ -378,14 +377,12 @@ Item { active: root.systemSurfaceActive && (root.expanded || root.expandedFade > 0) asynchronous: false sourceComponent: root.systemExpandedComponent - opacity: root.systemExpandedOpacity() } CompactFace { active: root.surfaceActive("notification") activity: "notification" face: root.notificationCompactComponent - opacity: root.compactOpacity("notification") } ExpandedFace { @@ -393,6 +390,5 @@ Item { active: root.surfaceActive("notification") && (root.expanded || root.expandedFade > 0) asynchronous: false sourceComponent: root.notificationExpandedComponent - opacity: root.expandedOpacity("notification") } } diff --git a/quickshell/Modules/DIsland/IslandController.qml b/quickshell/Modules/DIsland/IslandController.qml index 20c830a31..e968c279f 100644 --- a/quickshell/Modules/DIsland/IslandController.qml +++ b/quickshell/Modules/DIsland/IslandController.qml @@ -324,12 +324,17 @@ QtObject { destinationRevision++; } + // The launcher stays loaded once built, so reopening never waits on a rebuild. + function keepsVisuals(activityId) { + return activityId === "launcher" && destinationState[activityId].ready; + } + function releaseIdleVisuals() { for (const id of destinations) { if (expanded && activeActivity === id) continue; const entry = destinationState[id]; - if (!entry.requested && !entry.ready) + if (root.keepsVisuals(id) || (!entry.requested && !entry.ready)) continue; visualsReleaseTimer.restart(); return; @@ -341,7 +346,7 @@ QtObject { for (const id of destinations) { if (expanded && activeActivity === id) continue; - if (destinationState[id].pending) + if (destinationState[id].pending || root.keepsVisuals(id)) continue; dropVisuals(id); } @@ -753,11 +758,21 @@ QtObject { } function requestLauncher(query, mode, shouldToggle) { - if (shouldToggle === true && activeActivity === "launcher" && expanded) + const open = activeActivity === "launcher" && expanded; + if (shouldToggle === true && open) return requestCollapse(); launcherPendingQuery = query || ""; launcherPendingMode = mode || ""; - return requestActivity("launcher", true, true); + const handled = requestActivity("launcher", true, true); + // An open launcher applies a new query or mode in place. + if (handled && open && (launcherPendingQuery || launcherPendingMode)) + sessionStarted("launcher"); + // Only a launcher that is still loading reads these later. + if (!destinationState.launcher.pending) { + launcherPendingQuery = ""; + launcherPendingMode = ""; + } + return handled; } function requestControlCenter(section, shouldToggle) { diff --git a/quickshell/Modules/SurfaceWidgets/VectorSpringMotion.qml b/quickshell/Modules/SurfaceWidgets/VectorSpringMotion.qml index 756e0ef9c..4b26c5291 100644 --- a/quickshell/Modules/SurfaceWidgets/VectorSpringMotion.qml +++ b/quickshell/Modules/SurfaceWidgets/VectorSpringMotion.qml @@ -148,27 +148,37 @@ QtObject { const steps = Math.max(1, Math.ceil(frameTime / integrationStep)); const step = frameTime / steps; const inverseMass = 1 / Math.max(0.001, mass); + const k = stiffness; + const c = damping; + const target = [targetWidth, targetHeight, targetOffsetAlong, targetOffsetCross, targetTopLeftRadius, targetTopRightRadius, targetBottomLeftRadius, targetBottomRightRadius]; + const current = [currentWidth, currentHeight, currentOffsetAlong, currentOffsetCross, currentTopLeftRadius, currentTopRightRadius, currentBottomLeftRadius, currentBottomRightRadius]; + const velocity = [velocityWidth, velocityHeight, velocityOffsetAlong, velocityOffsetCross, velocityTopLeftRadius, velocityTopRightRadius, velocityBottomLeftRadius, velocityBottomRightRadius]; + // Integrate in locals and write once per frame; every property write re-runs its bindings. for (let i = 0; i < steps; i++) { - velocityWidth += (stiffness * (targetWidth - currentWidth) - damping * velocityWidth) * inverseMass * step; - velocityHeight += (stiffness * (targetHeight - currentHeight) - damping * velocityHeight) * inverseMass * step; - velocityOffsetAlong += (stiffness * (targetOffsetAlong - currentOffsetAlong) - damping * velocityOffsetAlong) * inverseMass * step; - velocityOffsetCross += (stiffness * (targetOffsetCross - currentOffsetCross) - damping * velocityOffsetCross) * inverseMass * step; - velocityTopLeftRadius += (stiffness * (targetTopLeftRadius - currentTopLeftRadius) - damping * velocityTopLeftRadius) * inverseMass * step; - velocityTopRightRadius += (stiffness * (targetTopRightRadius - currentTopRightRadius) - damping * velocityTopRightRadius) * inverseMass * step; - velocityBottomLeftRadius += (stiffness * (targetBottomLeftRadius - currentBottomLeftRadius) - damping * velocityBottomLeftRadius) * inverseMass * step; - velocityBottomRightRadius += (stiffness * (targetBottomRightRadius - currentBottomRightRadius) - damping * velocityBottomRightRadius) * inverseMass * step; - - currentWidth += velocityWidth * step; - currentHeight += velocityHeight * step; - currentOffsetAlong += velocityOffsetAlong * step; - currentOffsetCross += velocityOffsetCross * step; - currentTopLeftRadius += velocityTopLeftRadius * step; - currentTopRightRadius += velocityTopRightRadius * step; - currentBottomLeftRadius += velocityBottomLeftRadius * step; - currentBottomRightRadius += velocityBottomRightRadius * step; + for (let j = 0; j < 8; j++) { + velocity[j] += (k * (target[j] - current[j]) - c * velocity[j]) * inverseMass * step; + current[j] += velocity[j] * step; + } } + velocityWidth = velocity[0]; + velocityHeight = velocity[1]; + velocityOffsetAlong = velocity[2]; + velocityOffsetCross = velocity[3]; + velocityTopLeftRadius = velocity[4]; + velocityTopRightRadius = velocity[5]; + velocityBottomLeftRadius = velocity[6]; + velocityBottomRightRadius = velocity[7]; + currentWidth = current[0]; + currentHeight = current[1]; + currentOffsetAlong = current[2]; + currentOffsetCross = current[3]; + currentTopLeftRadius = current[4]; + currentTopRightRadius = current[5]; + currentBottomLeftRadius = current[6]; + currentBottomRightRadius = current[7]; + if (isSettled()) settle(); else