How can I draw a right triangle with fabric js? - javascript

function normalTriangle() {
return new fabric.Triangle({
transparentCorners: false,
objectCaching: false,
shapeType: 'normalTriangle'
})
}
canvas = new fabric.Canvas(canvasElement, {
selection: true
preserveObjectStacking: true,
renderOnAddRemove: false,
uniformScaling: false
}).setWidth(basicWidth).setHeight(basicHeight);
canvas.on({
'mouse:over': CanvasMouseOver,
'mouse:down:before': CanvasMouseDownBefore,
'mouse:down': CanvasMouseDown,
'mouse:move': CanvasMouseMove,
'mouse:up:before': CanvasMouseUpBefore,
'mouse:up': CanvasMouseUp,
'mouse:dblclick': CanvasDoubleClick,
'object:scaling': CanvasObjectScaling,
'selection:created': CanvasSelectionCreated,
'selection:cleared': CanvasSelectionCleared
});
function CanvasMouseOver(evt) {
if (shapeType || lineType) {
canvas.defaultCursor = 'crosshair';
}
}
function CanvasMouseDownBefore(evt) {
if (shapeType || lineType) {
canvas.selection = false;
}
}
function CanvasMouseDown(evt) {
canvasDown = true;
let pointer = canvas.getPointer(evt.e);
if (shapeType || lineType) {
drawingStartX = pointer.x;
drawingStartY = pointer.y;
drawingObj = DrawingCombination(shapeType, lineType);
if (!drawingObj) return;
drawingObj.left = drawingStartX;
drawingObj.top = drawingStartY;
drawingObj.width = pointer.x - drawingStartX;
drawingObj.height = pointer.y - drawingStartY;
canvas.add(drawingObj);
}
}
function CanvasMouseMove(evt) {
if ((shapeType || lineType) && canvasDown && !canvas.getActiveObject()) {
let pointer = canvas.getPointer(evt.e);
if (!drawingObj) return;
if (drawingStartX > pointer.x) {
drawingObj.set({
left: Math.abs(pointer.x)
});
}
if (drawingStartY > pointer.y) {
drawingObj.set({
top: Math.abs(pointer.y)
});
}
drawingObj.set({
width: Math.abs(drawingStartX - pointer.x)
});
drawingObj.set({
height: Math.abs(drawingStartY - pointer.y)
});
canvas.renderAll();
}
}
function CanvasMouseUpBefore(evt) {
if ((shapeType || lineType) && canvasDown) {
if (!drawingObj) return;
if (drawingObj.width == 0 && drawingObj.height == 0) {
canvas.remove(drawingObj);
} else {
drawingObj.setCoords();
}
drawingObj = null;
drawingStartX = null;
drawingStartY = null;
}
}
function CanvasMouseUp(evt) {
canvasDown = false;
}
I would like to draw a right triangle using the triangle of fabric js. Is there a way?
I use canvas using drag function.
Among the figures that can be resized using a drag from the starting point of the mouse click, want a right triangle with 90 degrees.
function rightTriangle() {
return new fabric.Polygon([{x:0,y:100}, {x:100, y:0}, {x:0, y:0}],{
fill: document.querySelector('.SelectionFillColor').value,
stroke: document.querySelector('.SelectionStrokeColor').value,
transparentCorners: true,
objectCaching: false,
shapeType: 'rightTriangle',
})
}
I tried using polygons, but there was a problem that the shape and the outside size did not match.

override fabric.Rect.
I used "Polygon" to draw the shape and adjust the "Scale" to change the size
There was a problem with "Bounding Box" leaving, so I couldn't solve it by myself.
In the end, we borrowed the Override presented in the link above and proceeded.
However, this method does not cause a reversal of the shape at the start point when the first drag attempt is made.

Related

Freeze scrollbar

