FabricJS: pan canvas while drawing - javascript

Here is the link to the codepen: https://codepen.io/Jsbbvk/pen/RwGBwOO
const edgePadding = 80;
const panSpeed = 5;
const expandCanvasEdge = (x, y) => {
let pan = {
x: 0,
y: 0,
};
const width = canvas.getWidth(),
height = canvas.getHeight();
if (x <= edgePadding) {
//left
const speedRatio = 1 - Math.max(0, x) / edgePadding;
pan.x = panSpeed * speedRatio;
} else if (x >= width - edgePadding) {
//right
const speedRatio =
1 - (width - Math.min(width, x)) / edgePadding;
pan.x = -panSpeed * speedRatio;
}
if (y <= edgePadding) {
//top
const speedRatio = 1 - Math.max(0, y) / edgePadding;
pan.y = panSpeed * speedRatio;
} else if (y >= height - edgePadding) {
//bottom
const speedRatio =
1 - (height - Math.min(height, y)) / edgePadding;
pan.y = -panSpeed * speedRatio;
}
if (pan.x || pan.y) {
canvas.relativePan(new fabric.Point(pan.x, pan.y));
}
}
canvas.on('mouse:move', function(opt) {
if (this.isMouseDown && this.isDrawingMode) {
let {x, y} = canvas.getPointer(opt.e, true);
expandCanvasEdge(x, y);
}
if (!this.isDrawingMode && this.isDragging) {
//panning
var e = opt.e;
var vpt = this.viewportTransform;
vpt[4] += e.clientX - this.lastPosX;
vpt[5] += e.clientY - this.lastPosY;
this.requestRenderAll();
this.lastPosX = e.clientX;
this.lastPosY = e.clientY;
}
});
In the demo, when you draw close to the edge of the canvas, the canvas should pan to allow more drawing space.
However, while the panning is happening, the drawing (path) is static on the canvas; it doesn't stretch as the canvas pans.
Is there a way to fix this issue?

I did some deep research for you and found a few examples.
You can overcome this situation by using the relativePan function.
One of the examples I have found:
function startPan(event) {
if (event.button != 2) {
return;
}
var x0 = event.screenX,
y0 = event.screenY;
function continuePan(event) {
var x = event.screenX,
y = event.screenY;
fc.relativePan({ x: x - x0, y: y - y0 });
x0 = x;
y0 = y;
}
function stopPan(event) {
$(window).off('mousemove', continuePan);
$(window).off('mouseup', stopPan);
};
$(window).mousemove(continuePan);
$(window).mouseup(stopPan);
$(window).contextmenu(cancelMenu);
};
function cancelMenu() {
$(window).off('contextmenu', cancelMenu);
return false;
}
$(canvasWrapper).mousedown(startPan);
You can determine a roadmap by examining the resources and demos here.
JSFiddle demo https://jsfiddle.net/tornado1979/up48rxLs/

Related

How to limit this move function on zoomed image without exceed the image, I make limits at 0x,0y and I need the same at x,y max limits

