Fabric JS: Snapping guidelines not correctly positioned when zoomed - html5-canvas

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()
}

A more performant way would be to do the calculation directly in drawLine function insted of calling another one. As the function is called on every mouse movement, performance is important.
The new function:
function drawLine(x1, y1, x2, y2) {
ctx.save();
ctx.lineWidth = aligningLineWidth;
ctx.strokeStyle = aligningLineColor;
ctx.beginPath();
ctx.moveTo(((x1*zoom+viewportTransform[4])), ((y1*zoom+viewportTransform[5])));
ctx.lineTo(((x2*zoom+viewportTransform[4])), ((y2*zoom+viewportTransform[5])));
ctx.stroke();
ctx.restore();
}
The code pen: https://codepen.io/Theluiz-eduardo/pen/abqMVGV
Just out of curiosity, a performance test calling the function vs calculating directly:
const ctx = {}, canvas = {}, fabric = { util: {} }
ctx.moveTo = () => true;
ctx.lineTo = () => true;
canvas.viewportTransform = [0.5, 0, 0, 0.5, 200, 100]
canvas.getZoom = () => 0.5;
fabric.util.transformPoint = function(p, t, ignoreOffset) {
if (ignoreOffset) {
return fabric.Point(
t[0] * p.x + t[2] * p.y,
t[1] * p.x + t[3] * p.y
);
}
return fabric.Point(
t[0] * p.x + t[2] * p.y + t[4],
t[1] * p.x + t[3] * p.y + t[5]
);
}
fabric.Point = (x, y) => ({x: x, y: y})
const x1 = 10, x2 = 15, y1 = 20, y2 = 25, zoom = canvas.getZoom()
function performTest(fun, name, repeat) {
console.time(name)
while (repeat--) {
fun()
}
console.timeEnd(name)
}
const callingFun = () => {
var originXY = fabric.util.transformPoint(fabric.Point(x1, y1), canvas.viewportTransform),
dimensions = fabric.util.transformPoint(fabric.Point(x2, y2),canvas.viewportTransform);
ctx.moveTo(originXY.x, originXY.y)
ctx.lineTo(dimensions.x, dimensions.y)
}
const directCalc = () => {
ctx.moveTo(((x1*zoom+canvas.viewportTransform[4])), ((y1*zoom+canvas.viewportTransform[5])));
ctx.lineTo(((x2*zoom+canvas.viewportTransform[4])), ((y2*zoom+canvas.viewportTransform[5])));
}
performTest(directCalc, 'directCalc', 10**8)
performTest(callingFun, 'callingFun', 10**8)

Related

Fabric JS 2D lines into 3Dviews

