import { Renderer, SnappableProps, SnappableState, SnapGuideline, ScalableProps, SnapPosInfo, RotatableProps, RectInfo, MoveableManagerInterface, SnappableRenderType, BoundType, MoveableGroupInterface, SnapDirectionInfo, } from "../types"; import { prefix, calculatePoses, getRect, getAbsolutePosesByState, getAbsolutePoses, getClientRect, getRefTarget, getDragDistByState, triggerEvent, getDirectionCondition, abs, watchValue, } from "../utils"; import { find, findIndex, hasClass, throttle, } from "@daybrush/utils"; import { getDragDist, scaleMatrix, getPosByDirection, } from "../gesto/GestoUtils"; import { minus, rotate, plus } from "@scena/matrix"; import { dragControlCondition as rotatableDragControlCondtion } from "./Rotatable"; import { FLOAT_POINT_NUM } from "../consts"; import { getInnerBoundInfo, getCheckInnerBoundLineInfos, checkRotateInnerBounds, checkInnerBoundPoses, } from "./snappable/innerBounds"; import { checkBoundPoses, checkRotateBounds, getBounds, } from "./snappable/bounds"; import { checkSnaps, getSnapInfosByDirection, getNearOffsetInfo, getCheckSnapDirections, } from "./snappable/snap"; import { renderSnapPoses, renderGuidelines, renderDashedGuidelines, renderGapGuidelines, } from "./snappable/render"; import { getInitialBounds, hasGuidelines, } from "./snappable/utils"; import { checkMaxBounds, checkMoveableSnapBounds, getSnapBoundInfo, } from "./snappable/snapBounds"; import { getTotalGuidelines } from "./snappable/getTotalGuidelines"; export interface SnapPoses { vertical: number[]; horizontal: number[]; } export function checkSnapInfo( moveable: MoveableManagerInterface ) { const state = moveable.state; const container = state.container; const snapContainer = moveable.props.snapContainer || container!; if (state.snapContainer === snapContainer && state.guidelines && state.guidelines.length) { return false; } const containerClientRect = state.containerClientRect; const snapOffset = { left: 0, top: 0, bottom: 0, right: 0, }; if (container !== snapContainer) { const snapContainerTarget = getRefTarget(snapContainer, true); if (snapContainerTarget) { const snapContainerRect = getClientRect(snapContainerTarget); const offset1 = getDragDistByState(state, [ snapContainerRect.left - containerClientRect.left, snapContainerRect.top - containerClientRect.top, ]); const offset2 = getDragDistByState(state, [ snapContainerRect.right - containerClientRect.right, snapContainerRect.bottom - containerClientRect.bottom, ]); snapOffset.left = throttle(offset1[0], 0.00001); snapOffset.top = throttle(offset1[1], 0.00001); snapOffset.right = throttle(offset2[0], 0.00001); snapOffset.bottom = throttle(offset2[1], 0.00001); } } state.snapContainer = snapContainer; state.snapOffset = snapOffset; state.guidelines = getTotalGuidelines(moveable); state.enableSnap = true; return true; } function getNextFixedPoses( matrix: number[], width: number, height: number, fixedDirection: number[], fixedPos: number[], is3d: boolean ) { const nextPoses = calculatePoses(matrix, width, height, is3d ? 4 : 3); const nextFixedPos = getPosByDirection(nextPoses, fixedDirection); return getAbsolutePoses(nextPoses, minus(fixedPos, nextFixedPos)); } export function normalized(value: number) { return value ? value / abs(value) : 0; } export function getSizeOffsetInfo( moveable: MoveableManagerInterface, poses: number[][], direction: number[], keepRatio: boolean, isRequest: boolean, datas: any ) { const { fixedDirection } = datas; const directions = getCheckSnapDirections(direction, fixedDirection, keepRatio); const innerBoundLineInfos = getCheckInnerBoundLineInfos(moveable, poses, direction, keepRatio); const offsets = [ ...getSnapBoundInfo( moveable, poses, directions, keepRatio, isRequest, datas ), ...getInnerBoundInfo( moveable, innerBoundLineInfos, datas ), ]; const widthOffsetInfo = getNearOffsetInfo(offsets, 0); const heightOffsetInfo = getNearOffsetInfo(offsets, 1); // console.log( 'widthOffsetInfo', widthOffsetInfo, heightOffsetInfo); return { width: { isBound: widthOffsetInfo.isBound, offset: widthOffsetInfo.offset[0], }, height: { isBound: heightOffsetInfo.isBound, offset: heightOffsetInfo.offset[1], }, }; } export function recheckSizeByTwoDirection( moveable: MoveableManagerInterface, poses: number[][], width: number, height: number, maxWidth: number, maxHeight: number, direction: number[], isRequest: boolean, datas: any ) { const snapPos = getPosByDirection(poses, direction); const { horizontal: { offset: horizontalOffset }, vertical: { offset: verticalOffset }, } = checkMoveableSnapBounds(moveable, isRequest, { vertical: [snapPos[0]], horizontal: [snapPos[1]], }); if (throttle(verticalOffset, FLOAT_POINT_NUM) || throttle(horizontalOffset, FLOAT_POINT_NUM)) { const [nextWidthOffset, nextHeightOffset] = getDragDist({ datas, distX: -verticalOffset, distY: -horizontalOffset, }); const nextWidth = Math.min( maxWidth || Infinity, width + direction[0] * nextWidthOffset ); const nextHeight = Math.min( maxHeight || Infinity, height + direction[1] * nextHeightOffset ); return [nextWidth - width, nextHeight - height]; } return [0, 0]; } export function checkSizeDist( moveable: MoveableManagerInterface, getNextPoses: (widthOffset: number, heightOffset: number) => number[][], width: number, height: number, direction: number[], fixedPosition: number[], isRequest: boolean, datas: any ) { const poses = getAbsolutePosesByState(moveable.state); const keepRatio = moveable.props.keepRatio; let widthOffset = 0; let heightOffset = 0; for (let i = 0; i < 2; ++i) { const nextPoses = getNextPoses(widthOffset, heightOffset); const { width: widthOffsetInfo, height: heightOffsetInfo, } = getSizeOffsetInfo( moveable, nextPoses, direction, keepRatio, isRequest, datas ); const isWidthBound = widthOffsetInfo.isBound; const isHeightBound = heightOffsetInfo.isBound; let nextWidthOffset = widthOffsetInfo.offset; let nextHeightOffset = heightOffsetInfo.offset; if (i === 1) { if (!isWidthBound) { nextWidthOffset = 0; } if (!isHeightBound) { nextHeightOffset = 0; } } if (i === 0 && isRequest && !isWidthBound && !isHeightBound) { return [0, 0]; } if (keepRatio) { const widthDist = abs(nextWidthOffset) * (width ? 1 / width : 1); const heightDist = abs(nextHeightOffset) * (height ? 1 / height : 1); const isGetWidthOffset = isWidthBound && isHeightBound ? widthDist < heightDist : isHeightBound || (!isWidthBound && widthDist < heightDist); if (isGetWidthOffset) { // width : height = ? : heightOffset nextWidthOffset = (width * nextHeightOffset) / height; } else { // width : height = widthOffset : ? nextHeightOffset = (height * nextWidthOffset) / width; } } widthOffset += nextWidthOffset; heightOffset += nextHeightOffset; } if (!keepRatio && direction[0] && direction[1]) { const { maxWidth, maxHeight } = checkMaxBounds( moveable, poses, direction, fixedPosition, datas ); const [nextWidthOffset, nextHeightOffset] = recheckSizeByTwoDirection( moveable, getNextPoses(widthOffset, heightOffset).map(pos => pos.map(p => throttle(p, FLOAT_POINT_NUM))), width + widthOffset, height + heightOffset, maxWidth, maxHeight, direction, isRequest, datas ); widthOffset += nextWidthOffset; heightOffset += nextHeightOffset; } return [widthOffset, heightOffset]; } export function absDegree(deg: number) { if (deg < 0) { deg = deg % 360 + 360; } deg %= 360; return deg; } export function bumpDegree(baseDeg: number, snapDeg: number) { // baseDeg -80 // snapDeg 270 // return -90 snapDeg = absDegree(snapDeg); const count = Math.floor(baseDeg / 360); const deg1 = count * 360 + 360 - snapDeg; const deg2 = count * 360 + snapDeg; return abs(baseDeg - deg1) < abs(baseDeg - deg2) ? deg1 : deg2; } export function getMinDegreeDistance(deg1: number, deg2: number) { deg1 = absDegree(deg1); deg2 = absDegree(deg2); const deg3 = absDegree(deg1 - deg2); return Math.min(deg3, 360 - deg3); } export function checkSnapRotate( moveable: MoveableManagerInterface, rect: RectInfo, dist: number, rotation: number, ) { const props = moveable.props; const snapRotationThreshold = props[NAME_snapRotationThreshold] ?? 5; const snapRotationDegrees = props[NAME_snapRotationDegrees]; if (hasGuidelines(moveable, "rotatable")) { const { pos1, pos2, pos3, pos4, origin: origin2 } = rect; const rad = (dist * Math.PI) / 180; const prevPoses = [pos1, pos2, pos3, pos4].map((pos) => minus(pos, origin2)); const nextPoses = prevPoses.map((pos) => rotate(pos, rad)); // console.log(moveable.state.left, moveable.state.top, moveable.state.origin); // console.log(pos1, pos2, pos3, pos4, origin, rad, prevPoses, nextPoses); const result = [ ...checkRotateBounds(moveable, prevPoses, nextPoses, origin2, dist), ...checkRotateInnerBounds( moveable, prevPoses, nextPoses, origin2, dist ), ]; result.sort((a, b) => abs(a - dist) - abs(b - dist)); const isSnap = result.length > 0; if (isSnap) { return { isSnap, dist: isSnap ? result[0] : dist, }; } } if (snapRotationDegrees?.length && snapRotationThreshold) { const sorted = snapRotationDegrees.slice().sort((a, b) => { return getMinDegreeDistance(a, rotation) - getMinDegreeDistance(b, rotation); }); const firstDegree = sorted[0]; if (getMinDegreeDistance(firstDegree, rotation) <= snapRotationThreshold) { return { isSnap: true, dist: dist + bumpDegree(rotation, firstDegree) - rotation, }; } } return { isSnap: false, dist, }; } export function checkSnapResize( moveable: MoveableManagerInterface<{}, {}>, width: number, height: number, direction: number[], fixedPosition: number[], isRequest: boolean, datas: any ) { if (!hasGuidelines(moveable, "resizable")) { return [0, 0]; } const { fixedDirection, nextAllMatrix } = datas; const { allMatrix, is3d } = moveable.state; return checkSizeDist( moveable, (widthOffset: number, heightOffset: number) => { return getNextFixedPoses( nextAllMatrix || allMatrix, width + widthOffset, height + heightOffset, fixedDirection, fixedPosition, is3d ); }, width, height, direction, fixedPosition, isRequest, datas ); } export function checkSnapScale( moveable: MoveableManagerInterface, scale: number[], direction: number[], isRequest: boolean, datas: any ) { if (!hasGuidelines(moveable, "scalable")) { return [0, 0]; } const { startOffsetWidth, startOffsetHeight, fixedPosition, fixedDirection, is3d } = datas; const sizeDist = checkSizeDist( moveable, (widthOffset: number, heightOffset: number) => { return getNextFixedPoses( scaleMatrix( datas, plus(scale, [widthOffset / startOffsetWidth, heightOffset / startOffsetHeight]), ), startOffsetWidth, startOffsetHeight, fixedDirection, fixedPosition, is3d ); }, startOffsetWidth, startOffsetHeight, direction, fixedPosition, isRequest, datas ); return [sizeDist[0] / startOffsetWidth, sizeDist[1] / startOffsetHeight]; } export function startCheckSnapDrag( moveable: MoveableManagerInterface, datas: any ) { datas.absolutePoses = getAbsolutePosesByState(moveable.state); } function getSnapGuidelines(posInfos: SnapPosInfo[]) { const guidelines: Array<{ guideline: SnapGuideline, posInfo: SnapPosInfo }> = []; posInfos.forEach((posInfo) => { posInfo.guidelineInfos.forEach(({ guideline }) => { if (find(guidelines, info => info.guideline === guideline)) { return; } guideline.direction = ""; guidelines.push({ guideline, posInfo }); }); }); return guidelines.map(({ guideline, posInfo }) => { return { ...guideline, direction: posInfo.direction, }; }); } function addBoundGuidelines( moveable: MoveableManagerInterface, verticalPoses: number[], horizontalPoses: number[], verticalSnapPoses: SnappableRenderType[], horizontalSnapPoses: SnappableRenderType[], externalBounds?: BoundType | false | null ) { const { vertical: verticalBoundInfos, horizontal: horizontalBoundInfos, } = checkBoundPoses( getBounds(moveable, externalBounds), verticalPoses, horizontalPoses ); const boundMap = getInitialBounds(); verticalBoundInfos.forEach((info) => { if (info.isBound) { if (info.direction === "start") { boundMap.left = true; } if (info.direction === "end") { boundMap.right = true; } verticalSnapPoses.push({ type: "bounds", pos: info.pos, }); } }); horizontalBoundInfos.forEach((info) => { if (info.isBound) { if (info.direction === "start") { boundMap.top = true; } if (info.direction === "end") { boundMap.bottom = true; } horizontalSnapPoses.push({ type: "bounds", pos: info.pos, }); } }); const { boundMap: innerBoundMap, vertical: verticalInnerBoundPoses, horizontal: horizontalInnerBoundPoses, } = checkInnerBoundPoses(moveable); verticalInnerBoundPoses.forEach((innerPos) => { if ( findIndex( verticalSnapPoses, ({ type, pos }) => type === "bounds" && pos === innerPos ) >= 0 ) { return; } verticalSnapPoses.push({ type: "bounds", pos: innerPos, }); }); horizontalInnerBoundPoses.forEach((innerPos) => { if ( findIndex( horizontalSnapPoses, ({ type, pos }) => type === "bounds" && pos === innerPos ) >= 0 ) { return; } horizontalSnapPoses.push({ type: "bounds", pos: innerPos, }); }); return { boundMap, innerBoundMap, }; } const directionCondition = getDirectionCondition("", ["resizable", "scalable"]); const NAME_snapRotationThreshold = "snapRotationThreshold"; const NAME_snapRotationDegrees = "snapRotationDegrees"; /** * @namespace Moveable.Snappable * @description Whether or not target can be snapped to the guideline. (default: false) * @sort 2 */ export default { name: "snappable", dragRelation: "strong", props: [ "snappable", "snapContainer", "snapDirections", "elementSnapDirections", "snapGap", "snapGridWidth", "snapGridHeight", "isDisplaySnapDigit", "isDisplayInnerSnapDigit", "isDisplayGridGuidelines", "snapDigit", "snapThreshold", "snapRenderThreshold", "snapGridAll", NAME_snapRotationThreshold, NAME_snapRotationDegrees, "horizontalGuidelines", "verticalGuidelines", "elementGuidelines", "bounds", "innerBounds", "snapDistFormat", "maxSnapElementGuidelineDistance", "maxSnapElementGapDistance", ] as const, events: ["snap", "bound"] as const, css: [ `:host { --bounds-color: #d66; } .guideline { pointer-events: none; z-index: 2; } .guideline.bounds { background: #d66; background: var(--bounds-color); } .guideline-group { position: absolute; top: 0; left: 0; } .guideline-group .size-value { position: absolute; color: #f55; font-size: 12px; font-size: calc(12px * var(--zoom)); font-weight: bold; } .guideline-group.horizontal .size-value { transform-origin: 50% 100%; transform: translateX(-50%); left: 50%; bottom: 5px; bottom: calc(2px + 3px * var(--zoom)); } .guideline-group.vertical .size-value { transform-origin: 0% 50%; top: 50%; transform: translateY(-50%); left: 5px; left: calc(2px + 3px * var(--zoom)); } .guideline.gap { background: #f55; } .size-value.gap { color: #f55; } `, ], render( moveable: MoveableManagerInterface, React: Renderer ): any[] { const state = moveable.state; const { top: targetTop, left: targetLeft, pos1, pos2, pos3, pos4, snapRenderInfo, } = state; const { snapRenderThreshold = 1, } = moveable.props; if (!snapRenderInfo || !snapRenderInfo.render || !hasGuidelines(moveable, "")) { // reset store watchValue( moveable, "boundMap", getInitialBounds(), v => JSON.stringify(v), ); watchValue( moveable, "innerBoundMap", getInitialBounds(), v => JSON.stringify(v), ); return []; } state.guidelines = getTotalGuidelines(moveable); const minLeft = Math.min(pos1[0], pos2[0], pos3[0], pos4[0]); const minTop = Math.min(pos1[1], pos2[1], pos3[1], pos4[1]); const externalPoses = snapRenderInfo.externalPoses || []; const poses = getAbsolutePosesByState(moveable.state); const verticalSnapPoses: SnappableRenderType[] = []; const horizontalSnapPoses: SnappableRenderType[] = []; const verticalGuidelines: SnapGuideline[] = []; const horizontalGuidelines: SnapGuideline[] = []; const snapInfos: Array<{ vertical: SnapDirectionInfo; horizontal: SnapDirectionInfo; }> = []; const { width, height, top, left, bottom, right } = getRect(poses); const targetRect = { left, right, top, bottom, center: (left + right) / 2, middle: (top + bottom) / 2 }; const hasExternalPoses = externalPoses.length > 0; const externalRect = hasExternalPoses ? getRect(externalPoses) : ({} as ReturnType); if (!snapRenderInfo.request) { if (snapRenderInfo.direction) { snapInfos.push( getSnapInfosByDirection( moveable, poses, snapRenderInfo.direction, snapRenderThreshold, ) ); } if (snapRenderInfo.snap) { const rect = getRect(poses); if (snapRenderInfo.center) { (rect as any).middle = (rect.top + rect.bottom) / 2; (rect as any).center = (rect.left + rect.right) / 2; } snapInfos.push(checkSnaps(moveable, rect, snapRenderThreshold)); } if (hasExternalPoses) { if (snapRenderInfo.center) { (externalRect as any).middle = (externalRect.top + externalRect.bottom) / 2; (externalRect as any).center = (externalRect.left + externalRect.right) / 2; } snapInfos.push(checkSnaps(moveable, externalRect, snapRenderThreshold)); } snapInfos.forEach((snapInfo) => { const { vertical: { posInfos: verticalPosInfos }, horizontal: { posInfos: horizontalPosInfos }, } = snapInfo; verticalSnapPoses.push( ...verticalPosInfos.filter(({ guidelineInfos }) => { return guidelineInfos.some(({ guideline }) => !guideline.hide); }).map( (posInfo) => ({ type: "snap", pos: posInfo.pos, } as const) ) ); horizontalSnapPoses.push( ...horizontalPosInfos.filter(({ guidelineInfos }) => { return guidelineInfos.some(({ guideline }) => !guideline.hide); }).map( (posInfo) => ({ type: "snap", pos: posInfo.pos, } as const) ) ); verticalGuidelines.push(...getSnapGuidelines(verticalPosInfos)); horizontalGuidelines.push(...getSnapGuidelines(horizontalPosInfos)); }); } const { boundMap, innerBoundMap, } = addBoundGuidelines( moveable, [left, right], [top, bottom], verticalSnapPoses, horizontalSnapPoses ); if (hasExternalPoses) { addBoundGuidelines( moveable, [externalRect.left, externalRect.right], [externalRect.top, externalRect.bottom], verticalSnapPoses, horizontalSnapPoses, snapRenderInfo.externalBounds ); } const allGuidelines = [...verticalGuidelines, ...horizontalGuidelines]; const elementGuidelines = allGuidelines.filter(guideline => guideline.element && !guideline.gapRects); const gapGuidelines = allGuidelines.filter(guideline => guideline.gapRects).sort((a, b) => { return a.gap! - b.gap!; }); triggerEvent( moveable, "onSnap", { guidelines: allGuidelines.filter(({ element }) => !element), elements: elementGuidelines, gaps: gapGuidelines, }, true ); const nextBoundMap = watchValue( moveable, "boundMap", boundMap, v => JSON.stringify(v), getInitialBounds(), ); const nextInnerBoundMap = watchValue( moveable, "innerBoundMap", innerBoundMap, v => JSON.stringify(v), getInitialBounds(), ); if (boundMap === nextBoundMap || innerBoundMap === nextInnerBoundMap) { triggerEvent( moveable, "onBound", { bounds: boundMap, innerBounds: innerBoundMap, }, true ); } // verticalSnapPoses. return [ ...renderDashedGuidelines( moveable, elementGuidelines, [minLeft, minTop], targetRect, React, ), ...renderGapGuidelines( moveable, gapGuidelines, [minLeft, minTop], targetRect, React, ), ...renderGuidelines( moveable, "horizontal", horizontalGuidelines, [targetLeft, targetTop], targetRect, React ), ...renderGuidelines( moveable, "vertical", verticalGuidelines, [targetLeft, targetTop], targetRect, React ), ...renderSnapPoses( moveable, "horizontal", horizontalSnapPoses, minLeft, targetTop, width, 0, React ), ...renderSnapPoses( moveable, "vertical", verticalSnapPoses, minTop, targetLeft, height, 1, React ), ]; }, dragStart( moveable: MoveableManagerInterface, e: any ) { moveable.state.snapRenderInfo = { request: e.isRequest, snap: true, center: true, }; checkSnapInfo(moveable); }, drag( moveable: MoveableManagerInterface ) { const state = moveable.state; if (!checkSnapInfo(moveable)) { state.guidelines = getTotalGuidelines(moveable); } if (state.snapRenderInfo) { state.snapRenderInfo.render = true; } }, pinchStart( moveable: MoveableManagerInterface ) { this.unset(moveable); }, dragEnd( moveable: MoveableManagerInterface ) { this.unset(moveable); }, dragControlCondition(moveable: MoveableManagerInterface, e: any) { if (directionCondition(moveable, e) || rotatableDragControlCondtion(moveable, e)) { return true; } if (!e.isRequest && e.inputEvent) { return hasClass(e.inputEvent.target, prefix("snap-control")); } }, dragControlStart( moveable: MoveableManagerInterface ) { moveable.state.snapRenderInfo = null; checkSnapInfo(moveable); }, dragControl( moveable: MoveableManagerInterface ) { this.drag(moveable); }, dragControlEnd( moveable: MoveableManagerInterface ) { this.unset(moveable); }, dragGroupStart(moveable: any, e: any) { this.dragStart(moveable, e); }, dragGroup( moveable: MoveableGroupInterface ) { this.drag(moveable); }, dragGroupEnd( moveable: MoveableGroupInterface ) { this.unset(moveable); }, dragGroupControlStart( moveable: MoveableGroupInterface ) { moveable.state.snapRenderInfo = null; checkSnapInfo(moveable); }, dragGroupControl( moveable: MoveableManagerInterface ) { this.drag(moveable); }, dragGroupControlEnd( moveable: MoveableGroupInterface ) { this.unset(moveable); }, unset(moveable: any) { const state = moveable.state; state.enableSnap = false; state.guidelines = []; state.snapRenderInfo = null; state.elementRects = []; }, }; /** * Whether or not target can be snapped to the guideline. (default: false) * @name Moveable.Snappable#snappable * @see {@link https://daybrush.com/moveable/release/latest/doc/Moveable.Snappable.html#.SnappableOptions} * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body); * * moveable.snappable = true; */ /** * A snap container that is the basis for snap, bounds, and innerBounds. (default: null = container) * @name Moveable.Snappable#snapContainer * @see {@link https://daybrush.com/moveable/release/latest/doc/Moveable.Snappable.html#.SnappableOptions} * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.querySelector(".container")); * * moveable.snapContainer = document.body; */ /** * You can specify the directions to snap to the target. (default: { left: true, top: true, right: true, bottom: true }) * @name Moveable.Snappable#snapDirections * @see {@link https://daybrush.com/moveable/release/latest/doc/Moveable.Snappable.html#.SnappableOptions} * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body, { * snappable: true, * snapDirections: true, * }); * // snap center * moveable.snapDirections = { left: true, top: true, right: true, bottom: true, center: true, middle: true }; */ /** * You can specify the snap directions of elements. (default: { left: true, ftrue, right: true, bottom: true }) * @name Moveable.Snappable#elementSnapDirections * @see {@link https://daybrush.com/moveable/release/latest/doc/Moveable.Snappable.html#.SnappableOptions} * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body, { * snappable: true, * elementSnapDirections: true, * }); * // snap center * moveable.elementSnapDirections = { left: true, top: true, right: true, bottom: true, center: true, middle: true }; */ /** * When you drag, make the gap snap in the element guidelines. (default: true) * @name Moveable.Snappable#snapGap * @see {@link https://daybrush.com/moveable/release/latest/doc/Moveable.Snappable.html#.SnappableOptions} * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body, { * snappable: true, * snapElement: true, * snapGap: true, * }); * * moveable.snapGap = false; */ /** * Distance value that can snap to guidelines. (default: 5) * @name Moveable.Snappable#snapThreshold * @see {@link https://daybrush.com/moveable/release/latest/doc/Moveable.Snappable.html#.SnappableOptions} * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body); * * moveable.snapThreshold = 5; */ /** * Add guidelines in the horizontal direction. (default: []) * @name Moveable.Snappable#horizontalGuidelines * @see {@link https://daybrush.com/moveable/release/latest/doc/Moveable.Snappable.html#.SnappableOptions} * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body); * * moveable.horizontalGuidelines = [100, 200, 500]; */ /** * Add guidelines in the vertical direction. (default: []) * @name Moveable.Snappable#verticalGuidelines * @see {@link https://daybrush.com/moveable/release/latest/doc/Moveable.Snappable.html#.SnappableOptions} * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body); * * moveable.verticalGuidelines = [100, 200, 500]; */ /** * Add guidelines for the element. (default: []) * @name Moveable.Snappable#elementGuidelines * @see {@link https://daybrush.com/moveable/release/latest/doc/Moveable.Snappable.html#.SnappableOptions} * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body); * * moveable.elementGuidelines = [ * document.querySelector(".element"), * ]; */ /** * You can set up boundaries. * @name Moveable.Snappable#bounds * @see {@link https://daybrush.com/moveable/release/latest/doc/Moveable.Snappable.html#.SnappableOptions} * @default null * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body); * * moveable.bounds = { left: 0, right: 1000, top: 0, bottom: 1000}; */ /** * You can set up inner boundaries. * @name Moveable.Snappable#innerBounds * @see {@link https://daybrush.com/moveable/release/latest/doc/Moveable.Snappable.html#.SnappableOptions} * @default null * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body); * * moveable.innerBounds = { left: 500, top: 500, width: 100, height: 100}; */ /** * snap distance digits (default: 0) * @name Moveable.Snappable#snapDigit * @see {@link https://daybrush.com/moveable/release/latest/doc/Moveable.Snappable.html#.SnappableOptions} * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body); * * moveable.snapDigit = 0 */ /** * If width size is greater than 0, you can vertical snap to the grid. (default: 0) * @name Moveable.Snappable#snapGridWidth * @see {@link https://daybrush.com/moveable/release/latest/doc/Moveable.Snappable.html#.SnappableOptions} * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body); * * moveable.snapGridWidth = 5; */ /** * If height size is greater than 0, you can horizontal snap to the grid. (default: 0) * @name Moveable.Snappable#snapGridHeight * @see {@link https://daybrush.com/moveable/release/latest/doc/Moveable.Snappable.html#.SnappableOptions} * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body); * * moveable.snapGridHeight = 5; */ /** * Whether to show snap distance (default: true) * @name Moveable.Snappable#isDisplaySnapDigit * @see {@link https://daybrush.com/moveable/release/latest/doc/Moveable.Snappable.html#.SnappableOptions} * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body); * * moveable.isDisplaySnapDigit = true; */ /** * Whether to show element inner snap distance (default: false) * @name Moveable.Snappable#isDisplayInnerSnapDigit * @see {@link https://daybrush.com/moveable/release/latest/doc/Moveable.Snappable.html#.SnappableOptions} * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body); * * moveable.isDisplayInnerSnapDigit = true; */ /** * You can set the text format of the distance shown in the guidelines. (default: self) * @name Moveable.Snappable#snapDistFormat * @see {@link https://daybrush.com/moveable/release/latest/doc/Moveable.Snappable.html#.SnappableOptions} * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body, { * snappable: true, * snapDistFormat: (v, type) => v, * }); * moveable.snapDistFormat = (v, type) => `${v}px`; */ /** * When you drag or dragControl, the `snap` event is called. * @memberof Moveable.Snappable * @event snap * @param {Moveable.Snappable.OnSnap} - Parameters for the `snap` event * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body, { * snappable: true * }); * moveable.on("snap", e => { * console.log("onSnap", e); * }); */