How can I limit this move function on zoomed image without exceed the image, I make limits at 0x,0y and I need the same at x,y max limits
var scale = 1,
panning = false,
pointX = 0,
pointY = 0,
start = { x: 0, y: 0 },
zoom = document.getElementById("zoom");
function setTransform() {
zoom.style.transform = "translate(" + pointX + "px, " + pointY + "px) scale(" + scale + ")";
}
function centerMap() {
if(pointY>=0){
pointY = 0;
}
if(pointX>=0){
pointX = 0;
}
var offX= document.getElementById("img").offsetWidth
var offY = document.getElementById("img").offsetHeight
}
zoom.onmousedown = function (e) {
e.preventDefault();
start = { x: e.clientX - pointX, y: e.clientY - pointY };
panning = true;
}
zoom.onmouseup = function (e) {
panning = false;
}
zoom.onmousemove = function (e) {
console.log(e);
e.preventDefault();
if (!panning) {
return;
}
pointX = (e.clientX - start.x);
pointY = (e.clientY - start.y);
centerMap();
console.log(pointX,pointY, e.clientY,scale,);
setTransform();
}
zoom.onwheel = function (e) {
e.preventDefault();
var xs = (e.clientX - pointX) / scale,
ys = (e.clientY - pointY) / scale,
delta = (e.wheelDelta ? e.wheelDelta : -e.deltaY);
(delta > 0) ? (scale += 0.2) : (scale -= 0.2);
if(scale<1){
scale=1;
}
pointX = e.clientX - xs * scale;
pointY = e.clientY - ys * scale;
centerMap();
setTransform();
}
This is the JS code.
In function centerMap I move the image auto to not seeing the white background, but in right and bottom I didn't find the solve, I tried to make calculations with image coords, but I need a resolvation in any resolutions
Ex: https://shorturl.at/jlpw2

HTML5 Canvas with Apple Pen - Only works with (position: absolute;)

I'm using this code to enable Apple Pencil to work on HTML5 Canvas. It works perfectly with it's default css but the moment I change position:absolute it stops working. The pen strokes do not line up with what's drawn on the screen.
I've been trying for two days and I can't seem to wrap my head around why this would happen.
I think it might have something to do with this:
canvas.width = window.innerWidth * 2
canvas.height = window.innerHeight * 2
Any help would be much appreciated.
Link to code: https://github.com/shuding/apple-pencil-safari-api-test
CSS (canvas)
canvas {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
}
Code for pen:
const $force = document.querySelectorAll('#force')[0]
const $touches = document.querySelectorAll('#touches')[0]
const canvas = document.querySelectorAll('canvas')[0]
const context = canvas.getContext('2d')
let lineWidth = 0
let isMousedown = false
let points = []
canvas.width = window.innerWidth * 2
canvas.height = window.innerHeight * 2
const strokeHistory = []
const requestIdleCallback = window.requestIdleCallback || function (fn) { setTimeout(fn, 1) };
/**
* This function takes in an array of points and draws them onto the canvas.
* #param {array} stroke array of points to draw on the canvas
* #return {void}
*/
function drawOnCanvas (stroke) {
context.strokeStyle = 'black'
context.lineCap = 'round'
context.lineJoin = 'round'
const l = stroke.length - 1
if (stroke.length >= 3) {
const xc = (stroke[l].x + stroke[l - 1].x) / 2
const yc = (stroke[l].y + stroke[l - 1].y) / 2
context.lineWidth = stroke[l - 1].lineWidth
context.quadraticCurveTo(stroke[l - 1].x, stroke[l - 1].y, xc, yc)
context.stroke()
context.beginPath()
context.moveTo(xc, yc)
} else {
const point = stroke[l];
context.lineWidth = point.lineWidth
context.strokeStyle = point.color
context.beginPath()
context.moveTo(point.x, point.y)
context.stroke()
}
}
/**
* Remove the previous stroke from history and repaint the entire canvas based on history
* #return {void}
*/
function undoDraw () {
strokeHistory.pop()
context.clearRect(0, 0, canvas.width, canvas.height)
strokeHistory.map(function (stroke) {
if (strokeHistory.length === 0) return
context.beginPath()
let strokePath = [];
stroke.map(function (point) {
strokePath.push(point)
drawOnCanvas(strokePath)
})
})
}
for (const ev of ["touchstart", "mousedown"]) {
canvas.addEventListener(ev, function (e) {
let pressure = 0.1;
let x, y;
if (e.touches && e.touches[0] && typeof e.touches[0]["force"] !== "undefined") {
if (e.touches[0]["force"] > 0) {
pressure = e.touches[0]["force"]
}
x = e.touches[0].pageX * 2
y = e.touches[0].pageY * 2
} else {
pressure = 1.0
x = e.pageX * 2
y = e.pageY * 2
}
isMousedown = true
lineWidth = Math.log(pressure + 1) * 40
context.lineWidth = lineWidth// pressure * 50;
points.push({ x, y, lineWidth })
drawOnCanvas(points)
})
}
for (const ev of ['touchmove', 'mousemove']) {
canvas.addEventListener(ev, function (e) {
if (!isMousedown) return
e.preventDefault()
let pressure = 0.1
let x, y
if (e.touches && e.touches[0] && typeof e.touches[0]["force"] !== "undefined") {
if (e.touches[0]["force"] > 0) {
pressure = e.touches[0]["force"]
}
x = e.touches[0].pageX * 2
y = e.touches[0].pageY * 2
} else {
pressure = 1.0
x = e.pageX * 2
y = e.pageY * 2
}
// smoothen line width
lineWidth = (Math.log(pressure + 1) * 40 * 0.2 + lineWidth * 0.8)
points.push({ x, y, lineWidth })
drawOnCanvas(points);
requestIdleCallback(() => {
$force.textContent = 'force = ' + pressure
const touch = e.touches ? e.touches[0] : null
if (touch) {
$touches.innerHTML = `
touchType = ${touch.touchType} ${touch.touchType === 'direct' ? '👆' : '✍️'} <br/>
radiusX = ${touch.radiusX} <br/>
radiusY = ${touch.radiusY} <br/>
rotationAngle = ${touch.rotationAngle} <br/>
altitudeAngle = ${touch.altitudeAngle} <br/>
azimuthAngle = ${touch.azimuthAngle} <br/>
`
}
})
})
}
for (const ev of ['touchend', 'touchleave', 'mouseup']) {
canvas.addEventListener(ev, function (e) {
let pressure = 0.1;
let x, y;
if (e.touches && e.touches[0] && typeof e.touches[0]["force"] !== "undefined") {
if (e.touches[0]["force"] > 0) {
pressure = e.touches[0]["force"]
}
x = e.touches[0].pageX * 2
y = e.touches[0].pageY * 2
} else {
pressure = 1.0
x = e.pageX * 2
y = e.pageY * 2
}
isMousedown = false
requestIdleCallback(function () { strokeHistory.push([...points]); points = []})
lineWidth = 0
})
};