I create a game using javascript. The code is placed at the bottom of the page. Visualize it as if the game is below a footer. The game starts when the user selects the start game button. The mechanics of the game are controlled by the arrow key. Arrow keys are used to move up, right, and left down. The problem is when I press the up or down key, the page scrolls too. How to stop this? I want the page not to scroll when the game is active. I am attaching the code of the game I create. It is a snake game.There's CSS too, but I'm not attaching it as I don't think its relevant
<div id="app" class="app">
<div class="start-screen">
<h2>🦊 </h2>
<div class="options">
<h3>Choose Difficulty</h3>
<p class="end-score"></p>
<button data-difficulty="100" class="active">Easy</button>
<button data-difficulty="75">Medium</button>
<button data-difficulty="50">Hard</button>
</div>
<button class="play-btn">Play</button>
</div>
<canvas id="board"></canvas>
<div class="score">0</div>
</div>
<script>
class SnakeGame {
constructor() {
this.$app = document.querySelector('#app');
this.$canvas = this.$app.querySelector('canvas');
this.ctx = this.$canvas.getContext('2d');
this.$startScreen = this.$app.querySelector('.start-screen');
this.$score = this.$app.querySelector('.score');
this.settings = {
canvas: {
width: 500,
height: 500,
background: 'white',
border: 'black'
},
snake: {
size: 20,
background: '#73854A',
border: '#000'
}
};
this.game = {
// "direction" (set in setUpGame())
// "nextDirection" (set in setUpGame())
// "score" (set in setUpGame())
speed: 100,
keyCodes: {
38: 'up',
40: 'down',
39: 'right',
37: 'left'
}
}
this.soundEffects = {
score: new Audio('./sounds/score.mp3'),
gameOver: new Audio('./sounds/game-over.mp3')
};
this.setUpGame();
this.init();
}
init() {
// Choose difficulty
// Rather than using "this.$startScreen.querySelectorAll('button')" and looping over the node list
// and attaching seperate event listeners on each item, it's more efficient to just listen in on the container and run a check at runtime
this.$startScreen.querySelector('.options').addEventListener('click', event => {
this.chooseDifficulty(event.target.dataset.difficulty);
});
// Play
this.$startScreen.querySelector('.play-btn').addEventListener('click', () => {
this.startGame();
});
}
chooseDifficulty(difficulty) {
if(difficulty) {
this.game.speed = difficulty;
this.$startScreen.querySelectorAll('.options button').forEach(btn => btn.classList.remove('active'));
event.target.classList.add('active');
}
}
setUpGame() {
// The snake starts off with 5 pieces
// Each piece is 30x30 pixels
// Each following piece must be n times as far from the first piece
const x = 300;
const y = 300;
this.snake = [
{ x: x, y: y },
{ x: x - this.settings.snake.size, y: y },
{ x: x - (this.settings.snake.size * 2), y: y },
{ x: x - (this.settings.snake.size * 3), y: y },
{ x: x - (this.settings.snake.size * 4), y: y }
];
this.food = {
active: false,
background: '#EC5E0B',
border: '#73AA24',
coordinates: {
x: 0,
y: 0
}
};
this.game.score = 0;
this.game.direction = 'right';
this.game.nextDirection = 'right';
}
startGame() {
// Stop the game over sound effect if a new game was restarted quickly before it could end
this.soundEffects.gameOver.pause();
this.soundEffects.gameOver.currentTime = 0;
// Reset a few things from the prior game
this.$app.classList.add('game-in-progress');
this.$app.classList.remove('game-over');
this.$score.innerText = 0;
this.generateSnake();
this.startGameInterval = setInterval(() => {
if(!this.detectCollision()) {
this.generateSnake();
} else {
this.endGame();
}
}, this.game.speed);
// Change direction
document.addEventListener('keydown', event => {
this.changeDirection(event.keyCode);
});
}
changeDirection(keyCode) {
const validKeyPress = Object.keys(this.game.keyCodes).includes(keyCode.toString()); // Only allow (up|down|left|right)
if(validKeyPress && this.validateDirectionChange(this.game.keyCodes[keyCode], this.game.direction)) {
this.game.nextDirection = this.game.keyCodes[keyCode];
}
}
// When already moving in one direction snake shouldn't be allowed to move in the opposite direction
validateDirectionChange(keyPress, currentDirection) {
return (keyPress === 'left' && currentDirection !== 'right') ||
(keyPress === 'right' && currentDirection !== 'left') ||
(keyPress === 'up' && currentDirection !== 'down') ||
(keyPress === 'down' && currentDirection !== 'up');
}
resetCanvas() {
// Full screen size
this.$canvas.width = this.settings.canvas.width;
this.$canvas.height = this.settings.canvas.height;
this.$canvas.style.border = `3px solid ${this.settings.canvas.border}`;
// Background
this.ctx.fillStyle = this.settings.canvas.background;
this.ctx.fillRect(0, 0, this.$canvas.width, this.$canvas.height);
}
generateSnake() {
let coordinate;
switch(this.game.direction) {
case 'right':
coordinate = {
x: this.snake[0].x + this.settings.snake.size,
y: this.snake[0].y
};
break;
case 'up':
coordinate = {
x: this.snake[0].x,
y: this.snake[0].y - this.settings.snake.size
};
break;
case 'left':
coordinate = {
x: this.snake[0].x - this.settings.snake.size,
y: this.snake[0].y
};
break;
case 'down':
coordinate = {
x: this.snake[0].x,
y: this.snake[0].y + this.settings.snake.size
};
}
// The snake moves by adding a piece to the beginning "this.snake.unshift(coordinate)" and removing the last piece "this.snake.pop()"
// Except when it eats the food in which case there is no need to remove a piece and the added piece will make it grow
this.snake.unshift(coordinate);
this.resetCanvas();
const ateFood = this.snake[0].x === this.food.coordinates.x && this.snake[0].y === this.food.coordinates.y;
if(ateFood) {
this.food.active = false;
this.game.score += 10;
this.$score.innerText = this.game.score;
this.soundEffects.score.play();
} else {
this.snake.pop();
}
this.generateFood();
this.drawSnake();
}
drawSnake() {
const size = this.settings.snake.size;
this.ctx.fillStyle = this.settings.snake.background;
this.ctx.strokestyle = this.settings.snake.border;
// Draw each piece
this.snake.forEach(coordinate => {
this.ctx.fillRect(coordinate.x, coordinate.y, size, size);
this.ctx.strokeRect(coordinate.x, coordinate.y, size, size);
});
this.game.direction = this.game.nextDirection;
}
generateFood() {
// If there is uneaten food on the canvas there's no need to regenerate it
if(this.food.active) {
this.drawFood(this.food.coordinates.x, this.food.coordinates.y);
return;
}
const gridSize = this.settings.snake.size;
const xMax = this.settings.canvas.width - gridSize;
const yMax = this.settings.canvas.height - gridSize;
const x = Math.round((Math.random() * xMax) / gridSize) * gridSize;
const y = Math.round((Math.random() * yMax) / gridSize) * gridSize;
// Make sure the generated coordinates do not conflict with the snake's present location
// If so recall this method recursively to try again
this.snake.forEach(coordinate => {
const foodSnakeConflict = coordinate.x == x && coordinate.y == y;
if(foodSnakeConflict) {
this.generateFood();
} else {
this.drawFood(x, y);
}
});
}
drawFood(x, y) {
const size = this.settings.snake.size;
this.ctx.fillStyle = this.food.background;
this.ctx.strokestyle = this.food.border;
this.ctx.fillRect(x, y, size, size);
this.ctx.strokeRect(x, y, size, size);
this.food.active = true;
this.food.coordinates.x = x;
this.food.coordinates.y = y;
}
detectCollision() {
// Self collison
// It's impossible for the first 3 pieces of the snake to self collide so the loop starts at 4
for(let i = 4; i < this.snake.length; i++) {
const selfCollison = this.snake[i].x === this.snake[0].x && this.snake[i].y === this.snake[0].y;
if(selfCollison) {
return true;
}
}
// Wall collison
const leftCollison = this.snake[0].x < 0;
const topCollison = this.snake[0].y < 0;
const rightCollison = this.snake[0].x > this.$canvas.width - this.settings.snake.size;
const bottomCollison = this.snake[0].y > this.$canvas.height - this.settings.snake.size;
return leftCollison || topCollison || rightCollison || bottomCollison;
}
endGame() {
this.soundEffects.gameOver.play();
clearInterval(this.startGameInterval);
this.$app.classList.remove('game-in-progress');
this.$app.classList.add('game-over');
this.$startScreen.querySelector('.options h3').innerText = 'Game Over';
this.$startScreen.querySelector('.options .end-score').innerText = `Score: ${this.game.score}`;
this.setUpGame();
}
}
const snakeGame = new SnakeGame();
</script>
Stop the page from moving up and down when the game is active
You just need to prevent the default behaviour of these keys:
// Change direction
document.addEventListener('keydown', event => {
event.preventDefault(); // add this line
this.changeDirection(event.keyCode);
});
Note that this isn't a very good idea to do it that way, as it will prevent the default behaviour of all keypresses anywhere on your page. Most notably, F5 will never refresh the browser, and if you have other elements like buttons, links, etc, the standard keypresses to activate those will stop working (turning these off is a particular problem if you want your site to be accessible to those with disabilities).
You'd be better off checking the keyCode first and only preventing default if it corresponds to one of the arrow keys - which isn't so easy to do with your code as it stands but I'd encourage you to rewrite it that way.
Even doing that presents a problem to those who are not able to use a mouse, as (depending on the position of your game interface on the screen and its size) they may still need to scroll down and will be prevented from doing this by this preventDefault behaviour. But well that's what you asked for, and I've given you the solution - but with this important caveat.