I m working on a script that let clients draw the way they want to bend iron with detailed angles. everything is OK.
Now the client want's a 3D view of it. but i have no idea of how to do it (with three.js I guess)
I need a simple 3D view.
Here is what i did till now for 2D view, it's based on FabricJS:
$(function(){
var canvas = this.__canvas = new fabric.Canvas('c', { selection: false });
fabric.Object.prototype.originX = fabric.Object.prototype.originY = 'center';
var Lines = Array();
var Points = Array();
var circleAngles = Array();
var anglesValues = Array();
function radianToDegrees(r){
r = r * 180/Math.PI;
if(r < 0) r = -r;
if(360 - r < r) r = 360 - r;
return 180 - parseFloat(r).toFixed(0);
}
function makeCircle(left, top, line1, line2) {
var c = new fabric.Circle({
left: left,
top: top,
strokeWidth: 0,
radius: 8,
fill: '#000',
stroke: '#000'
});
c.hasControls = c.hasBorders = false;
c.line1 = line1;
c.line2 = line2;
return c;
}
function makeLine(coords) {
return new fabric.Line(coords, {
fill: 'red',
stroke: 'red',
strokeWidth: 2,
selectable: false,
evented: false,
});
}
function makeCircleAngle(angle, startAngle, endAngle){
if (angle == 1) color = 'red'; else color = '#003366';
circleAngle = new fabric.Circle({
radius: 20,
left: Lines[i].get('x1'),
top: Lines[i].get('y1'),
angle: 0,
startAngle: startAngle,
endAngle: endAngle,
strokeDashArray: [3, 2],
stroke: color,
fill: '',
selectable: false,
evented: false,
});
return circleAngle;
}
function makeText(text, x, y){
t = new fabric.Text(text, {
left: x, top: y, fontSize: 13, fill: '#003366', fontFamily: 'Arial',
selectable: false,
evented: false,
});
return t;
}
function drawLinesCanvas(){
$.each(Lines, function(i, e){
canvas.add(e);
})
}
function drawDotsCanvas(){
Points = Array();
p = makeCircle(Lines[0].get('x1'), Lines[0].get('y1'), null, Lines[0]);
Points.push(p)
canvas.add(p);
for(i = 0; i< Lines.length-1; i++){
p = makeCircle(Lines[i].get('x2'), Lines[i].get('y2'), Lines[i], Lines[i+1]);
Points.push(p)
canvas.add( p );
}
if (Lines.length-1 >= 0){
p = makeCircle(Lines[Lines.length-1].get('x2'), Lines[Lines.length-1].get('y2'), Lines[Lines.length-1]);
Points.push(p)
canvas.add( p );
}
}
function calculateAndDrawAngles(){
$.each(circleAngles, function(i, ce){
canvas.remove(ce);
})
$.each(Lines, function(i, l){
canvas.remove(l);
})
$.each(Points, function(i, p){
canvas.remove(p);
})
$.each(anglesValues, function(i, a){
canvas.remove(a);
})
if(Lines.length >= 2){
for(i=1; i<Lines.length; i++){
y11 = Lines[i].get('y1');
y12 = Lines[i].get('y2');
y21 = Lines[i-1].get('y1');
y22 = Lines[i-1].get('y2');
x11 = Lines[i].get('x1');
x12 = Lines[i].get('x2');
x21 = Lines[i-1].get('x1');
x22 = Lines[i-1].get('x2');
angle1 = Math.atan2(y11 - y12, x11 - x12);
angle2 = Math.atan2(y21 - y22, x21 - x22);
angle = angle1 - angle2;
if (angle < 0){
sStartAngle = Math.PI + angle1;
sEndAngle = angle2;
} else {
sStartAngle = angle1 - Math.PI;
sEndAngle = angle2;
}
myAngle = radianToDegrees(angle1 - angle2);
if(sStartAngle > sEndAngle) {
c = makeCircleAngle(1, sStartAngle, sEndAngle);
c1 = makeCircleAngle(2, sEndAngle, sStartAngle);
myAngleText = makeText(myAngle.toString()+'°', Lines[i].get('x1') +20, Lines[i].get('y1') + 20)
} else {
c = makeCircleAngle(2, sStartAngle, sEndAngle);
c1 = makeCircleAngle(1, sEndAngle, sStartAngle);
myAngleText = makeText(myAngle.toString()+'°', Lines[i].get('x1') - 20, Lines[i].get('y1') - 20)
}
circleAngles.push(c, c1);
canvas.add(c, c1);
canvas.add(myAngleText)
anglesValues.push(myAngleText);
}
drawLinesCanvas();
drawDotsCanvas();
}
}
canvas.on('object:moving', function(e) {
var p = e.target;
p.line1 && p.line1.set({ 'x2': p.left, 'y2': p.top });
p.line2 && p.line2.set({ 'x1': p.left, 'y1': p.top });
if (Lines.length > 1){
calculateAndDrawAngles();
}
});
canvas.on('mouse:wheel', function(opt) {
var delta = opt.e.deltaY;
var zoom = canvas.getZoom();
zoom = zoom + delta/200;
if (zoom > 20) zoom = 5;
if (zoom < 0.01) zoom = 0.5;
canvas.zoomToPoint({ x: opt.e.offsetX, y: opt.e.offsetY }, zoom);
opt.e.preventDefault();
opt.e.stopPropagation();
var vpt = this.viewportTransform;
if (zoom < 400 / 1000) {
this.viewportTransform[4] = 200 - 1000 * zoom / 2;
this.viewportTransform[5] = 200 - 1000 * zoom / 2;
} else {
if (vpt[4] >= 0) {
this.viewportTransform[4] = 0;
} else if (vpt[4] < canvas.getWidth() - 1000 * zoom) {
this.viewportTransform[4] = canvas.getWidth() - 1000 * zoom;
}
if (vpt[5] >= 0) {
this.viewportTransform[5] = 0;
} else if (vpt[5] < canvas.getHeight() - 1000 * zoom) {
this.viewportTransform[5] = canvas.getHeight() - 1000 * zoom;
}
}
});
canvas.on('mouse:down', function(opt) {
var evt = opt.e;
if (evt.altKey === true) {
this.isDragging = true;
this.selection = false;
this.lastPosX = evt.clientX;
this.lastPosY = evt.clientY;
}
});
canvas.on('mouse:move', function(opt) {
if (this.isDragging) {
var e = opt.e;
this.viewportTransform[4] += e.clientX - this.lastPosX;
this.viewportTransform[5] += e.clientY - this.lastPosY;
this.requestRenderAll();
this.lastPosX = e.clientX;
this.lastPosY = e.clientY;
}
});
canvas.on('mouse:up', function(opt) {
this.isDragging = false;
this.selection = true;
});
$('#addRight').on('click', function(){
fromPoint = Lines[Lines.length - 1];
Lines.push(makeLine([ fromPoint.get('x2'), fromPoint.get('y2'), fromPoint.get('x2') - 50, fromPoint.get('y2') + 50 ]))
calculateAndDrawAngles()
});
$('#addLeft').on('click', function(){
fromPoint = Lines[0];
Lines.unshift(makeLine([ fromPoint.get('x1') + 50, fromPoint.get('y1') + 50, fromPoint.get('x1'), fromPoint.get('y1') ]))
calculateAndDrawAngles()
});
function drawGrid(){
options = {
distance: 10,
width: c.width,
height: c.height,
param: {
stroke: '#ebebeb',
strokeWidth: 1,
selectable: false
}
},
gridLen = options.width / options.distance;
for (var i = 0; i < gridLen; i++) {
distance = i * options.distance,
horizontal = new fabric.Line([ distance, 0, distance, options.width], options.param),
vertical = new fabric.Line([ 0, distance, options.width, distance], options.param);
canvas.add(horizontal);
canvas.add(vertical);
if(i%5 === 0){
horizontal.set({stroke: '#cccccc'});
vertical.set({stroke: '#cccccc'});
};
canvas.sendBackwards(horizontal);
canvas.sendBackwards(vertical);
};
}
Lines = [makeLine([ 100, 50, 400, 50 ])];
drawGrid();
drawLinesCanvas();
drawDotsCanvas();
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.3/fabric.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<canvas id="c" width="500" height="400" style="border:1px solid #ccc;"></canvas>
<div class="text-center">
<button class="btn btn-info" id="addLeft">Ajouter un point à gauche</button>
<button class="btn btn-info" id="addRight">Ajouter un point à droite</button>
</div>
Just an option, when you can obtain the coordinates of points and use them to build a bended iron:
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 1, 1000);
camera.position.setScalar(10);
var renderer = new THREE.WebGLRenderer();
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
var controls = new THREE.OrbitControls(camera, renderer.domElement);
var obtained_data = [];
for (let i = 0; i < 10; i++) {
obtained_data.push(
new THREE.Vector2(i, Math.random() * 8 * 0.5 - 0.5) // fill the data with example coordinates
);
}
//console.log(obtained_data);
var dataLength = obtained_data.length;
var geom = new THREE.PlaneBufferGeometry(1, 10, dataLength - 1, 10); // size on Y and its segmentation is up to you
geom.rotateX(Math.PI * 0.5);
var pos = geom.getAttribute("position");
for (let i = 0; i < pos.count; i++) {
let idx = i % dataLength;
let x = obtained_data[idx].x;
let y = obtained_data[idx].y;
pos.setXY(i, x, y);
}
pos.needsUpdate = true;
geom.computeVertexNormals();
var mat = new THREE.MeshBasicMaterial({
color: "aqua",
wireframe: true
});
var iron = new THREE.Mesh(geom, mat);
scene.add(iron);
renderer.setAnimationLoop(() => {
renderer.render(scene, camera);
});
body {
overflow: hidden;
margin: 0;
}
<script src="https://threejs.org/build/three.min.js"></script>
<script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script>

FabricJS - change the anchor side of the rotating point

I'm trying to render a box with the rotating point in the bottom. Normaly, we can render a box with the property hasRotatingPoint: true and a handle appear in the top side of the box to the user rotate it.
What I want is to draw it in the bottom side of the box.
This could be achieved by set angle: 180 and the rotating point will appear in the bottom side (as we rotate the box 180°) but I got a draw problem with this approach: the drawing process also rotated 180° and now my box goes in the opposite direction when I'm drawing.
Thanks in advance for any help!
Edit 1
There is a very similar question about this problem here How to change Rotating point position to bottom in FabricJS? but the solution accepted creates another bug in my context as described in the third paragraph.
You need to rewrite 3 functions. fabric.Object.prototype.calcCoords, fabric.Object.prototype.drawControls, fabric.Object.prototype.drawBorders
Those 3 are almost the same, the only difference is that the mtr point is calculated different.
fabric.Object.prototype.calcCoords= function(absolute) {
var rotateMatrix = this._calcRotateMatrix(),
translateMatrix = this._calcTranslateMatrix(),
startMatrix = fabric.util.multiplyTransformMatrices(translateMatrix, rotateMatrix),
vpt = this.getViewportTransform(),
finalMatrix = absolute ? startMatrix : fabric.util.multiplyTransformMatrices(vpt, startMatrix),
dim = this._getTransformedDimensions(),
w = dim.x / 2, h = dim.y / 2,
tl = fabric.util.transformPoint({ x: -w, y: -h }, finalMatrix),
tr = fabric.util.transformPoint({ x: w, y: -h }, finalMatrix),
bl = fabric.util.transformPoint({ x: -w, y: h }, finalMatrix),
br = fabric.util.transformPoint({ x: w, y: h }, finalMatrix);
if (!absolute) {
var padding = this.padding, angle = fabric.util.degreesToRadians(this.angle),
cos = fabric.util.cos(angle), sin = fabric.util.sin(angle),
cosP = cos * padding, sinP = sin * padding, cosPSinP = cosP + sinP,
cosPMinusSinP = cosP - sinP;
if (padding) {
tl.x -= cosPMinusSinP;
tl.y -= cosPSinP;
tr.x += cosPSinP;
tr.y -= cosPMinusSinP;
bl.x -= cosPSinP;
bl.y += cosPMinusSinP;
br.x += cosPMinusSinP;
br.y += cosPSinP;
}
var ml = new fabric.Point((tl.x + bl.x) / 2, (tl.y + bl.y) / 2),
mt = new fabric.Point((tr.x + tl.x) / 2, (tr.y + tl.y) / 2),
mr = new fabric.Point((br.x + tr.x) / 2, (br.y + tr.y) / 2),
mb = new fabric.Point((br.x + bl.x) / 2, (br.y + bl.y) / 2),
mtr = new fabric.Point(mb.x - sin * this.rotatingPointOffset, mb.y + cos * this.rotatingPointOffset);
}
// if (!absolute) {
// var canvas = this.canvas;
// setTimeout(function() {
// canvas.contextTop.clearRect(0, 0, 700, 700);
// canvas.contextTop.fillStyle = 'green';
// canvas.contextTop.fillRect(mb.x, mb.y, 3, 3);
// canvas.contextTop.fillRect(bl.x, bl.y, 3, 3);
// canvas.contextTop.fillRect(br.x, br.y, 3, 3);
// canvas.contextTop.fillRect(tl.x, tl.y, 3, 3);
// canvas.contextTop.fillRect(tr.x, tr.y, 3, 3);
// canvas.contextTop.fillRect(ml.x, ml.y, 3, 3);
// canvas.contextTop.fillRect(mr.x, mr.y, 3, 3);
// canvas.contextTop.fillRect(mt.x, mt.y, 3, 3);
// canvas.contextTop.fillRect(mtr.x, mtr.y, 3, 3);
// }, 50);
// }
var coords = {
// corners
tl: tl, tr: tr, br: br, bl: bl,
};
if (!absolute) {
// middle
coords.ml = ml;
coords.mt = mt;
coords.mr = mr;
coords.mb = mb;
// rotating point
coords.mtr = mtr;
}
return coords;
}
fabric.Object.prototype.drawControls=function(ctx, styleOverride) {
styleOverride = styleOverride || {};
var wh = this._calculateCurrentDimensions(),
width = wh.x,
height = wh.y,
scaleOffset = styleOverride.cornerSize || this.cornerSize,
left = -(width + scaleOffset) / 2,
top = -(height + scaleOffset) / 2,
transparentCorners = typeof styleOverride.transparentCorners !== 'undefined' ?
styleOverride.transparentCorners : this.transparentCorners,
hasRotatingPoint = typeof styleOverride.hasRotatingPoint !== 'undefined' ?
styleOverride.hasRotatingPoint : this.hasRotatingPoint,
methodName = transparentCorners ? 'stroke' : 'fill';
ctx.save();
ctx.strokeStyle = ctx.fillStyle = styleOverride.cornerColor || this.cornerColor;
if (!this.transparentCorners) {
ctx.strokeStyle = styleOverride.cornerStrokeColor || this.cornerStrokeColor;
}
this._setLineDash(ctx, styleOverride.cornerDashArray || this.cornerDashArray, null);
// top-left
this._drawControl('tl', ctx, methodName,
left,
top, styleOverride);
// top-right
this._drawControl('tr', ctx, methodName,
left + width,
top, styleOverride);
// bottom-left
this._drawControl('bl', ctx, methodName,
left,
top + height, styleOverride);
// bottom-right
this._drawControl('br', ctx, methodName,
left + width,
top + height, styleOverride);
if (!this.get('lockUniScaling')) {
// middle-top
this._drawControl('mt', ctx, methodName,
left + width / 2,
top, styleOverride);
// middle-bottom
this._drawControl('mb', ctx, methodName,
left + width / 2,
top + height, styleOverride);
// middle-right
this._drawControl('mr', ctx, methodName,
left + width,
top + height / 2, styleOverride);
// middle-left
this._drawControl('ml', ctx, methodName,
left,
top + height / 2, styleOverride);
}
// middle-top-rotate
if (hasRotatingPoint) {
this._drawControl('mtr', ctx, methodName,
left + width / 2,
top + height + this.rotatingPointOffset, styleOverride);
}
ctx.restore();
return this;
}
fabric.Object.prototype.drawBorders=function(ctx, styleOverride) {
styleOverride = styleOverride || {};
var wh = this._calculateCurrentDimensions(),
strokeWidth = 1 / this.borderScaleFactor,
width = wh.x + strokeWidth,
height = wh.y + strokeWidth,
drawRotatingPoint = typeof styleOverride.hasRotatingPoint !== 'undefined' ?
styleOverride.hasRotatingPoint : this.hasRotatingPoint,
hasControls = typeof styleOverride.hasControls !== 'undefined' ?
styleOverride.hasControls : this.hasControls,
rotatingPointOffset = typeof styleOverride.rotatingPointOffset !== 'undefined' ?
styleOverride.rotatingPointOffset : this.rotatingPointOffset;
ctx.save();
ctx.strokeStyle = styleOverride.borderColor || this.borderColor;
this._setLineDash(ctx, styleOverride.borderDashArray || this.borderDashArray, null);
ctx.strokeRect(
-width / 2,
-height / 2,
width,
height
);
if (drawRotatingPoint && this.isControlVisible('mtr') && hasControls) {
var rotateHeight = height / 2;
ctx.beginPath();
ctx.moveTo(0, rotateHeight);
ctx.lineTo(0, rotateHeight + rotatingPointOffset);
ctx.stroke();
}
ctx.restore();
return this;
}
body {
background-color: ivory;
padding:20px;
}
#canvas {
border:1px solid red;
}
<canvas id="canvas" width="666" height="444"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.0.0/fabric.js"></script>
<script>
var obj = new fabric.Triangle({
fill: 'lime',
top: 110,
left: 110,
borderColor: 'red',
cornerColor: 'cyan',
cornerSize: 9,
transparentCorners: false
});
var canvas = new fabric.Canvas('canvas');
canvas.add(obj);
</script>