How to fix Object Position in Canvas Drag and Drop

I am implementing Canvas Drag and Drop Functionality in ReactJs. Problem is that the Object is always centered under the pointer. To initiate the drag’n’drop can we mousedown anywhere on the Object. If do it at the edge, then the Object suddenly “jumps” to become centered. Here is my Code...
var rect = this.props.refs.svg.getBoundingClientRect();
const { xUnit, yUnit } = CanvasDimensions(
rect,
this.props.canvasWidth,
this.props.canvasHeight,
e.clientX,
e.clientY
);
if (this.state.isDragging) {
var tempELement = this.props.conceptItemsList.findIndex(
item => item.sys.id === this.state.DraggingId
);
var ElementWidth = this.props.conceptItemsList[tempELement].width;
var ElementHeight = this.props.conceptItemsList[tempELement].height;
if (this.props.conceptItemsList[tempELement].__typename === "Text") {
ElementWidth =
this.props.refs[this.state.DraggingId].getBoundingClientRect().width /
xUnit;
}
const { X, Y } = CanvasDimensions(
rect,
this.props.canvasWidth,
this.props.canvasHeight,
e.clientX,
e.clientY,
ElementWidth,
ElementHeight
);
if (
Y + this.props.conceptItemsList[tempELement].height >
this.props.CanvasHeight
) {
this.props.setCanvasHeight(
Y + this.props.conceptItemsList[tempELement].height
);
}
this.props.conceptItemsList[tempELement].x = X;
this.props.conceptItemsList[tempELement].y = Y;
this.props.SetDragging(this.props.conceptItemsList[tempELement]);
}
export const CanvasDimensions = (
rect,
canvasWidth,
canvasHeight,
clientX,
clientY,
width = ImageWidth,
height = ImageHeight
) => {
const xUnit = rect.width / canvasWidth;
const yUnit = rect.height / canvasHeight;
const canvasX = clientX - rect.left;
const canvasY = clientY - rect.top;
var X = canvasX / xUnit;
var Y = canvasY / yUnit;
X = X < width / 2 ? X - X : X - width / 2;
Y = Y < height / 2 ? Y - Y : Y - height / 2;
if (X + width / 1.7 > canvasWidth) {
X = X - width / 2;
}
return { X, Y, xUnit, yUnit };
};

