@andynoob/move-it
v1.0.0
Published
DOM rectangle transform controls
Readme
Move it!
A library that adds simple PowerPoint like DOM object manipulation: resize, drag, and rotate. Pretty lightweight and comes with (very beautifully) styled controls. This library also provides the means for you to calculate collision via an implementation of oriented bounding boxes.
Installing
Run npm i @andynoob/move-it and read on.
The lifecycle
Fully functional sample code can be found here.
Please ensure that the moving element is absolutely positioned, relative to the control root. Otherwise, expect the transform controls to be mis-fit around the moving element.
To start, create an instance of Moving by calling createMoveMe (or MoveIt.createMoveMe). For example:
const el = document.querySelector(/* ... */);
const controlRoot = el.parentElement!;
const snapping = {
// omitted... we'll get to this later
};
const moving = createMoveMe(el, {
initialState: { // optional, the code will call computeState on the target (first parameter) if this is absent
x: 200,
y: 200,
width: 200,
height: 120,
rotation: 75,
},
format: {
// when asPercent = true, the x, y, width, and height values is in
// percentages (in decimal) relative to the control root
asPercent: false,
// when centered = true, the RectState represents the pivot of the element (which can be changed
// by `pivotOffset`). this can be used with usePercent. when this is paired with `autoSize`,
// the library will keep the element centered on the pivot point whenever resizing happens
centered: false
},
snapping, // optional
controlRoot, // required, sets the bounds for the object
// when autoSize = true, the library stops assigning width and height directly via CSS, but rather syncs the
// `RectState` automatically whenever the size changes via DOM `ResizeObserver`.
// this also implicitly disables the resize feature.
autoSize: false,
// percent (expressed as decimal) of each axis to offset the pivot point of the moving element.
// this will impact the rotation pivot, the grid snapping location, and `autoSize` if enabled. the default pivot
// point is the center of the element. to make it top left, do `{x: -0.5, y: -0.5}`. this will affect `centered` states
pivotOffset: {x: 0, y: 0}
});If
initialStateis not present as a part of the option parameter,computeStatewill be called to calculate aRectStatefrom the target element (width, rotation, etc.).
Calling the createMoveMe function will return an instance of the Moving interface:
interface Moving {
element: HTMLElement,
id: string,
/**
* @description a copy of the current `RectState`. in the format option provided in the initial options
*/
getState: () => RectState,
/**
* @description partial in the format option provided in the options
*/
updateState: (partial: Partial<RectState>) => void,
destroy: () => void,
render: () => void,
select: () => void,
isSelected: () => boolean,
checkBounds: () => void,
updateControls: (select: boolean) => Controls,
getCollisionSiblings: () => Moving[],
/**
* You need to do this for both instances, the behavior is not mirrored by default
* For example, say you have `instanceA` and `instanceB`, you need to run both
* `instanceA.addCollisionSibling(instanceB)` and `instanceB.addCollisionSibling(instanceA)`
* for both instances to collide with the other.
*/
addCollisionSibling: (sibling: Moving) => void,
removeCollisionSibling: (sibling: Moving) => void,
getOptions: () => MoveMeOpt
}Call destroy when you're done moving the object.
The transform controls
The transform control is added immediately when you call the createMoveMe. As seen in the GIF below, it consists of five lines and five dots.
The CSS for these controls can be found here (the CONTROL_ID is E4UKgq3cxN, contrary to its name, it's actually a class). The style is injected into the <head> tag (if not present), under a <style> tag with id mGW3wTwrZ6. The cursor CSS property is updated accordingly to the rotation of the rectangle. Every moving element is given the move cursor.
The transformations
- Dragging the rectangle itself will move the rectangle.
- Dragging the lone dot protruding from the right side of the rectangle will start rotating.
- Dragging the sides of the rectangle will scale the rectangle in that direction only. Whereas the dots on the corners allow free transform. The user may hold shift to keep ratio when free transforming.
Snapping
There are two types of snapping behaviors: rotation snapping, and guideline based snapping.
| Rotation Snap | Guideline Snap | |-------------------------------------------------------------------------------|---------------------------------------------------------------------------------| | | |
When
doResizeis true, the grid x & y are percentages (0-1)
The SnappingOpt interface is defined as follows:
export interface SnappingOpt {
rotation?: SnappingRotation,
grid?: SnappingGrid
}
export interface SnappingGrid {
/**
* number of pixels away to snap the element
*/
threshold: number,
/**
* number of pixels away to display the nearest grid/guideline
*/
displayThreshold: number,
verticalX?: number[], // relative to the control root
horizontalY?: number[] // also relative to the control root
}
export interface SnappingRotation {
anglesDeg: number[],
threshold: number // degrees also
}Simply provide that in the option parameter of createMoveMe, like so:
const moving = createMoveMe(el, {
snapping: {
rotation: {
anglesDeg: [0, 180],
threshold: 5
},
grid: {
displayThreshold: 20,
threshold: 5,
verticalX: [controlRoot.offsetWidth / 2],
horizontalY: [controlRoot.offsetHeight / 10 * 8]
}
},
controlRoot
});The snapping behavior can be disabled by the user if they hold shift while dragging/rotating the element.
Finding overlap
The Moving interface allows you to add another Moving instance as a collision sibling. When you do so, the current instance will collide with the other instance. You must do the same on the other instance for both collisions to be enabled.
You may manually trigger collision resolution by calling Moving.checkBounds. The underlying function that powers this feature is findOverlap. It takes two RectState parameters. You may check a moving element against a non-moving element by calling computeState on the non-moving element to obtain a RectState.
Auto size
Enabling this feature disables resizing.

When enabled in MoveItOpt, the library will sync the width and height of the RectSate to the actual computed width and height of the moving element. Additionally, whenever the moving element resizes, the library will shift the element such that it's center remains anchored in the same location. You should store the RectState of such moving elements as centered=true (i.e. moving.getState(..., true)).
Credits
This project was made with the help of generative AI (GPT-5.3/5.5 & Claude Sonnet & Gemini & Deepseek). The tests are currently mostly generated by AI. The getDistanceToLine was mostly generated by Gemini.
Additionally, code snippets were taken from StackOverflow posts and Mozilla.