interact.js prevent resizable elements from overlapping

I am using the interact.js library for drag-and-dropping elements inside a grid. Once it is inside my grid I want to be able to resize them which is working, but what is the recommend way for preventing two resizable elements from overlapping. I basically want to stop the resizable move listener when it hits another resizable element. I already know when one resizable elements hits another, But how do I prevent the user from being able to overlap them?
full function for resizing elements:
function resizeZone(target, parentContainer, minHeight, width) {
if (interact.isSet(target) == false) {
interact(target)
.resizable({
edges: { top: true, left: false, bottom: true, right: false },
listeners: {
start: function (event) {
startDataSet = event.target.dataset;
startDeltaRect = event.deltaRect;
startRect = event.rect;
},
move: function (event) {
var elementsAtDragLocation = document.elementsFromPoint(event.x0, event.client.y);
for (let elementId in elementsAtDragLocation) {
var result = elementsAtDragLocation[elementId].getAttribute('free-schedule');
var isFalseSet = (result === 'false');
if (isFalseSet) {
let dragLocationRoutineId = elementsAtDragLocation[elementId].id;
let routineId = event.target.id;
// if user drags element past other element they will overlap, because the isFalseSet check won't work =(
if (dragLocationRoutineId !== routineId) {
return;
}
}
}
let { x, y } = event.target.dataset
x = (parseFloat(x) || 0) + event.deltaRect.left
y = (parseFloat(y) || 0) + event.deltaRect.top
Object.assign(event.target.style, {
width: `${event.rect.width}px`,
height: `${event.rect.height}px`,
transform: `translate(${x}px, ${y}px)`
});
Object.assign(event.target.dataset, { x, y });
},
end: async function (event) {
let routineId = event.target.id;
ResizeTaskTrigger(routineId, event.rect.height, event.deltaRect);
},
},
modifiers: [
interact.modifiers.restrictSize({
min: { width: width, height: minHeight },
}),
interact.modifiers.restrictRect({
restriction: parentContainer
}),
],
});
}
}