Fix Piechart label overlap using C3/D3

Based on my original Problem ( C3/D3 pie legend format / label overlap ) I have attempted to apply a bugfix originally created for flot to the C3 piechart.
In principal it seems to work, a collsion is detected and the label is being moved but the positioning seems to be incorrect.
Here ist some sample code to show the problem
var columns = ['data11', 'data2', 'data347', 'data40098', 'data777'];
var data = [150, 250, 300, 50, 50];
var colors = ['#0065A3', '#767670', '#D73648', '#7FB2CE', '#00345B'];
var padding = 5;
var chart = c3.generate({
bindto: d3.select('#chart'),
data: {
columns: [
[columns[0]].concat(data[0])
],
type: 'pie',
},
legend: {
position: 'right',
show: true
},
pie: {
label: {
threshold: 0.001,
format: function(value, ratio, id) {
return [id, d3.format(",.0f")(value), "[" + d3.format(",.1%")(ratio) + "]"].join(';');
}
}
},
color: {
pattern: colors
},
onrendered: function() {
redrawLabelBackgrounds();
}
});
function addLabelBackground(index) {
//get label text element
var textLabel = d3.select(".c3-target-" + columns[index] + " > text");
//add rect to parent
var labelNode = textLabel.node();
if (labelNode /*&& labelNode.innerHTML.length > 0*/ ) {
var p = d3.select(labelNode.parentNode).insert("rect", "text")
.style("fill", colors[index]);
}
}
for (var i = 0; i < columns.length; i++) {
if (i > 0) {
setTimeout(function(column) {
chart.load({
columns: [
[columns[column]].concat(data[column]),
]
});
//chart.data.names(columnNames[column])
addLabelBackground(column);
}, (i * 5000 / columns.length), i);
} else {
addLabelBackground(i);
}
}
function redrawLabelBackgrounds() {
function isOverlapping(pos1, pos2) {
/*isOverlapping = (x1min < x2max AND x2min < x1max AND y1min < y2max AND y2min < y1max)
/*
* x1min = pos1[0][0]
* x1max = pos1[0][1]
* y1min = pos1[1][0]
* y1max = pos1[1][1]
*
* x2min = pos2[0][0]
* x2max = pos2[0][1]
* y2min = pos2[1][0]
* y2max = pos2[1][1]
*
* isOverlapping = (pos1[0][0] < pos2[0][1] AND pos2[0][0] < pos1[0][1] AND pos1[1][0] < pos2[1][1] AND pos2[1][0] < pos1[1][1])
*/
return (pos1[0][0] < pos2[0][1] && pos2[0][0] < pos1[0][1] && pos1[1][0] < pos2[1][1] && pos2[1][0] < pos1[1][1]);
}
function getAngle(pos) {
//Q1 && Q4
if ((pos[0] > 0 && pos[1] >= 0) || (pos[0] > 0 && pos[1] > 0)) {
return Math.atan(pos[0] / pos[1]);
}
//Q2
else if (pos[0] < 0 && pos[1] >= 0) {
return Math.atan(pos[0] / pos[1]) + Math.PI;
}
//Q3
else if (pos[0] < 0 && pos[1] <= 0) {
return Math.atan(pos[0] / pos[1]) - Math.PI;
}
// x = 0, y>0
else if (pos[0] === 0 && pos[1] > 0) {
return Math.PI / 2;
}
// x = 0, y<0
else if (pos[0] === 0 && pos[1] < 0) {
return Math.PI / -2;
}
// x= 0, y = 0
else {
return 0;
}
}
//for all label texts drawn yet
var labelSelection = d3.select('#chart').selectAll(".c3-chart-arc > text");
//first put all label nodes in one array
var allLabels = [];
labelSelection.each(function() {
allLabels.push(d3.select(this));
});
//then check and modify labels
labelSelection.each(function(v) {
// get d3 node
var label = d3.select(this);
var labelNode = label.node();
//check if label is drawn
if (labelNode) {
var bbox = labelNode.getBBox();
var labelTextHeight = bbox.height;
if (labelNode.childElementCount === 0 && labelNode.innerHTML.length > 0) {
//build data
var data = labelNode.innerHTML.split(';');
label.text("");
data.forEach(function(i, n) {
label.append("tspan")
.text(i)
.attr("dy", (n === 0) ? 0 : "1.2em")
.attr("x", 0)
.attr("text-anchor", "middle");
}, label);
}
//check if element is visible
if (d3.select(labelNode.parentNode).style("display") !== 'none') {
//get pos of the label text
var labelPos = label.attr("transform").match(/-?\d+(\.\d+)?/g);
if (labelPos && labelPos.length === 2) {
//get surrounding box of the label
bbox = labelNode.getBBox();
// modify the labelPos of the text - check to make sure that the label doesn't overlap one of the other labels
// check to make sure that the label doesn't overlap one of the other labels - 4.3.2014 - from flot user fix pie_label_ratio on github
var newRadius = Math.sqrt(labelPos[0] * labelPos[0] + labelPos[1] * labelPos[1]);
var angle = getAngle(labelPos);
var labelBottom = (labelPos[1] - bbox.height / 2); //
var labelLeft = (labelPos[0] - bbox.width / 2); //
var bCollision = false;
var labelBox = [
[labelLeft, labelLeft + bbox.width],
[labelBottom, labelBottom + bbox.height]
];
var yix = 10; //max label reiterations with collisions
do {
for (var i = allLabels.length - 1; i >= 0; i--) {
if (!labelNode.isEqualNode(allLabels[i].node())) {
var checkLabelBBox = allLabels[i].node().getBBox();
var checkLabelPos = allLabels[i].attr("transform").match(/-?\d+(\.\d+)?/g);
var checkLabelBox = [
[(checkLabelPos[0] - checkLabelBBox.width / 2), (checkLabelPos[0] - checkLabelBBox.width / 2) + checkLabelBBox.width],
[(checkLabelPos[1] - checkLabelBBox.height / 2), (checkLabelPos[1] - checkLabelBBox.height / 2) + checkLabelBBox.height]
];
while (isOverlapping(labelBox, checkLabelBox)) {
newRadius -= 2;
if (newRadius < 0.00) {
break;
}
x = Math.round(Math.cos(angle) * newRadius);
y = Math.round(Math.sin(angle) * newRadius);
labelBottom = (y - bbox.height / 2);
labelLeft = (x - bbox.width / 2);
labelBox[0][0] = labelLeft;
labelBox[0][1] = labelLeft + bbox.width;
labelBox[1][0] = labelBottom;
labelBox[1][1] = labelBottom + bbox.height;
bCollision = true;
}
if (bCollision) break;
}
}
if (bCollision) bCollision = false;
else break;
yix--;
}
while (yix > 0);
//now apply the potentially corrected positions to the label
if (labelPos[0] !== (labelLeft + bbox.width / 2) || labelpos[1] !== (labelBottom + bbox.height / 2)) {
labelPos[0] = labelLeft + bbox.width / 2;
labelPos[1] = labelBottom + bbox.height / 2;
label.attr("transform", "translate(" + labelPos[0] + ',' + labelPos[1] + ")");
}
//now draw and move the rects
d3.select(labelNode.parentNode).select("rect")
.attr("transform", "translate(" + (labelLeft - padding) +
"," + (labelPos[1] - labelTextHeight / 2 - padding) + ")")
.attr("width", bbox.width + 2 * padding)
.attr("height", bbox.height + 2 * padding);
}
}
}
});
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/c3/0.6.9/c3.min.css" rel="stylesheet" />
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/c3/0.6.12/c3.min.js"></script>
<div id="chart">
</div>
Does anyone have an idea what is going wrong here?
Basically it was a combination of several things:
- calculating the angle (messed up arctan ;))
- checking the overlav of the rects (not only text boxes)
- wrong loops
This does work now:
var columns = ['data11', 'data2', 'data347', 'data40098', 'data777'];
var data = [150, 250, 300, 50, 50];
var colors = ['#0065A3', '#767670', '#D73648', '#7FB2CE', '#00345B'];
var padding = 5;
var chart = c3.generate({
bindto: d3.select('#chart'),
data: {
columns: [
[columns[0]].concat(data[0])
],
type: 'pie',
},
legend: {
position: 'right',
show: true
},
pie: {
label: {
threshold: 0.001,
format: function(value, ratio, id) {
return [id, d3.format(",.0f")(value), "[" + d3.format(",.1%")(ratio) + "]"].join(';');
}
}
},
color: {
pattern: colors
},
onrendered: function() {
redrawLabelBackgrounds();
}
});
function addLabelBackground(index) {
//get label text element
var textLabel = d3.select(".c3-target-" + columns[index] + " > text");
//add rect to parent
var labelNode = textLabel.node();
if (labelNode /*&& labelNode.innerHTML.length > 0*/ ) {
var p = d3.select(labelNode.parentNode).insert("rect", "text")
.style("fill", colors[index]);
}
}
for (var i = 0; i < columns.length; i++) {
if (i > 0) {
setTimeout(function(column) {
chart.load({
columns: [
[columns[column]].concat(data[column]),
]
});
//chart.data.names(columnNames[column])
addLabelBackground(column);
}, (i * 5000 / columns.length), i);
} else {
addLabelBackground(i);
}
}
function redrawLabelBackgrounds() {
function isOverlapping(pos1, pos2) {
/*isOverlapping = (x1min < x2max AND x2min < x1max AND y1min < y2max AND y2min < y1max)
/*
* x1min = pos1[0][0]
* x1max = pos1[0][1]
* y1min = pos1[1][0]
* y1max = pos1[1][1]
*
* x2min = pos2[0][0]
* x2max = pos2[0][1]
* y2min = pos2[1][0]
* y2max = pos2[1][1]
*
* isOverlapping = (pos1[0][0] < pos2[0][1] AND pos2[0][0] < pos1[0][1] AND pos1[1][0] < pos2[1][1] AND pos2[1][0] < pos1[1][1])
*/
return (pos1[0][0] < pos2[0][1] && pos2[0][0] < pos1[0][1] && pos1[1][0] < pos2[1][1] && pos2[1][0] < pos1[1][1]);
}
function getAngle(pos) {
//Q1
if ((pos[0] > 0 && pos[1] >= 0)) {
return Math.atan(pos[1] / pos[0]);
}
//Q2 & Q3
else if (pos[0] < 0) {
return Math.atan(pos[1] / pos[0]) + Math.PI;
} //Q4
else if (pos[0] > 0 && pos[1] < 0) {
return Math.atan(pos[1] / pos[0]) + 2 * Math.PI;
}
// x = 0, y>0
else if (pos[0] === 0 && pos[1] > 0) {
return Math.PI / 2;
}
// x = 0, y<0
else if (pos[0] === 0 && pos[1] < 0) {
return Math.PI / -2;
}
// x= 0, y = 0
else {
return 0;
}
}
//for all label texts drawn yet
var labelSelection = d3.select('#chart').selectAll(".c3-chart-arc > text");
//.filter(function(d){return d.style("display") !== 'none'});
//first put all label nodes in one array
var allLabels = [];
labelSelection.each(function() {
allLabels.push(d3.select(this));
});
//padding of the surrounding rect
var rectPadding = 1;
//then check and modify labels
labelSelection.each(function(v, labelIndex) {
// get d3 node
var label = d3.select(this);
var labelNode = label.node();
//check if label is drawn
if (labelNode) {
var bbox = labelNode.getBBox();
var labelTextHeight = bbox.height;
if (labelNode.childElementCount === 0 && labelNode.innerHTML.length > 0) {
//build data
var data = labelNode.innerHTML.split(';');
if (data.length > 1) {
label.html('')
.attr("dominant-baseline", "central")
.attr("text-anchor", "middle");
data.forEach(function(i, n) {
label.append("tspan")
.text(i)
.attr("dy", (n === 0) ? 0 : "1.2em")
.attr("x", 0);
}, label);
}
}
//check if element is visible
if (d3.select(labelNode.parentNode).style("display") !== 'none') {
//get pos of the label text
var labelPos = label.attr("transform").match(/-?\d+(\.\d+)?/g);
if (labelPos && labelPos.length === 2) {
labelPos[0] = parseFloat(labelPos[0]);
labelPos[1] = parseFloat(labelPos[1]);
//get surrounding box of the label
bbox = labelNode.getBBox();
// modify the labelPos of the text - check to make sure that the label doesn't overlap one of the other labels
// check to make sure that the label doesn't overlap one of the other labels - 4.3.2014 - from flot user fix pie_label_ratio on github
var oldRadius = Math.sqrt(labelPos[0] * labelPos[0] + labelPos[1] * labelPos[1]);
var newRadius = oldRadius;
var angle = getAngle(labelPos);
var labelBottom = (labelPos[1] - bbox.height / 2); //
var labelLeft = (labelPos[0] - bbox.width / 2); //
var bCollision = false;
var labelBox = [ [ labelLeft - rectPadding, labelLeft + bbox.width + rectPadding ], [ labelBottom-rectPadding, labelBottom + bbox.height + rectPadding ] ];
var yix = 10; //max label reiterations with collisions
do {
for (var i = labelIndex - 1; i >= 0; i--) {
var checkLabelNode = allLabels[i].node();
var checkLabelBBox = checkLabelNode.getBBox();
var checkLabelPos = allLabels[i].attr("transform").match(/-?\d+(\.\d+)?/g);
if (checkLabelBBox && checkLabelPos) { //element visible
checkLabelPos[0] = parseFloat(checkLabelPos[0]);
checkLabelPos[1] = parseFloat(checkLabelPos[1]);
//box is text bbox + padding from rect
var checkLabelBox = [
[(checkLabelPos[0] - checkLabelBBox.width / 2) - rectPadding, (checkLabelPos[0] + checkLabelBBox.width / 2) + rectPadding],
[(checkLabelPos[1] - checkLabelBBox.height / 2) - rectPadding, (checkLabelPos[1] + checkLabelBBox.height / 2) + rectPadding]
];
while (isOverlapping(labelBox, checkLabelBox)) {
newRadius -= 2;
if (newRadius < 0.00) {
bCollision = true;
break;
}
x = Math.round(Math.cos(angle) * newRadius);
y = Math.round(Math.sin(angle) * newRadius);
labelBottom = (y - bbox.height / 2);
labelLeft = (x - bbox.width / 2);
labelBox[0][0] = labelLeft;
labelBox[0][1] = labelLeft + bbox.width;
labelBox[1][0] = labelBottom;
labelBox[1][1] = labelBottom + bbox.height;
}
if (bCollision) break;
}
}
if (bCollision) bCollision = false;
else break;
yix--;
}
while (yix > 0);
//now apply the potentially corrected positions to the label
if (Math.round(labelPos[0]) !== Math.round(labelLeft + bbox.width / 2) || Math.round(labelPos[1]) !== Math.round(labelBottom + bbox.height / 2)) {
/*console.log("moving label[" + labelIndex + "][" + labelPos[0] + "," + labelPos[1] + "] to [" + (labelLeft + bbox.width / 2) + "," + (labelBottom + bbox.height / 2) + "] Radius " + oldRadius + "=>" + newRadius + " #" + yix);*/
labelPos[0] = labelLeft + bbox.width / 2;
labelPos[1] = labelBottom + bbox.height / 2;
label.attr("transform", "translate(" + labelPos[0] + ',' + labelPos[1] + ")");
}
//now draw and move the rects
d3.select(labelNode.parentNode).select("rect")
.attr("transform", "translate(" + (labelLeft - rectPadding) +
"," + (labelPos[1] - labelTextHeight / 2 - rectPadding) + ")")
.attr("width", bbox.width + 2 * rectPadding)
.attr("height", bbox.height + 2 * rectPadding);
}
}
}
});
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/c3/0.6.14/c3.min.css" rel="stylesheet" />
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/c3/0.6.14/c3.min.js"></script>
<div id="chart">
</div>
optional todo: hide labels still overlapping in the end

How to change the colour of straight line using Canvas?

I try to use a tools to change the colour of the line that I want to draw
The DIV is call Canvas
$(function() {
$.each(['#f00', '#ff0', '#0f0', '#0ff', '#00f', '#f0f', '#000', '#fff'], function() {
$('#tools').append("<a href='#canvas' data-color='" + this + "' style='width: 10px; background: " + this + ";'></a> ");
});
$.each([3, 5, 10, 15], function() {
$('#tools').append("<a href='#canvas' data-size='" + this + "' style='background: #ccc'>" + this + "</a> ");
});
});
This is the function for drawing line
document.getElementById('canvas').addEventListener('click', drawLine, false);
function getCursorPosition(e) {
var x;
var y;
if (e.pageX != undefined && e.pageY != undefined) {
x = e.pageX;
y = e.pageY;
} else {
x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
}
return [x, y];
}
function drawLine(e) {
context = this.getContext('2d');
x = getCursorPosition(e)[0] - this.offsetLeft;
y = getCursorPosition(e)[1] - this.offsetTop;
if (clicks != 1) {
clicks++;
} else {
context.beginPath();
context.moveTo(lastClick[0], lastClick[1]);
context.lineTo(x, y, 6);
context.strokeStyle = '#000000';
context.stroke();
clicks = 0;
}
lastClick = [x, y];
};
How can I change the stroke style by click on the colour on the tools?
You can do the following provided your canvas' context is available and pre-initialized on the global scope:
$('#tools').append("<a href='#' onclick=\"context.strokeStyle = '" + this + "';return false;\" style='width: 10px; background: " + this + ";'></a> ");
and remove
context.strokeStyle = '#000000';
This is however far from elegant, better add an event-listener and call a function to set the strokeStyle from that.
Also change to this to make context available globally:
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
canvas.addEventListener('click', drawLine, false);
//....
function drawLine(e) {
//context = this.getContext('2d'); //not here..
x = getCursorPosition(e)[0] - this.offsetLeft;
y = getCursorPosition(e)[1] - this.offsetTop;
//...

Mouseup event is intermittant from Scaled KineticJS layer

See here for example:
http://jsfiddle.net/tigz_uk/B8UDq/45/embedded/result/
Fiddle code:
http://jsfiddle.net/tigz_uk/B8UDq/45/
Most Relevant snippet:
function whenAreaSelected(stage, layer, image) {
var rect, down = false;
var eventObj = layer;
eventObj.off("mousedown");
eventObj.off("mousemove");
eventObj.off("mouseup");
eventObj.on("mousedown", function (e) {
console.log("Mousedown...");
if (rect) {
rect.remove();
}
var relativePos = getRelativePos ( stage, layer);
down = true;
var r = Math.round(Math.random() * 255),
g = Math.round(Math.random() * 255),
b = Math.round(Math.random() * 255);
rect = new Kinetic.Rect({
x: relativePos.x,
y: relativePos.y,
width: 11,
height: 1,
fill: 'rgb(' + r + ',' + g + ',' + b + ')',
stroke: 'black',
strokeWidth: 4,
opacity: 0.3
});
layer.add(rect);
});
eventObj.on("mousemove", function (e) {
if (!down) return;
var relativePos = getRelativePos ( stage, layer );
var p = rect.attrs;
rect.setWidth(relativePos.x - p.x);
rect.setHeight(relativePos.y - p.y);
layer.draw();
});
eventObj.on("mouseup", function (e) {
console.log("Mouse Up...");
down = false;
var p = rect.attrs;
var s = layer.getScale();
console.log("Rect x: " + p.x + " y: " + p.y + " width: " + p.width + " height: " + p.height + " sx: " + s.x + " sy: " + s.y);
});
}
var stageWidth = 1024;
var stageHeight = 700;
var imageWidth = 1299;
var imageHeight = 1064;
var initialScale = calcScale(imageWidth, imageHeight, stageWidth, stageHeight);
var stage = new Kinetic.Stage({
container: "canvas",
width: stageWidth,
height: stageHeight
});
var layer = new Kinetic.Layer();
var imageObj = new Image();
imageObj.onload = function () {
var diagram = new Kinetic.Image({
x: -500,
y: -500,
image: imageObj,
width: imageWidth,
height: imageHeight
});
layer.add(diagram);
layer.setScale(initialScale);
whenAreaSelected(stage, layer, diagram);
layer.draw();
}
var zoom = function (e) {
var zoomAmount = e.wheelDeltaY * 0.001;
layer.setScale(layer.getScale().x + zoomAmount)
layer.draw();
}
document.addEventListener("mousewheel", zoom, false);
stage.add(layer);
imageObj.src = 'https://dl.dropbox.com/u/746967/Serenity/MARAYA%20GA.png';
It seems to me as though the mouseup event is intermittent at best.
Any idea what's going on here? It also seems to be worse when the Image is offset rather than displayed at 0,0. And I think it relates to the scaling of the layer as it all works okay at scale 1.
Is this a kinetic bug?
Try using layer.drawScene() instead of layer.draw() in your mousemove handler
eventObj.on("mousemove", function (e) {
if (!down) return;
var relativePos = getRelativePos ( stage, layer );
var p = rect.attrs;
rect.setWidth(relativePos.x - p.x);
rect.setHeight(relativePos.y - p.y);
// try drawScene() instead of draw()
layer.drawScene();
});
[Edited based on info forwarded by from user814628 here: Binding MouseMove event causes inconsistency with mouse release event being fired

Resources