detecting a click inside a hexagon drawn using canvas?

I'm working on a certain layout where I need to draw a hexagon which needs to be clickable. I'm using the Path2D construct and isPointInPath function. I'm constructing an animation where a number of hexagons is created and then each moved to a certain position. After the movement is done, I am attaching onclick event handlers to certain hexagons. However there is weird behaviour.
Some initialized variables
const COLOR_DARK = "#73b6c6";
const COLOR_LIGHT = "#c3dadd";
const COLOR_PRIMARY = "#39a4c9";
const TYPE_PRIMARY = 'primary';
let hexagons = [];
Below is the function which draws the hexagons.
function drawHex(ctx, x, y, hexProps, stroke, color) {
let myPath = new Path2D();
myPath.moveTo(x + hexProps.width*0.5, y);
myPath.lineTo(x, y + hexProps.height*hexProps.facShort);
myPath.lineTo(x, y + hexProps.height*hexProps.facLong);
myPath.lineTo(x + hexProps.width*0.5, y + hexProps.height);
myPath.lineTo(x + hexProps.width, y + hexProps.height*hexProps.facLong);
myPath.lineTo(x + hexProps.width, y + hexProps.height*hexProps.facShort);
myPath.lineTo(x + hexProps.width*0.5, y);
myPath.closePath();
if (stroke){
ctx.strokeStyle = color;
ctx.stroke(myPath);
} else {
ctx.fillStyle = color;
ctx.fill(myPath);
}
return myPath;
}
This function populates the hexagon array
function populateLeftHex(canvasWidth, canvasHeight, hexProps) {
const startX = canvasWidth / 2;
const startY = canvasHeight / 2;
const baseLeft = canvasWidth * 0.05;
for(let i = 0; i < 5; i++){
let hexNumber = (i % 4 == 0)? 2: 1;
for(let j = 0; j < hexNumber; j++){
hexagons.push({
startX: startX,
startY: startY,
endX: baseLeft + (2 * j) + ((i % 2 == 0)? (hexProps.width * j) : (hexProps.width/2)),
endY: ((i + 1) * hexProps.height) - ((i) * hexProps.height * hexProps.facShort) + (i* 2),
stroke: true,
color: ( i % 2 == 0 && j % 2 == 0)? COLOR_DARK : COLOR_LIGHT,
type: TYPE_PRIMARY
});
}
}
}
And here is where Im calling the isPointInPath function.
window.onload = function (){
const c = document.getElementById('canvas');
const canvasWidth = c.width = window.innerWidth,
canvasHeight = c.height = window.innerHeight,
ctx = c.getContext('2d');
window.requestAnimFrame = (function (callback) {
return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function (callback) {
window.setTimeout(callback, 1000 / 60);
};
})();
console.log(canvasWidth);
let hexProps = {
width: canvasWidth * 0.075,
get height () {
return this.width/Math.sqrt(3) + (1.5)*(this.width/Math.sqrt(2)/2);
} ,
facShort: 0.225,
get facLong () {
return 1 - this.facShort;
}
};
populateLeftHex(canvasWidth, canvasHeight, hexProps);
let pct = 0;
const fps = 200;
animate();
function animate () {
setTimeout(function () {
// increment pct towards 100%
pct += .03;
// if we're not done, request another animation frame
if (pct < 1.00) {
requestAnimFrame(animate);
} else { //if pct is no longer less than 1.00, then the movement animation is over.
hexagons.forEach(function (hex) {
if(hex.type === TYPE_PRIMARY) {
console.info(hex.path);
c.onclick = function(e) {
let x = e.clientX - c.offsetLeft,
y = e.clientY - c.offsetTop;
console.info(ctx.isPointInPath(hex.path, (e.clientX - c.offsetLeft), (e.clientY - c.offsetTop) ));
};
}
})
}
ctx.clearRect(0, 0, c.width, c.height);
// draw all hexagons
for ( let i = 0; i < hexagons.length; i++) {
// get reference to next shape
let hex = hexagons[i];
// note: dx/dy are fixed values
// they could be put in the shape object for efficiency
let dx = hex.endX - hex.startX;
let dy = hex.endY - hex.startY;
let nextX = hex.startX + dx * pct;
let nextY = hex.startY + dy * pct;
hex = hexagons[i];
ctx.fillStyle = hex.color;
hex.path = drawHex(ctx, nextX, nextY, hexProps, hex.stroke, hex.color);
}
}, 1000 / fps);
}
Can you help me figure out what I'm doing wrong? Maybe I misunderstood how Path2D works? Thanks in advance.
Had to do a bit of work to build a test page as your example is incomplete, but this is working for me - though my hexagon is concave...
var myCanvas = document.getElementById("myCanvas");
var ctx = myCanvas.getContext("2d");
var hexProps = {width:100, height:100, facShort:-2, facLong:10};
var hexagons = [];
function drawHex(ctx, x, y, hexProps, stroke, color) {
let myPath = new Path2D();
myPath.moveTo(x + hexProps.width*0.5, y);
myPath.lineTo(x, y + hexProps.height*hexProps.facShort);
myPath.lineTo(x, y + hexProps.height*hexProps.facLong);
myPath.lineTo(x + hexProps.width*0.5, y + hexProps.height);
myPath.lineTo(x + hexProps.width, y + hexProps.height*hexProps.facLong);
myPath.lineTo(x + hexProps.width, y + hexProps.height*hexProps.facShort);
myPath.lineTo(x + hexProps.width*0.5, y);
myPath.closePath();
if (stroke){
ctx.strokeStyle = color;
ctx.stroke(myPath);
} else {
ctx.fillStyle = color;
ctx.fill(myPath);
}
return myPath;
}
hexagons.push({type:0, path:drawHex(ctx,100,100,hexProps,false,"#0f0")});
hexagons.forEach(function (hex) {
if(hex.type === 0) {
console.info(hex.path);
myCanvas.onclick = function(e) {
let x = e.clientX - myCanvas.offsetLeft,
y = e.clientY - myCanvas.offsetTop;
console.info(x,y);
console.info(ctx.isPointInPath(hex.path, (e.clientX -
myCanvas.offsetLeft), (e.clientY - myCanvas.offsetTop) ));
};
}
})
<canvas width=500 height=500 id=myCanvas style='border:1px solid red'></canvas>
Test clicks give true and false where expected:
test.htm:48 165 168
test.htm:49 true
test.htm:48 151 336
test.htm:49 false
test.htm:48 124 314
test.htm:49 true
test.htm:48 87 311
test.htm:49 false

Ease in/out camera movement for canvas

I have a function which moves a camera to a position that the user clicks on the canvas. But currently the movement is linear. I would like it to use an ease in/out transition but am having difficulty understanding how to implement it with my current function.
This is the code I have:
animate.mouseEvent = function(e,el){
var mousePos = mouse.relativePosition(e,el);
var parent = el;
if(e.button == 0){
function update(){
if(centerX == mousePos.x && centerY == mousePos.y){
clearInterval(parent.timer);
//center X / Y is canvas width & height halved
}
var difx = centerX - mousePos.x,
dify = centerY - mousePos.y,
distance = Math.sqrt(difx*difx + dify*dify),
normalX = difx/distance,
normalY = dify/distance,
speed = 1, //currently linear [replace with ease in/out]
x = normalX * speed,
y = normalY * speed;
updateOffsets(x,y);
mousePos.x += x;
mousePos.y += y;
}
parent.timer = setInterval(update,1);
}
}
animate.updateOffsets = function(x,y){
canvas.offsetX -= x;
canvas.offsetY -= y;
}
So i was wondering how i would implement an ease in/out from my current linear method. Using the below function:
Math.easeInOutQuad = function (t, b, c, d) {
t /= d/2;
if (t < 1) return c/2*t*t + b;
t--;
return -c/2 * (t*(t-2) - 1) + b;
};
I don't fully know how to implement it or what exactly it would be returning for me to calculate the new offset with. Was hoping some one could explain.
This should give you an idea. I used another easing function, you can substitute your own. Example
var element = document.getElementById('moving');
var bg = document.getElementById('background');
bg.addEventListener('click', function(evt) {
animate({
x: evt.offsetX,
y: evt.offsetY
});
}, false);
function getPosition() {
return {
x: element.offsetLeft,
y: element.offsetTop
};
}
function setPosition(x, y) {
element.style.left = x + 'px';
element.style.top = y + 'px';
}
function easing(x) {
return 0.5 + 0.5 * Math.sin((x - 0.5) * Math.PI);
}
function animate(target) {
var initial = getPosition();
var initialX = initial.x;
var initialY = initial.y;
var targetX = target.x;
var targetY = target.y;
var deltaX = targetX - initialX;
var deltaY = targetY - initialY;
var timeStart = timestamp();
var timeLength = 800;
var timer = setInterval(update, 10);
function timestamp() {
return Date.now();
}
function stop() {
clearInterval(timer);
}
function update() {
var t = (timestamp() - timeStart) / timeLength;
if (t > 1) {
fraction(1);
stop();
} else {
fraction(easing(t));
}
}
function fraction(t) {
setPosition(initialX + t * deltaX, initialY + t * deltaY);
}
}
EDIT
Applied to the fiddle you provided:
var element = document.getElementById('background');
var ctx = element.getContext("2d");
var camera = {};
// camera.offset marks the position seen at the upper left corner
// (It would be better if it marked the center)
// Let these be negative, so (0,0) scene position is at the center of the image
camera.offsetX = -element.width/2;
camera.offsetY = -element.height/2;
var obj = {};
obj.x = 50;
obj.y = 50;
element.addEventListener('click', animate, false);
function easing(x) {
return 0.5 + 0.5 * Math.sin((x - 0.5) * Math.PI);
}
function animate(evt){
// Transform click position from screen coordinates to scene coordinates
var targetX = evt.offsetX + camera.offsetX - element.width / 2,
targetY = evt.offsetY + camera.offsetY - element.height / 2;
var initialX = camera.offsetX,
initialY = camera.offsetY;
var deltaX = targetX - initialX,
deltaY = targetY - initialY;
var timeStart = Date.now();
var timeLength = 800;
var timer = setInterval(update, 10);
function stop(){
clearInterval(timer);
}
function update(){
var t = (Date.now() - timeStart) / timeLength;
if (t > 1) {
fraction(1);
stop();
} else {
fraction(easing(t));
}
}
function fraction(t){
camera.offsetX = initialX + t * deltaX,
camera.offsetY = initialY + t * deltaY;
}
}
function draw(){
ctx.clearRect(0,0,element.width,element.height);
ctx.fillStyle = 'red';
ctx.fillRect((element.width/2)-2,(element.height/2)-2,4,4);
ctx.fillStyle = 'blue';
// ===> Here the size and position arguments were swapped
ctx.fillRect(obj.x-camera.offsetX,obj.y-camera.offsetY, 20, 20);
}
// Consider using requestAnimationFrame
setInterval(draw, 10);

Categories