Phaser 3: Check if draggable Object is over interactive area of a Sprite

What I want to achieve is to be able to drag a Sprite, and when it moves over the interactive area of another Sprite, it emits particles.
However the Pointermove event doesn't get called if I am dragging a different sprite (likely since my pointer is over the draggable sprite, not the interactive sprite.) I tried adding an If-Condition to my drag event instead, which tests if my draggable object is over the interactive Sprite, which sort of works, but it reacts over any area of the sprite, as opposed to exclusively the interactive area. This is important because my Sprite's interactive area uses pixel perfect.
Is there any way to achieve the result I am looking for?
My try with Pointermove:
gameState.chara.setInteractive({cursor: "pointer", pixelPerfect: true});
gameState.chara.on('pointermove', function(activePointer){
if (activePointer.isDown && gameState.clean_cursor) {
switch(gameState.clean_cursor.frame.name.split('_')[0]){
case "sponge":
gameState.bubble.emitParticle(1, activePointer.x, activePointer.y);
break;
case "showerhead":
gameState.splash.emitParticle(1, activePointer.x-50, activePointer.y+50);
break;
}
}
});
this.input.on('drag', (activePointer, gameObject, dragX, dragY) => {
switch(gameObject.frame.name.split('_')[0]){
case "sponge":
gameState.clean_cursor.play('sponge_cursor', true);
break;
case "showerhead":
gameState.clean_cursor.play('shower_cursor', true);
break;
}
gameObject.x = dragX;
gameObject.y = dragY;
});
this.input.on('dragend', function(pointer, gameObject) {
gameObject.destroy();
gameState.clean_cursor = false;
});
(Pointermove Event stops reacting once I drag the other sprite)
My attempt with Drag only:
gameState.chara.setInteractive({cursor: "pointer", pixelPerfect: true});
this.input.on('drag', (activePointer, gameObject, dragX, dragY) => {
if(gameState.chara.getBounds().contains(dragX, dragY)) {
switch(gameState.clean_cursor.frame.name.split('_')[0]){
case "sponge":
gameState.bubble.emitParticle(1, activePointer.x, activePointer.y);
break;
case "showerhead":
gameState.splash.emitParticle(1, activePointer.x-50, activePointer.y+50);
break;
}
}
switch(gameObject.frame.name.split('_')[0]){
case "sponge":
gameState.clean_cursor.play('sponge_cursor', true);
break;
case "showerhead":
gameState.clean_cursor.play('shower_cursor', true);
break;
}
gameObject.x = dragX;
gameObject.y = dragY;
});
this.input.on('dragend', function(pointer, gameObject) {
gameObject.destroy();
});
(Works, but over entire sprite, as opposed to the Pixel Perfect interactive Area only)
With the solution how to dragg the button/image https://stackoverflow.com/a/71310078/1679286
the solution, for this question should be easy.
Just check if the sprite overlaps with the boundaries. (is not pixelperfect, because it matches boxes) here the documentation
if you need pixel-perfect, this would be more difficult.
Here the code, for the "simple" solution:
(the code needs some tweaking)
let Example = {
preload () {
this.load.spritesheet('brawler', 'https://labs.phaser.io/assets/animations/brawler48x48.png', { frameWidth: 48, frameHeight: 48 });
this.load.atlas('flares', 'https://labs.phaser.io/assets/particles/flares.png', 'https://labs.phaser.io/assets/particles/flares.json');
},
create () {
this._particles = this.add.particles('flares');
this._emitter = this._particles.createEmitter({
frame: 'blue',
lifespan: 2000,
speed: { min: 50, max: 400 },
angle: 330,
gravityY: 300,
scale: { start: 0.4, end: 0 },
quantity: 2,
blendMode: 'ADD',
on: false
});
let buttonPos = { x:20, y:20};
let buttonPlaceholder = this.add.image(buttonPos.x, buttonPos.y, 'brawler', ).setOrigin(0.5);
let button = this.add.image(buttonPos.x, buttonPos.y, 'brawler', ).setOrigin(0.5);
let interact = this.add.image(200, 100, 'brawler', 3).setOrigin(0.5);
interact.setDepth(-1);
button.setInteractive({ useHandCursor: true });
this.input.setDraggable(button);
this.input.on('drag', function(pointer, gameObject, dragX, dragY) {
gameObject.x = dragX;
gameObject.y = dragY;
if(Phaser.Geom.Intersects.RectangleToRectangle(gameObject.getBounds(), interact.getBounds() )){
this._emitter.start()
this._particles.x = interact.x;
this._particles.y = interact.y;
} else {
if (this._emitter.active){
this._emitter.stop();
}
}
}, this);
this.input.on('dragend', function(pointer, gameObject) {
gameObject.x = buttonPos.x;
gameObject.y = buttonPos.y;
this._emitter.stop();
}, this);
}
}
const config = {
type: Phaser.AUTO,
width: 400,
height: 200,
scene: [ Example ]
};
const game = new Phaser.Game(config);
<script src="https://cdn.jsdelivr.net/npm/phaser#3.55.2/dist/phaser.js"></script>
Extra info, to improve: You could check overlap with CircleToCircle, and keeping the radius abit smaller. (Or other intersection functions, that fit your needs better https://photonstorm.github.io/phaser3-docs/Phaser.Geom.Intersects.html)
A pixel perfect version, could be done like this:
(just found this property this.input.topOnly, docs)
If set to false this property allows the input events to be passed to other object, that are under the top most object.
let Example = {
preload () {
this.load.spritesheet('brawler', 'https://labs.phaser.io/assets/animations/brawler48x48.png', { frameWidth: 48, frameHeight: 48 });
this.load.atlas('flares', 'https://labs.phaser.io/assets/particles/flares.png', 'https://labs.phaser.io/assets/particles/flares.json');
},
create () {
this.input.topOnly = false;
let isDragging = false;
this._particles = this.add.particles('flares');
this._emitter = this._particles.createEmitter({
frame: 'blue',
lifespan: 2000,
speed: { min: 400, max: 600 },
angle: 330,
gravityY: 300,
scale: { start: 0.4, end: 0 },
quantity: 2,
blendMode: 'ADD',
on: false
});
let buttonPos = { x:20, y:20};
let buttonPlaceholder = this.add.image(buttonPos.x, buttonPos.y, 'brawler', )
.setOrigin(0.5);
let button = this.add.image(buttonPos.x, buttonPos.y, 'brawler', )
.setOrigin(0.5);
let interact = this.add.image(200, 100, 'brawler', 3)
.setOrigin(0.5);
interact
.setInteractive(this.input.makePixelPerfect());
interact.on('pointerover', function(pointer){
if(isDragging){
this._particles.x = interact.x;
this._particles.y = interact.y;
this._emitter.start();
}
}, this);
interact.on('pointerout', function(pointer){
this._emitter.stop();
}, this);
interact.setDepth(-1);
button.setInteractive({ useHandCursor: true });
this.input.setDraggable(button);
this.input.on('drag', function(pointer, gameObject, dragX, dragY) {
isDragging = true;
gameObject.x = dragX;
gameObject.y = dragY;
}, this);
this.input.on('dragend', function(pointer, gameObject) {
gameObject.x = buttonPos.x;
gameObject.y = buttonPos.y;
this._emitter.stop();
isDragging = false;
}, this);
}
}
const config = {
type: Phaser.AUTO,
width: 400,
height: 200,
scene: [ Example ]
};
const game = new Phaser.Game(config);
<script src="https://cdn.jsdelivr.net/npm/phaser#3.55.2/dist/phaser.js"></script>

Highcharts draggable annotaion how to move by steps?

I would like to make the annotations move by a certain step and not stop in between two columns in axis.
For example, the x axis is [0, 10, 20, 30]. When dragging the annotation, I want its point to change directly from {x: 10, y: 10000} to {x: 20, y: 10000} without going to x: 15. i.e. make the annotation a bit sticky to the column and not sitting between columns.
I also need to get the current annotation point so I can update some other element.
I tried the solution from stackoverflow but it doesn't work.
Here is my current code. CodePen
enter code here
Edit 1:
Thanks to the solution from #ppotaczek, the annotation can move by steps now. Here is the updated code JSFiddle. What needs to be refined is when dragging the annotation too fast, it cannot keep up with the mouse. Is it because of the performance of Highcharts.redraw() method and how can we solve this?
Another unsolved question is -> how to get the current point of the annotation? It seems the annotation object doesn't provide any value about that. All I can think of is keep a record of the initial point, then everytime the annotation is moved by a step, update the record. Is there a better way?
//this code can deal with vertical and horizontal annotations now.
Highcharts.Annotation.prototype.onDrag = function(e) {
if (
this.chart.isInsidePlot(
e.chartX - this.chart.plotLeft,
e.chartY - this.chart.plotTop
)
) {
var translation = this.mouseMoveToTranslation(e),
xAxis = this.chart.xAxis[0],
yAxis = this.chart.yAxis[0],
xStep = 1,
yStep = 1000,
pxStep = xAxis.toPixels(xStep) - xAxis.toPixels(0),
pyStep = yAxis.toPixels(yStep) - yAxis.toPixels(0);
if (!Highcharts.defined(this.potentialTranslation)) { //not defined
this.potentialTranslation = 0;
}
if (this.options.draggable === 'x') {
this.potentialTranslation += translation.x;
translation.y = 0;
if (Math.abs(this.potentialTranslation) >= Math.abs(pxStep)) {
translation.x = (this.potentialTranslation > 0) ? pxStep : -pxStep;
this.potentialTranslation = 0;
//this.potentialTranslation = undefined;
if (this.points.length) {
this.translate(translation.x, translation.y);
} else {
this.shapes.forEach(function(shape) {
shape.translate(translation.x, translation.y);
});
this.labels.forEach(function(label) {
label.translate(translation.x, translation.y);
});
}
}
}
if (this.options.draggable === 'y') {
this.potentialTranslation += translation.y;
translation.x = 0;
if (Math.abs(this.potentialTranslation) >= Math.abs(pyStep)) {
translation.y = (this.potentialTranslation > 0) ? -pyStep : pyStep;
this.potentialTranslation = 0;
if (this.points.length) {
this.translate(translation.x, translation.y);
} else {
this.shapes.forEach(function(shape) {
shape.translate(translation.x, translation.y);
});
this.labels.forEach(function(label) {
label.translate(translation.x, translation.y);
});
}
}
}
this.redraw(false);
}
}
Edit 2:
Thanks to #ppotaczek again! The "mouse moving too fast" issue is solved now. I apply his updates and also my idea of how to get the annotation value into JSFiddle.
You can overwrite Highcharts.Annotation.prototype.onDrag method:
(function(H) {
H.Annotation.prototype.onDrag = function(e) {
if (
this.chart.isInsidePlot(
e.chartX - this.chart.plotLeft,
e.chartY - this.chart.plotTop
)
) {
var translation = this.mouseMoveToTranslation(e),
xAxis = this.chart.xAxis[0],
step = 0.5,
pxStep = xAxis.toPixels(step) - xAxis.toPixels(0);
if (!H.defined(this.potentialTranslation)) {
this.potentialTranslation = 0;
}
this.potentialTranslation += translation.x;
if (Math.abs(this.potentialTranslation) >= pxStep) {
translation.y = 0;
translation.x = (this.potentialTranslation > 0) ? pxStep : -pxStep;
this.potentialTranslation = 0;
if (this.points.length) {
this.translate(translation.x, translation.y);
} else {
this.shapes.forEach(function(shape) {
shape.translate(translation.x, translation.y);
});
this.labels.forEach(function(label) {
label.translate(translation.x, translation.y);
});
}
}
this.redraw(false);
}
}
}(Highcharts));
Live demo: http://jsfiddle.net/BlackLabel/vmon2chx/
Docs: https://www.highcharts.com/docs/extending-highcharts

Fabric.js - Constrain Resize/Scale to Canvas/Object

How can an object be constrained to being resized/scaled only within the bounds of the canvas (or another object in my case) when using Fabric.js?
Currently, I'm handling the object:scaling event:
// Typescript being used;
onObjectScaling = (event): boolean => {
// Prevent scaling object outside of image
var object = <fabric.IObject>event.target;
var objectBounds: BoundsHelper;
var imageBounds: BoundsHelper;
object.setCoords();
objectBounds = new BoundsHelper(object);
imageBounds = new BoundsHelper(this.imageManipulatorContext.getImageObject());
if (objectBounds.left < imageBounds.left || objectBounds.right > imageBounds.right) {
console.log('horizontal bounds exceeded');
object.scaleX = this.selectedObjectLastScaleX;
object.lockScalingX = true;
object.setCoords();
} else {
this.selectedObjectLastScaleX = object.scaleX;
}
if (objectBounds.top < imageBounds.top || objectBounds.bottom > imageBounds.bottom) {
console.log('vertical bounds exceeded');
object.scaleY = this.selectedObjectLastScaleY;
object.lockScalingY = true;
object.setCoords();
} else {
this.selectedObjectLastScaleY = object.scaleY;
}
return true;
}
**edit The BoundsHelper class is just a helper for wrapping up the math of calculating the right/bottom sides of the bounding box for an object.
import fabric = require('fabric');
class BoundsHelper {
private assetRect: { left: number; top: number; width: number; height: number; };
get left(): number { return this.assetRect.left; }
get top(): number { return this.assetRect.top; }
get right(): number { return this.assetRect.left + this.assetRect.width; }
get bottom(): number { return this.assetRect.top + this.assetRect.height; }
get height(): number { return this.assetRect.height; }
get width(): number { return this.assetRect.width; }
constructor(asset: fabric.IObject) {
this.assetRect = asset.getBoundingRect();
}
}
export = BoundsHelper;
I also make use of the onBeforeScaleRotate callback to disable the scaling lock added by the above:
onObjectBeforeScaleRotate = (targetObject: fabric.IObject): boolean => {
targetObject.lockScalingX = targetObject.lockScalingY = false;
return true;
}
The issue I observe is that it seems like Fabric doesn't redraw the object fast enough for me to accurately detect that scale is passing the image's boundary. In other words, if I scale slowly, then life is good; if I scale quickly, then I can scale outside of the image's bounds.
It is not about speed.
Events are discrete sampling of something you are doing with the mouse.
Even if you move pixel by pixel virtually, the mouse has its own sample rate and the javascript event do not fire every pixel you move when you go fast.
So if you limit your application to stop scaling when you overcome a limit, when you overcome this limit you will stop your scaling, some pixel after the limit, simply because the last check you were 1 pixel before bound, the event after you are 10 pixel after it.
I changed the code, that is not perfect at all, but gives you idea for dealing with the issue.
When you overcome the limit, instead of lock scaling, calculate the correct scaling to be inside the image and apply that scale.
This logic has problems when you are completely outside the image, so i placed the rect already in, just to demonstrate the different approach.
var BoundsHelper = (function () {
function BoundsHelper(asset) {
this.assetRect = asset.getBoundingRect();
}
Object.defineProperty(BoundsHelper.prototype, "left", {
get: function () {
return this.assetRect.left;
},
enumerable: true,
configurable: true
});
Object.defineProperty(BoundsHelper.prototype, "top", {
get: function () {
return this.assetRect.top;
},
enumerable: true,
configurable: true
});
Object.defineProperty(BoundsHelper.prototype, "right", {
get: function () {
return this.assetRect.left + this.assetRect.width;
},
enumerable: true,
configurable: true
});
Object.defineProperty(BoundsHelper.prototype, "bottom", {
get: function () {
return this.assetRect.top + this.assetRect.height;
},
enumerable: true,
configurable: true
});
Object.defineProperty(BoundsHelper.prototype, "height", {
get: function () {
return this.assetRect.height;
},
enumerable: true,
configurable: true
});
Object.defineProperty(BoundsHelper.prototype, "width", {
get: function () {
return this.assetRect.width;
},
enumerable: true,
configurable: true
});
return BoundsHelper;
})();
////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////
var canvas = new fabric.Canvas('c');
var rectangle = new fabric.Rect({
fill: 'black',
originX: 'left',
originY: 'top',
stroke: 'false',
opacity: 1,
left: 180,
top: 180,
height: 50,
width: 50
});
var rectangleBounds = new BoundsHelper(rectangle);
var image = new fabric.Image(i, {
selectable: false,
borderColor: 'black',
width: 200,
height: 200
});
canvas.on("object:scaling", function (event) {
var object = event.target;
var objectBounds = null;
var imageBounds = null;
var desiredLength;
object.setCoords();
objectBounds = new BoundsHelper(object);
imageBounds = new BoundsHelper(image);
if (objectBounds.left < imageBounds.left) {
object.lockScalingX = true;
// now i have to calculate the right scaleX factor.
desiredLength =objectBounds.right - imageBounds.left;
object.scaleX = desiredLength / object.width;
}
if (objectBounds.right > imageBounds.right) {
object.lockScalingX = true;
desiredLength = imageBounds.right - objectBounds.left;
object.scaleX = desiredLength / object.width;
}
if (objectBounds.top < imageBounds.top) {
object.lockScalingY = true;
desiredLength = objectBounds.bottom - imageBounds.top;
object.scaleY = desiredLength / object.height;
}
if (objectBounds.bottom > imageBounds.bottom) {
object.lockScalingY = true;
desiredLength = imageBounds.bottom - objectBounds.top;
object.scaleY = desiredLength / object.height;
}
return true;
});
canvas.onBeforeScaleRotate = function (targetObject) {
targetObject.lockScalingX = targetObject.lockScalingY = false;
return true;
};
canvas.on('after:render', function() {
canvas.contextContainer.strokeStyle = '#555';
var bound = image.getBoundingRect();
canvas.contextContainer.strokeRect(
bound.left + 0.5,
bound.top + 0.5,
bound.width,
bound.height
);
});
canvas.add(image);
canvas.centerObject(image);
image.setCoords();
canvas.add(rectangle);
canvas.renderAll();
img {
display: none;
}
canvas {
border: solid black 1px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.4.12/fabric.min.js"></script>
<img id="i" src="http://fabricjs.com/assets/ladybug.png" />
<canvas id="c" width="500" height="500"></canvas>
https://jsfiddle.net/84zovnek/2/
if (objectBounds.left < imageBounds.left) {
//object.lockScalingX = true;
// now i have to calculate the right scaleX factor.
desiredLength = imageBounds.left - objectBounds.right;
object.scaleX = desiredLength / object.width;
}
Other that, you should update to latest version

Categories

Resources