For my fabric.js project: I'm trying to setup object snapping and alignment guidelines. For snapping, this means when a user drags an object around, if any edge of the object comes close to alignment with another object edge, it will snap into place. During this time, guidelines appear as visual helpers for the user.
So far I'm implementing existing work, done by various fabric.js contributors, found here:
centering_guidelines.js & aligning_guidelines.js.
WORKS: At default zoom (1), object snapping and alignment guidelines work great!
FAILS: When zooming (in or out), the visual guidelines appear in the wrong position, however snapping maintains correct functionality.
CODE SAMPLES: Move objects around. At default zoom, snapping and guidelines work great. Change zoom level (with mousewheel) and notice guidelines are not positioned correctly, however snapping works fine.
Sample 1: Simple
Original libraries loaded as-is; simple demo.
https://codepen.io/MarsAndBack/pen/ZEQMXoM
Sample 2: Detailed
Original libraries copy-pasted inline, with modifications to help investigation.
https://codepen.io/MarsAndBack/pen/LYGJGoq
Note: Codepen has full code.
// ==========================================
// SETUP
// ==========================================
const canvas = new fabric.Canvas("myCanvas")
canvas.backgroundColor = "#222222";
var lastClientX = 0
var lastClientY = 0
var state = "default"
const outer = null
const box1 = null
const box2 = null
this.centerLine_horizontal = ""
this.centerLine_vertical = ""
this.alignmentLines_horizontal = ""
this.alignmentLines_vertical = ""
fabric.Object.prototype.set({
cornerSize: 15,
cornerStyle: 'circle',
cornerColor: '#ffffff',
transparentCorners: false
})
setupObjects()
updateInfo(canvas)
function setupObjects() {
this.outer = new fabric.Rect({
width: canvas.getWidth(),
height: canvas.getHeight(),
top: 20,
left: 20,
stroke: '#ffffff',
evented: false,
selectable: false
})
this.box1 = new fabric.Rect({
width: 240,
height: 100,
top: 20,
left: 20,
fill: '#fff28a',
myType: "box"
})
this.box2 = new fabric.Rect({
width: 240,
height: 100,
top: 140,
left: 20,
fill: '#ff8a8a',
myType: "box"
})
this.box3 = new fabric.Rect({
width: 100,
height: 160,
top: 20,
left: 280,
fill: '#cf8aff',
myType: "box"
})
canvas.add(this.outer)
this.outer.center()
canvas.add(this.box1)
canvas.add(this.box2)
canvas.add(this.box3)
let allBoxes = new fabric.ActiveSelection(canvas.getObjects().filter(obj => obj.myType == "box"), {
canvas: canvas
})
allBoxes.center()
allBoxes.destroy()
}
function updateInfo() {
let info_zoom = document.getElementById('info_zoom')
let info_vptTop = document.getElementById('info_vptTop')
let info_vptLeft = document.getElementById('info_vptLeft')
let info_centerLine_horizontal = document.getElementById('info_centerLine_horizontal')
let info_centerLine_vertical = document.getElementById('info_centerLine_vertical')
let info_alignmentLines_horizontal = document.getElementById('info_alignmentLines_horizontal')
let info_alignmentLines_vertical = document.getElementById('info_alignmentLines_vertical')
info_zoom.innerHTML = canvas.getZoom().toFixed(2)
info_vptTop.innerHTML = Math.round(canvas.viewportTransform[5])
info_vptLeft.innerHTML = Math.round(canvas.viewportTransform[4])
info_centerLine_horizontal.innerHTML = this.centerLine_horizontal
info_centerLine_vertical.innerHTML = this.centerLine_vertical
info_alignmentLines_horizontal.innerHTML = this.alignmentLines_horizontal
info_alignmentLines_vertical.innerHTML = this.alignmentLines_vertical
}
// ------------------------------------
// Reset
// ------------------------------------
let resetButton = document.getElementById('reset')
resetButton.addEventListener('click', function() {
reset()
}, false)
function reset() {
canvas.remove(...canvas.getObjects())
setupObjects()
canvas.setViewportTransform([1, 0, 0, 1, 0, 0])
updateInfo()
}
// ------------------------------------
// ==========================================
// MOUSE INTERACTIONS
// ==========================================
// MOUSEWHEEL ZOOM
canvas.on('mouse:wheel', (opt) => {
let delta = 0
// -------------------------------
// WHEEL RESOLUTION
let wheelDelta = opt.e.wheelDelta
let deltaY = opt.e.deltaY
// CHROME WIN/MAC | SAFARI 7 MAC | OPERA WIN/MAC | EDGE
if (wheelDelta) {
delta = -wheelDelta / 120
}
// FIREFOX WIN / MAC | IE
if (deltaY) {
deltaY > 0 ? delta = 1 : delta = -1
}
// -------------------------------
let pointer = canvas.getPointer(opt.e)
let zoom = canvas.getZoom()
zoom = zoom - delta / 10
// limit zoom in
if (zoom > 4) zoom = 4
// limit zoom out
if (zoom < 0.2) {
zoom = 0.2
}
//canvas.zoomToPoint({
// x: opt.e.offsetX,
// y: opt.e.offsetY
//}, zoom)
canvas.zoomToPoint(
new fabric.Point(canvas.width / 2, canvas.height / 2),
zoom);
opt.e.preventDefault()
opt.e.stopPropagation()
canvas.renderAll()
canvas.calcOffset()
updateInfo(canvas)
})
initCenteringGuidelines(canvas)
initAligningGuidelines(canvas)
// ==========================================
// CANVAS CENTER SNAPPING & ALIGNMENT GUIDELINES
// ==========================================
// ORIGINAL:
// https://github.com/fabricjs/fabric.js/blob/master/lib/centering_guidelines.js
/**
* Augments canvas by assigning to `onObjectMove` and `onAfterRender`.
* This kind of sucks because other code using those methods will stop functioning.
* Need to fix it by replacing callbacks with pub/sub kind of subscription model.
* (or maybe use existing fabric.util.fire/observe (if it won't be too slow))
*/
function initCenteringGuidelines(canvas) {
let canvasWidth = canvas.getWidth(),
canvasHeight = canvas.getHeight(),
canvasWidthCenter = canvasWidth / 2,
canvasHeightCenter = canvasHeight / 2,
canvasWidthCenterMap = {},
canvasHeightCenterMap = {},
centerLineMargin = 4,
centerLineColor = 'purple',
centerLineWidth = 2,
ctx = canvas.getSelectionContext(),
viewportTransform
for (let i = canvasWidthCenter - centerLineMargin, len = canvasWidthCenter + centerLineMargin; i <= len; i++) {
canvasWidthCenterMap[Math.round(i)] = true
}
for (let i = canvasHeightCenter - centerLineMargin, len = canvasHeightCenter + centerLineMargin; i <= len; i++) {
canvasHeightCenterMap[Math.round(i)] = true
}
function showVerticalCenterLine() {
showCenterLine(canvasWidthCenter + 0.5, 0, canvasWidthCenter + 0.5, canvasHeight)
}
function showHorizontalCenterLine() {
showCenterLine(0, canvasHeightCenter + 0.5, canvasWidth, canvasHeightCenter + 0.5)
}
function showCenterLine(x1, y1, x2, y2) {
ctx.save()
ctx.strokeStyle = centerLineColor
ctx.lineWidth = centerLineWidth
ctx.beginPath()
ctx.moveTo(x1 * viewportTransform[0], y1 * viewportTransform[3])
ctx.lineTo(x2 * viewportTransform[0], y2 * viewportTransform[3])
ctx.stroke()
ctx.restore()
}
let afterRenderActions = [],
isInVerticalCenter,
isInHorizontalCenter
canvas.on('mouse:down', () => {
isInVerticalCenter = isInHorizontalCenter = null
this.centerLine_horizontal = ""
this.centerLine_vertical = ""
updateInfo()
viewportTransform = canvas.viewportTransform
})
canvas.on('object:moving', function(e) {
let object = e.target,
objectCenter = object.getCenterPoint(),
transform = canvas._currentTransform
if (!transform) return
isInVerticalCenter = Math.round(objectCenter.x) in canvasWidthCenterMap,
isInHorizontalCenter = Math.round(objectCenter.y) in canvasHeightCenterMap
if (isInHorizontalCenter || isInVerticalCenter) {
object.setPositionByOrigin(new fabric.Point((isInVerticalCenter ? canvasWidthCenter : objectCenter.x), (isInHorizontalCenter ? canvasHeightCenter : objectCenter.y)), 'center', 'center')
}
})
canvas.on('before:render', function() {
canvas.clearContext(canvas.contextTop)
})
canvas.on('after:render', () => {
if (isInVerticalCenter) {
showVerticalCenterLine()
this.centerLine_horizontal = ""
this.centerLine_vertical = (canvasWidthCenter + 0.5) + ", " + 0 + ", " + (canvasWidthCenter + 0.5) + ", " + canvasHeight
}
if (isInHorizontalCenter) {
showHorizontalCenterLine()
this.centerLine_horizontal = (canvasWidthCenter + 0.5) + ", " + 0 + ", " + (canvasWidthCenter + 0.5) + ", " + canvasHeight
this.centerLine_vertical = ""
}
updateInfo()
})
canvas.on('mouse:up', function() {
// clear these values, to stop drawing guidelines once mouse is up
canvas.renderAll()
})
}
// ===============================================
// OBJECT SNAPPING & ALIGNMENT GUIDELINES
// ===============================================
// ORIGINAL:
// https://github.com/fabricjs/fabric.js/blob/master/lib/aligning_guidelines.js
// Original author:
/**
* Should objects be aligned by a bounding box?
* [Bug] Scaled objects sometimes can not be aligned by edges
*
*/
function initAligningGuidelines(canvas) {
let ctx = canvas.getSelectionContext(),
aligningLineOffset = 5,
aligningLineMargin = 4,
aligningLineWidth = 2,
aligningLineColor = 'lime',
viewportTransform,
zoom = null,
verticalLines = [],
horizontalLines = [],
canvasContainer = document.getElementById("myCanvas"),
containerWidth = canvasContainer.offsetWidth,
containerHeight = canvasContainer.offsetHeight
function drawVerticalLine(coords) {
drawLine(
coords.x + 0.5, coords.y1 > coords.y2 ? coords.y2 : coords.y1,
coords.x + 0.5, coords.y2 > coords.y1 ? coords.y2 : coords.y1
)
}
function drawHorizontalLine(coords) {
drawLine(
coords.x1 > coords.x2 ? coords.x2 : coords.x1, coords.y + 0.5,
coords.x2 > coords.x1 ? coords.x2 : coords.x1, coords.y + 0.5
)
}
function drawLine(x1, y1, x2, y2) {
ctx.save()
ctx.lineWidth = aligningLineWidth
ctx.strokeStyle = aligningLineColor
ctx.beginPath()
//console.log("x1 :" + x1)
//console.log("viewportTransform[4] :" + viewportTransform[4])
//console.log("zoom :" + zoom)
ctx.moveTo(
((x1 + viewportTransform[4]) * zoom),
((y1 + viewportTransform[5]) * zoom)
)
//console.log("-------")
//console.log("x1 :" + x1)
//console.log("viewportTransform[4] :" + viewportTransform[4])
//console.log("zoom :" + zoom)
//console.log("x :" + (x1 + canvas.viewportTransform[4]) * zoom)
ctx.lineTo(
((x2 + viewportTransform[4]) * zoom),
((y2 + viewportTransform[5]) * zoom)
)
ctx.stroke()
ctx.restore()
}
function isInRange(value1, value2) {
value1 = Math.round(value1)
value2 = Math.round(value2)
for (var i = value1 - aligningLineMargin, len = value1 + aligningLineMargin; i <= len; i++) {
if (i === value2) {
return true
}
}
return false;
}
canvas.on('mouse:down', function() {
verticalLines.length = horizontalLines.length = 0
viewportTransform = canvas.viewportTransform
zoom = canvas.getZoom()
})
canvas.on('object:moving', (e) => {
verticalLines.length = horizontalLines.length = 0
let activeObject = e.target,
canvasObjects = canvas.getObjects().filter(obj => obj.myType == "box"),
activeObjectCenter = activeObject.getCenterPoint(),
activeObjectLeft = activeObjectCenter.x,
activeObjectTop = activeObjectCenter.y,
activeObjectBoundingRect = activeObject.getBoundingRect(),
activeObjectHeight = activeObjectBoundingRect.height / viewportTransform[3],
activeObjectWidth = activeObjectBoundingRect.width / viewportTransform[0],
horizontalInTheRange = false,
verticalInTheRange = false,
transform = canvas._currentTransform;
//console.log("|||||||||")
//console.log("active acoords is: " + JSON.stringify(activeObject.aCoords, null, 4))
//console.log("active acoords is: " + JSON.stringify(activeObject.oCoords, null, 4))
//console.log("active left offset is: " + JSON.stringify(activeObject.aCoords, null, 4))
//containerWidth = canvasContainer.offsetWidth
//containerHeight = canvasContainer.offsetHeight
//console.log("active left from container is: " + (containerWidth - this.outer.width) / 2 + activeObject.aCoords.tl.x )
if (!transform) return;
// It should be trivial to DRY this up by encapsulating (repeating) creation of x1, x2, y1, and y2 into functions,
// but we're not doing it here for perf. reasons -- as this a function that's invoked on every mouse move
for (let i = canvasObjects.length; i--;) {
if (canvasObjects[i] === activeObject) continue
let objectCenter = canvasObjects[i].getCenterPoint(),
objectLeft = objectCenter.x,
objectTop = objectCenter.y,
objectBoundingRect = canvasObjects[i].getBoundingRect(),
objectHeight = objectBoundingRect.height / viewportTransform[3],
objectWidth = objectBoundingRect.width / viewportTransform[0]
// snap by the horizontal center line
if (isInRange(objectLeft, activeObjectLeft)) {
verticalInTheRange = true
verticalLines.push({
x: objectLeft,
y1: (objectTop < activeObjectTop) ?
(objectTop - objectHeight / 2 - aligningLineOffset) :
(objectTop + objectHeight / 2 + aligningLineOffset),
y2: (activeObjectTop > objectTop) ?
(activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) :
(activeObjectTop - activeObjectHeight / 2 - aligningLineOffset)
})
activeObject.setPositionByOrigin(new fabric.Point(objectLeft, activeObjectTop), 'center', 'center');
}
// snap by the left edge
if (isInRange(objectLeft - objectWidth / 2, activeObjectLeft - activeObjectWidth / 2)) {
verticalInTheRange = true
verticalLines.push({
x: objectLeft - objectWidth / 2,
y1: (objectTop < activeObjectTop) ?
(objectTop - objectHeight / 2 - aligningLineOffset) :
(objectTop + objectHeight / 2 + aligningLineOffset),
y2: (activeObjectTop > objectTop) ?
(activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) :
(activeObjectTop - activeObjectHeight / 2 - aligningLineOffset)
})
activeObject.setPositionByOrigin(new fabric.Point(objectLeft - objectWidth / 2 + activeObjectWidth / 2, activeObjectTop), 'center', 'center')
}
// snap by the right edge
if (isInRange(objectLeft + objectWidth / 2, activeObjectLeft + activeObjectWidth / 2)) {
verticalInTheRange = true
verticalLines.push({
x: objectLeft + objectWidth / 2,
y1: (objectTop < activeObjectTop) ?
(objectTop - objectHeight / 2 - aligningLineOffset) :
(objectTop + objectHeight / 2 + aligningLineOffset),
y2: (activeObjectTop > objectTop) ?
(activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) :
(activeObjectTop - activeObjectHeight / 2 - aligningLineOffset)
})
activeObject.setPositionByOrigin(new fabric.Point(objectLeft + objectWidth / 2 - activeObjectWidth / 2, activeObjectTop), 'center', 'center')
}
// snap by the vertical center line
if (isInRange(objectTop, activeObjectTop)) {
horizontalInTheRange = true;
horizontalLines.push({
y: objectTop,
x1: (objectLeft < activeObjectLeft) ?
(objectLeft - objectWidth / 2 - aligningLineOffset) :
(objectLeft + objectWidth / 2 + aligningLineOffset),
x2: (activeObjectLeft > objectLeft) ?
(activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) :
(activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset)
})
activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop), 'center', 'center')
}
// snap by the top edge
if (isInRange(objectTop - objectHeight / 2, activeObjectTop - activeObjectHeight / 2)) {
horizontalInTheRange = true
horizontalLines.push({
y: objectTop - objectHeight / 2,
x1: (objectLeft < activeObjectLeft) ?
(objectLeft - objectWidth / 2 - aligningLineOffset) :
(objectLeft + objectWidth / 2 + aligningLineOffset),
x2: (activeObjectLeft > objectLeft) ?
(activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) :
(activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset)
})
activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop - objectHeight / 2 + activeObjectHeight / 2), 'center', 'center');
}
// snap by the bottom edge
if (isInRange(objectTop + objectHeight / 2, activeObjectTop + activeObjectHeight / 2)) {
horizontalInTheRange = true
horizontalLines.push({
y: objectTop + objectHeight / 2,
x1: (objectLeft < activeObjectLeft) ?
(objectLeft - objectWidth / 2 - aligningLineOffset) :
(objectLeft + objectWidth / 2 + aligningLineOffset),
x2: (activeObjectLeft > objectLeft) ?
(activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) :
(activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset)
})
activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop + objectHeight / 2 - activeObjectHeight / 2), 'center', 'center')
}
}
if (!horizontalInTheRange) {
horizontalLines.length = 0
}
if (!verticalInTheRange) {
verticalLines.length = 0
}
})
canvas.on('mouse:wheel', (opt) => {
verticalLines.length = horizontalLines.length = 0
})
canvas.on('before:render', function() {
canvas.clearContext(canvas.contextTop)
})
canvas.on('after:render', () => {
for (let i = verticalLines.length; i--;) {
drawVerticalLine(verticalLines[i])
}
for (let i = horizontalLines.length; i--;) {
drawHorizontalLine(horizontalLines[i])
}
this.alignmentLines_horizontal = JSON.stringify(horizontalLines, null, 4)
this.alignmentLines_vertical = JSON.stringify(verticalLines, null, 4)
updateInfo()
// console.log("activeObject left edge x is: " + canvas.getActiveObject().left)
//verticalLines.length = horizontalLines.length = 0
canvas.calcOffset()
})
canvas.on('mouse:up', () => {
//verticalLines.length = horizontalLines.length = 0
canvas.renderAll()
//this.alignmentLines_horizontal = horizontalLines
//this.alignmentLines_vertical = verticalLines
//updateInfo()
})
}
#container {
display: flex;
font-family: sans-serif;
}
#header {
display: flex;
}
#reset {
background-color: #333333;
color: #ffffff;
padding: 1em;
border: none;
margin: 0.5em;
margin-top: 6em;
cursor: pointer;
}
#reset:hover {
background-color: #666666;
}
#reset:active {
background-color: #333333;
}
#info {
/* display: flex; */
display: none;
flex-direction: column;
}
#info>div {
display: flex;
flex-direction: column;
}
#info>div>div {
display: flex;
margin: 0.5em;
}
canvas {
display: block;
}
hr {
width: 100%;
}
<script src="https://pagecdn.io/lib/fabric/3.6.3/fabric.min.js"></script>
<div id="container">
<canvas id="myCanvas" width="500" height="300"></canvas>
<div id="sidebar">
<button id="reset">RESET</button>
<div id="info">
<div>
<div><b>zoom:</b>
<div id="info_zoom"></div>
</div>
<div><b>viewport top:</b>
<div id="info_vptTop"></div>
</div>
<div><b>viewport left:</b>
<div id="info_vptLeft"></div>
</div>
</div>
<hr />
<div>
<div><b>Alignment lines (green)</b></div>
<div><b>Horizontal:</b>
<div id="info_alignmentLines_horizontal"></div>
</div>
<div><b>Vertical:</b>
<div id="info_alignmentLines_vertical"></div>
</div>
</div>
<hr />
<div>
<div><b>Canvas-center lines (purple)</b></div>
<div><b>Horizontal:</b>
<div id="info_centerLine_horizontal"></div>
</div>
<div><b>Vertical:</b>
<div id="info_centerLine_vertical"></div>
</div>
</div>
</div>
</div>
</div>
I changed drawLine
function.Should work
https://jsfiddle.net/3mtcsy6p/1/
function drawLine(x1, y1, x2, y2) {
var originXY = fabric.util.transformPoint(new fabric.Point(x1, y1), canvas.viewportTransform),
dimensions = fabric.util.transformPoint(new fabric.Point(x2, y2),canvas.viewportTransform);
ctx.save()
ctx.lineWidth = aligningLineWidth
ctx.strokeStyle = aligningLineColor
ctx.beginPath()
ctx.moveTo(
( (originXY.x ) ),
( (originXY.y ) )
)
ctx.lineTo(
( (dimensions.x ) ),
( (dimensions.y ) )
)
ctx.stroke()
ctx.restore()
}