Konvajs: How to change position of group of texts - position

I'm using Konvajs, I have group of texts, and I want don't allow drag group outside of the canvas, I'm tried solved that using dragBoundFunc, but that don't help me, now I just try change group position during dragmove, but setPosition, setAbsloutePosition, nothing allow me to change group position
stage.on('dragmove', (e) => stageOnDragMove(e, layer));
const stageOnDragMove = (e: Konva.KonvaEventObject<any>, layer: Konva.Layer) => {
const selectionGroup = layer.findOne('#selection-group');
const transformer = layer.findOne<Konva.Transformer>('Transformer');
if (selectionGroup?.hasName('text-group')) {
const pos = selectionGroup.getClientRect({});
if (pos.x <= 0 || pos.y <= 0) {
selectionGroup.setAbsolutePosition({
x: 0,
y: 0
});
layer.draw();
}
}
transformer.attachTo(selectionGroup);
};

You can use this function to limit drag&drop and resize feature to limit its boundaries:
shape.on('dragmove transform', () => {
const box = shape.getClientRect();
const absPos = shape.getAbsolutePosition();
const offsetX = box.x - absPos.x;
const offsetY = box.y - absPos.y;
const newAbsPos = {...absPos}
if (box.x < 0) {
newAbsPos.x = -offsetX;
}
if (box.y < 0) {
newAbsPos.y = -offsetY;
}
if (box.x + box.width > stage.width()) {
newAbsPos.x = stage.width() - box.width - offsetX;
}
if (box.y + box.height > stage.height()) {
newAbsPos.y = stage.height() - box.height - offsetY;
}
shape.setAbsolutePosition(newAbsPos)
})
Demo: https://jsbin.com/rofupicupu/edit?html,js,output

Related

How to get quantity of duplicates in array nodejs

I'm trying to get the quantity of for example: DragonBall, it would return x3 or Featured it would return x2 etc, however I have tried this method with just the spammed response of 2
let data = mockdata.forEach(function (i) {
count[i] = (count[i] || 0) + 1;
console.log(count[i] = (count[i] || 0) + 1)
});
[
'Daily',
'DragonBall1B',
'DragonBall2B',
'DragonBall3B',
'Featured',
'Featured2',
'SquadOrigins',
'SquadOrigins2'
]
API used to retrieve the above information:
https://fortnitecontent-website-prod07.ol.epicgames.com/content/api/pages/fortnite-game/shop-sections
A regular expression can remove the first instance of digits (along with whatever follows) to get you to the key you're interested in grouping on.
const mockdata = [
'Daily',
'DragonBall1B',
'DragonBall2B',
'DragonBall3B',
'Featured',
'Featured2',
'SquadOrigins',
'SquadOrigins2'
]
const count = {};
mockdata.forEach((str) => {
const key = str.replace(/\d+.*/, '');
count[key] = (count[key] || 0) + 1;
});
console.log(count.DragonBall);
const arr = [
'Daily',
'DragonBall1B',
'DragonBall2B',
'DragonBall3B',
'Featured',
'Featured2',
'SquadOrigins',
'SquadOrigins2'
]
const count = {};
arr.forEach((str) => {
const key = str.replace(/\d+.*/, "");
count[key] = (count[key] || 0) + 1;
});
let val = Object.entries(count);
let itemName;
let itemNum;
let result = [];
for (var i in val) {
itemName = val[i][0];
itemNum = val[i][1];
result += `${itemName} (x${itemNum})\n`;
}
console.log(result);

Dashed border seems like its multiplying

I'm makking a Node.js app with Jimp. I have a function for selecting part of an image. It draws a rectangle in the area, writes the number of the selection in the middle and then draws a dashed border around the outside of the rectangle selection. The dashed border seems to be multiplying and I have no idea why. Here's the full code of the file:
imageManipulationUtil.js:
const Jimp = require("jimp");
module.exports = async (readPath, writePath, comments, callback) => {
const originalImage = await Jimp.read(readPath);
const font = await Jimp.loadFont(Jimp.FONT_SANS_32_BLACK);
// Please ignore this part. Focus on the other part (the functions I mentioned above).
const addedWidth = 500;
const commentsHeight = comments.reduce((commentsHeight, { comment }, i) => {
comments[i].comment = `${i + 1}. ${comment}`;
const textWidth = Jimp.measureText(font, comment);
const textHeight = Jimp.measureTextHeight(font, comment);
const lines = Math.ceil(textWidth / addedWidth);
const height = textHeight * lines;
return commentsHeight + height;
}, 0);
const imageHeight = commentsHeight + 10;
if (imageHeight > originalImage.getHeight())
originalImage.resize(Jimp.AUTO, imageHeight);
const newImage = new Jimp(
originalImage.getWidth() + addedWidth,
originalImage.getHeight(),
0xffffffff
);
// Some other code for another purpose
// !!! Important code START !!!
drawSelectionRects(comments, font, newImage);
async function drawSelectionRects(comments, font, image) {
comments.forEach(({ dimensions }) => {
image.scanQuiet(
dimensions.x,
dimensions.y,
dimensions.width,
dimensions.height,
(x, y, idx) => {
const color = {
r: image.bitmap.data[idx + 0],
g: image.bitmap.data[idx + 1],
b: image.bitmap.data[idx + 2],
a: image.bitmap.data[idx + 3] / 255,
};
const selectionColor = {
r: 187,
g: 187,
b: 187,
a: 187,
};
const newColor = blendColors(color, selectionColor);
const hexColor = Jimp.rgbaToInt(
newColor.r,
newColor.g,
newColor.b,
255
);
image.setPixelColor(hexColor, x, y);
}
);
dashedBorder(
image,
{ lineDash: [20, 5], lineWidth: 3, color: 0x1a53ffbb },
dimensions
);
});
comments.forEach(({ dimensions }, i) => {
const text = `${i + 1}`;
let textX =
dimensions.x + (dimensions.width - Jimp.measureText(font, text)) / 2;
let textY =
dimensions.y +
(dimensions.height - Jimp.measureTextHeight(font, text)) / 2;
image.print(font, textX, textY, text);
});
}
function blendColors(c1, c2) {
const stepPoint = c2.a / 255;
const r = c1.r + stepPoint * (c2.r - c1.r);
const g = c1.g + stepPoint * (c2.g - c1.g);
const b = c1.b + stepPoint * (c2.b - c1.b);
return { r, g, b };
}
function dashedBorder(
image,
{ lineDash, lineWidth, color },
{ x, y, width, height }
) {
let drawing = true,
passed = 0;
color = Jimp.intToRGBA(color);
// Top border
for (let i = x; i < x + width; i++) {
if (drawing) {
const pixelColor = Jimp.intToRGBA(image.getPixelColor(x, y));
const newColor = blendColors(pixelColor, color);
for (let k = 0; k < lineWidth; k++) {
image.setPixelColor(
Jimp.rgbaToInt(newColor.r, newColor.g, newColor.b, 255),
i,
y - k
);
}
}
passed++;
if (
(passed >= lineDash[0] && drawing) ||
(passed >= lineDash[1] && !drawing)
) {
drawing = !drawing;
passed = 0;
}
}
drawing = true;
// Right border
for (let j = y; j < y + height; j++) {
if (drawing) {
const pixelColor = Jimp.intToRGBA(image.getPixelColor(x + width, y));
const newColor = blendColors(pixelColor, color);
for (let k = 0; k < lineWidth; k++) {
image.setPixelColor(
Jimp.rgbaToInt(newColor.r, newColor.g, newColor.b, 255),
x + width + k,
j
);
}
}
passed++;
if (
(passed >= lineDash[0] && drawing) ||
(passed >= lineDash[1] && !drawing)
) {
drawing = !drawing;
passed = 0;
}
}
drawing = true;
// Bottom border
for (let i = x + width; i > x; i--) {
if (drawing) {
const pixelColor = Jimp.intToRGBA(image.getPixelColor(i, y + height));
const newColor = blendColors(pixelColor, color);
for (let k = 0; k < lineWidth; k++) {
image.setPixelColor(
Jimp.rgbaToInt(newColor.r, newColor.g, newColor.b, 255),
i,
y + height + k
);
}
}
passed++;
if (
(passed >= lineDash[0] && drawing) ||
(passed >= lineDash[1] && !drawing)
) {
drawing = !drawing;
passed = 0;
}
}
drawing = true;
// Left border
for (let j = y + height; j > y + lineWidth; j--) {
if (drawing) {
const pixelColor = Jimp.intToRGBA(image.getPixelColor(x, j));
const newColor = blendColors(pixelColor, color);
for (let k = 0; k < lineWidth; k++) {
image.setPixelColor(
Jimp.rgbaToInt(newColor.r, newColor.g, newColor.b, 255),
x - k,
j
);
}
}
passed++;
if (
(passed >= lineDash[0] && drawing) ||
(passed >= lineDash[1] && !drawing)
) {
drawing = !drawing;
passed = 0;
}
}
}
newImage.write(writePath);
callback();
};
// !!! Important code END !!!
The code exports a function that takes some parameters. The most important parameter is the comments parameter (specifically the dimensions of the comment). The comments param is an array of objects. The objects have a dimensions key which is the important one in this question.
The image uploaded:
Image recieved:
The problem that makes me think the image is multiplying is that in some places the border is more transparent than in other parts. Originally I thought that was just because of the image I used but then I switched it and noticed it was a real problem.
New full code:
const Jimp = require("jimp");
function dashedBorder(
image,
{ lineDash, lineWidth, color },
{ x, y, width, height }
) {
let drawing = true,
passed = 0,
outsideWidth = lineWidth - 1;
color = Jimp.intToRGBA(color);
// Top border
for (let i = x - outsideWidth; i < x + width + outsideWidth; i++) {
if (drawing) {
for (let k = 0; k < lineWidth; k++) {
image.setPixelColor(
Jimp.rgbaToInt(color.r, color.g, color.b, 255),
i,
y - k
);
}
}
passed++;
if (
(passed >= lineDash[0] && drawing) ||
(passed >= lineDash[1] && !drawing)
) {
drawing = !drawing;
passed = 0;
}
}
// Right border
for (
let j = y + lineWidth - outsideWidth;
j < y + height - (lineWidth - outsideWidth);
j++
) {
if (drawing) {
for (let k = 0; k < lineWidth; k++) {
image.setPixelColor(
Jimp.rgbaToInt(color.r, color.g, color.b, 255),
x + width + k - 1,
j
);
}
}
passed++;
if (
(passed >= lineDash[0] && drawing) ||
(passed >= lineDash[1] && !drawing)
) {
drawing = !drawing;
passed = 0;
}
}
// Bottom border
for (let i = x + width + lineWidth - outsideWidth; i > x - lineWidth; i--) {
if (drawing) {
for (let k = 0; k < lineWidth; k++) {
image.setPixelColor(
Jimp.rgbaToInt(color.r, color.g, color.b, 255),
i,
y + height + k - 1
);
}
}
passed++;
if (
(passed >= lineDash[0] && drawing) ||
(passed >= lineDash[1] && !drawing)
) {
drawing = !drawing;
passed = 0;
}
}
// Left border
for (let j = y + height - outsideWidth; j > y; j--) {
if (drawing) {
for (let k = 0; k < lineWidth; k++) {
image.setPixelColor(
Jimp.rgbaToInt(color.r, color.g, color.b, 255),
x - k,
j
);
}
}
passed++;
if (
(passed >= lineDash[0] && drawing) ||
(passed >= lineDash[1] && !drawing)
) {
drawing = !drawing;
passed = 0;
}
}
}
(async () => {
let image = await Jimp.read("./test.png");
dashedBorder(
image,
{ lineWidth: 3, lineDash: [20, 5], color: 0x1a53ffbb },
{ x: 0, y: 0, width: image.bitmap.width, height: image.bitmap.height }
);
image.write("./test-border.png");
})();
Image:

Infinitely looping background as character moves in Phaser?

I have a background image and a separate ground image that I want to loop infinitely as long as the character is moving forward. When the character stops, the background and ground should not be moving. For similar games it is often suggested to add this.game.background.tilePosition.x -= 1
to the update function. This is not what I am looking for as it makes the background constantly move regardless of whether the character is moving. At the moment my background and ground images are repeating, but they are restricted to this.game.world.setBounds(0, 0, 1280, 800);. Any suggestions would be greatly appreciated. My code is below:
function Hero(game, x, y) {
Phaser.Sprite.call(this, game, x, y, 'player');
//rest of code for Hero constructor....
}
Hero.prototype = Object.create(Phaser.Sprite.prototype);
Hero.prototype.constructor = Hero;
//code for Hero.prototype....
PlayState = {};
PlayState.init = function () {
//code for keyboard...
};
PlayState.preload = function () {
this.game.load.json('level:1', 'data/level01.json');
this.game.load.image('ground', 'images/ground.png'); // I need this to
//repeat infinitely
this.game.load.image('background', 'images/background.png'); // I need
//this to repeat infinitely
this.game.load.spritesheet('player', 'images/player.png', 64, 64);
};
PlayState.create = function () {
this.game.world.setBounds(0, 0, 1280, 800);
this.game.background = this.game.add.tileSprite(0, 0,
this.game.world.width, 800, 'background');
this.game.ground = this.game.add.tileSprite(0, 680,
this.game.world.width, 166, 'ground');
this.game.physics.arcade.enable(this.game.ground);
this.game.ground.body.immovable = true;
this.game.ground.body.allowGravity = false;
this.game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
this._loadLevel(this.game.cache.getJSON('level:1'));
};
PlayState.update = function () {
this.physics.arcade.collide(this.player, this.game.ground);
};
PlayState._loadLevel = function (data) {
this._spawnPlayer({player: data.player});
const GRAVITY = 1200;
this.game.physics.arcade.gravity.y = GRAVITY;
};
PlayState._spawnPlayer = function (data) {
this.player = new Hero(this.game, data.player.x, data.player.y);
this.game.add.existing(this.player);
this.game.camera.follow(this.player,
Phaser.Camera.FOLLOW_PLATFORMER);
};
window.onload = function () {
let game = new Phaser.Game(866, 520, Phaser.CANVAS, 'game');
game.state.add('play', PlayState);
game.state.start('play');
};
I have tried the following solution (see below) where I create a constructor function for the background. This idea is from an existing tutorial done in TypeScript that does exactly what I am looking for. However, I am not familiar with TypeScript so I have just tried to interpret the code from the tutorial as best I can and put it into Javascript but at the moment I am getting the error TypeError: "a is undefined"in the console. I am still learning Javascript and I can't see where I am going wrong. (I have included the keyboard logic and the character movement this time for clarity.)
function MyBackground(game, x, y) {
Phaser.Sprite.call(this, game, x, y, 'background');
}
MyBackground.prototype = Object.create(Phaser.Sprite.prototype);
MyBackground.prototype.constructor = MyScene;
MyBackground.prototype.repeatScene = function () {
this.nextFrame = new Phaser.Sprite(this.game, this.width, 0, "background", 0);
this.game.add.existing(this.nextFrame);
};
function Hero(game, x, y) {
Phaser.Sprite.call(this, game, x, y, 'player');
this.anchor.set(0.5, 0.5);
this.game.physics.enable(this);
this.body.collideWorldBounds = false;
this.animations.add('stop', [0]);
this.animations.add('run', [1, 2, 3, 4, 5], 14, true); // 14fps looped
this.animations.add('jump', [6]);
this.animations.add('fall', [7]);
this.animations.add('die', [8, 9, 8, 9, 8, 9, 8, 9], 12); // 12fps no loop
}
Hero.prototype = Object.create(Phaser.Sprite.prototype);
Hero.prototype.constructor = Hero;
Hero.prototype.move = function (direction) {
const SPEED = 200;
this.body.velocity.x = direction * SPEED;
// update image flipping & animations
if (this.body.velocity.x < 0) {
this.scale.x = -1;
}
else if (this.body.velocity.x > 0) {
this.scale.x = 1;
}
};
Hero.prototype.jump = function () {
const JUMP_SPEED = 600;
let canJump = this.body.touching.down;
if (canJump) {
this.body.velocity.y = -JUMP_SPEED;
}
return canJump;
};
Hero.prototype.bounce = function () {
const BOUNCE_SPEED = 200;
this.body.velocity.y = -BOUNCE_SPEED;
};
Hero.prototype.update = function () {
// update sprite animation, if it needs changing
let animationName = this._getAnimationName();
if (this.animations.name !== animationName) {
this.animations.play(animationName);
}
};
Hero.prototype.die = function () {
this.alive = false;
this.body.enable = false;
this.animations.play('die').onComplete.addOnce(function () {
this.kill();
}, this);
};
Hero.prototype._getAnimationName = function () {
let name = 'stop'; // default animation
if (!this.alive) {
name = 'die';
}
else if (this.body.velocity.y > 0 && !this.body.touching.down) {
name = 'fall';
}
else if (this.body.velocity.y < 0) {
name = 'jump';
}
else if (this.body.velocity.x !== 0 && this.body.touching.down ) {
name = 'run';
}
return name;
PlayState = {};
PlayState.init = function () {
this.game.renderer.renderSession.roundPixels = true;
this.keys = this.game.input.keyboard.addKeys({
left: Phaser.KeyCode.LEFT,
right: Phaser.KeyCode.RIGHT,
up: Phaser.KeyCode.UP
};
PlayState.preload = function () {
this.game.load.json('level:1', 'data/level01.json');
this.game.load.image('ground', 'images/ground.png'); // I need this to repeat infinitely
this.game.load.image('background', 'images/background.png'); // I need this to repeat infinitely
this.game.load.spritesheet('player', 'images/player.png', 64, 64);
};
PlayState.create = function () {
this.background = new MyBackground(this.game, 0, 0);
this.game.add.existing(this.MyBackground);
this.game.physics.arcade.enable(this.game.ground);
this.game.ground.body.immovable = true;
this.game.ground.body.allowGravity = false;
this.game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
this._loadLevel(this.game.cache.getJSON('level:1'));
this.game.world.setBounds(0, 0, this.MyBackground.width * 2, 800);
};
PlayState.update = function () {
var backgroundWidth = this.game.stage.getChildAt(0).getBounds().width; //getChildAt(0) because the background is created first in create
if (this.x > backgroundWidth * .75) {
this.x = backgroundWidth * .25;
repeatScene();
};
this._handleInput();
this.physics.arcade.collide(this.player, this.game.ground);
};
PlayState._handleInput = function () {
if (this.keys.up.isDown) {
this.player.jump();
} else if (this.keys.right.isDown) { // move hero right
this.player.move(1);
} else if (this.keys.left.isDown) { // move hero left
this.player.move(-1);
} else { // stop
this.player.move(0);
}
};
PlayState._loadLevel = function (data) {
this._spawnPlayer({player: data.player});
const GRAVITY = 1200;
this.game.physics.arcade.gravity.y = GRAVITY;
};
PlayState._spawnPlayer = function (data) {
this.player = new Hero(this.game, data.player.x, data.player.y);
this.game.add.existing(this.player);
this.game.camera.follow(this.player, Phaser.Camera.FOLLOW_PLATFORMER);
};
window.onload = function () {
let game = new Phaser.Game(866, 520, Phaser.CANVAS, 'game');
game.state.add('play', PlayState);
game.state.start('play');
};

Fabric custom object

fabric.ThreePointArc = fabric.util.createClass(fabric.Circle, {
type: 'threePointArc',
points: [], //array of startPoint, intermediatePoint, endPoint
arcCenter: new fabric.Point(null, null),
arcBounds: null,
radius: null,
initialize: function (points, options) {
if (!points || points.length === 0) {
return;
}
this.points = points;
this.callSuper('initialize', options);
// supports only originX and originY as center
this.originX = this.originY = 'center';
},
_set: function(key, value) {
this.callSuper('_set', key, value);
if (key === 'points') {
this._calcArcCenter();
this._calcDimensions();
this.setCoords();
}
return this;
},
setRadius: function(value) {
this.radius = value;
return this;
},
_calcDimensions: function() {
this._calcArcAngles();
this.setRadius(cMath.getLength(this.arcCenter, this.points[0]));
this._calcArcBounds();
this.width = this.arcBounds.width;
this.height = this.arcBounds.height;
this.top = this.arcBounds.y + this.arcBounds.height / 2;
this.left = this.arcBounds.x + this.arcBounds.width / 2;
},
_calcArcCenter: function() {
var c1Mp = cMath.getMidPoint(this.points[0], this.points[1]), // chord 1 midpoint
c2Mp = cMath.getMidPoint(this.points[1], this.points[2]), // chord 2 midpoint
c1pm = -(1 / cMath.getSlope(this.points[0], this.points[1])), // chord 1 perpendicular bisector slope
c2pm = -(1 / cMath.getSlope(this.points[1], this.points[2])); // chord 2 perpendicular bisector slope
// chord perpendicular bisectors meet at the center
this.arcCenter.x = (c2Mp.y - (c2pm * c2Mp.x) + (c1pm * c1Mp.x) - c1Mp.y) / (c1pm - c2pm);
this.arcCenter.y = (c2pm * (this.arcCenter.x - c2Mp.x)) + c2Mp.y;
},
_calcArcBounds: function() {
var validPoints = this.buildValidPointsForArc(),
minX = fabric.util.array.min(validPoints, 'x'),
minY = fabric.util.array.min(validPoints, 'y'),
maxX = fabric.util.array.max(validPoints, 'x'),
maxY = fabric.util.array.max(validPoints, 'y'),
width = (maxX - minX) || 1,
height = (maxY - minY) || 1;
this.arcBounds = {
x: minX,
y: minY,
width: width,
height: height
}
},
buildValidPointsForArc: function() {
var direction = this.getRenderingDirection(),
possibleBoundingPoints = this.points.concat();
!this.arcAngles && this._calcArcAngles();
if (direction) {
for (var i = 1; i <= 4; i++) {
var randomAngle = i * (PI / 2);
if (this.arcAngles.startAngle < this.arcAngles.endAngle) {
!(this.arcAngles.startAngle <= randomAngle && randomAngle <= this.arcAngles.endAngle) &&
possibleBoundingPoints.push(this.generateArcPoint(randomAngle));
} else {
(this.arcAngles.endAngle <= randomAngle && randomAngle <= this.arcAngles.startAngle) &&
possibleBoundingPoints.push(this.generateArcPoint(randomAngle));
}
}
} else {
for (var i = 4; i >= 1; i--) {
var randomAngle = i * (PI / 2);
if (this.arcAngles.startAngle < this.arcAngles.endAngle) {
(this.arcAngles.startAngle <= randomAngle && randomAngle <= this.arcAngles.endAngle) &&
possibleBoundingPoints.push(this.generateArcPoint(randomAngle));
} else {
!(this.arcAngles.endAngle <= randomAngle && randomAngle <= this.arcAngles.startAngle) &&
possibleBoundingPoints.push(this.generateArcPoint(randomAngle));
}
}
}
return possibleBoundingPoints;
},
generateArcPoint: function(angle) {
return new fabric.Point(this.arcCenter.x + this.radius * Math.cos(angle), this.arcCenter.y + this.radius * Math.sin(angle));
},
_calcArcAngles: function() {
var angleKeyRepo = ["startAngle", "intermediateAngle", "endAngle"];
this.arcAngles = this.arcAngles || {};
this.points.forEach(function(point, index) {
var a = cMath.getAngle(this.arcCenter, point);
this.arcAngles[angleKeyRepo[index]] = a < 0 ? ((PI * 2) + a) :
a > 2 * PI ? ((PI * 2) - a) : a;
}, this);
},
getRenderingDirection: function() {
return (((this.points[1].x - this.points[0].x) * (this.points[2].y - this.points[0].y)) -
((this.points[1].y - this.points[0].y) * (this.points[2].x - this.points[0].x))) < 0;
},
_render: function(ctx, noTransform) {
if (!this.visible) {
return;
}
ctx.beginPath();
ctx.arc(
noTransform ? this.left : 0,
noTransform ? this.top : 0,
this.radius,
this.arcAngles.startAngle,
this.arcAngles.endAngle,
this.getRenderingDirection()
);
this._renderFill(ctx);
this._renderStroke(ctx);
},
toObject: function (propertiesToInclude) {
return fabric.util.object.extend(this.callSuper('toObject', propertiesToInclude), {
points: this.points
});
}
});
fabric.ThreePointArc.fromObject = function(object) {
return new fabric.ThreePointArc(object.points, object);
};
fabric.ThreePointArc.async = false;
fabric.util.cMath = {
getSlope : function (startPoint, endPoint) {
if (!startPoint || !endPoint) {
console.error('startPoint and endPoint are required to evaluate slope');
return;
}
// hack to get around the indefinte slope problem
if (endPoint.x == startPoint.x) startPoint.x = startPoint.x + 0.01;
if (endPoint.y == startPoint.y) endPoint.y = endPoint.y + 0.01;
return (endPoint.y - startPoint.y) / (endPoint.x - startPoint.x);
},
getMidPoint: function (startPoint, endPoint) {
if (!startPoint || !endPoint) {
console.error('startPoint and endPoint are required to evaluate slope');
return;
}
return { x: (startPoint.x + endPoint.x) / 2, y: (startPoint.y + endPoint.y) / 2 };
},
getAngle: function (startPoint, endPoint, isDegree) {
if (!startPoint || !endPoint) {
console.error('startPoint and endPoint are required to evaluate slope');
return;
}
var radians = Math.atan2((endPoint.y - startPoint.y), (endPoint.x - startPoint.x)),
degrees = fabric.util.radiansToDegrees(radians);
return isDegree ? degrees < 0 ? 360 + degrees : degrees : radians;
},
getLength: function (startPoint, endPoint) {
if (!startPoint || !endPoint) {
console.error('startPoint and endPoint are required to evaluate slope');
return;
}
return Math.sqrt(Math.pow(endPoint.y - startPoint.y, 2) + Math.pow(endPoint.x - startPoint.x, 2));
}
}
var canvas = new fabric.Canvas('c');
var startPoint = new fabric.Point(47.25423728813553, 56.91525423728814),
intermediatePoint = new fabric.Point( 76.33898305084739,19.8983050847458 ),
endPoint = new fabric.Point( 105.42372881355931,86 );
var arc = new fabric.ThreePointArc([startPoint, intermediatePoint, endPoint] , {
fill: "#FF0000",
stroke: "#000",
strokeWidth: 10
});
canvas.add(arc);
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.4.10/fabric.js"></script>
<canvas id="c" width="800" height="800"></canvas>
I'm trying to create a three point arc on fabric js (ver: 1.4.10). What I want is exactly like this:- https://www.screencast.com/t/dMLvcOduEF
I have a points array from which I'm calcualting the startAngle, endAngle, arcCenter, radius, bounding boxc height and widht, top and left. When I draw it, the actual object is not drawing where my mouse points. It adds up the left and top value with the arcCenter.x and arcCenter.y respectively and draws there. Is there any workaround so that my arc can be drawn where my pointer is ?. So that I don't need to write any extra lines of code for moving, rotating and scaling.
What I'm getting is like this:- https://www.screencast.com/t/V9MUgB3pB
added the fiddle!!
Is there any workaround for this ??

How do I reverse a scanline using the jpeg-js module/node JS buffer?

I've been fiddling around with the jpeg-js module and Node JS Buffer, and attempting to create a small command line program that modifies the decoded JPEG buffer data and creates a pattern of X number of reversed scanlines and X number of normal scanlines before saving a new JPEG. In other words, I'm looking to flip portions of the image, but not the entire image itself (plenty of modules that do such a thing, of course, but not the specific use case I have).
To create the reversed/normal line patterns, I've been reading/writing line by line, and saving a slice of that line to a variable, then starting at the end of scanline and incrementally going down by slices of 4 bytes (the alloc for an RGBA value) until I'm at the beginning of the line. Code for the program:
'use strict';
const fs = require('fs');
const jpeg = require('jpeg-js');
const getPixels = require('get-pixels');
let a = fs.readFileSync('./IMG_0006_2.jpg');
let d = Buffer.allocUnsafe(a.width * a.height * 4);
let c = jpeg.decode(a);
let val = false; // track whether normal or reversed scanlines
let lineWidth = b.width * 4;
let lineCount = 0;
let track = 0;
let track2 = 0;
let track3 = 0;
let curr, currLine; // storage for writing/reading scnalines, respectively
let limit = {
one: Math.floor(Math.random() * 141),
two: Math.floor(Math.random() * 151),
three: Math.floor(Math.random() * 121)
};
if (limit.one < 30) {
limit.one = 30;
}
if (limit.two < 40) {
limit.two = 40;
}
if (limit.two < 20) {
limit.two = 20;
}
let calc = {};
calc.floor = 0;
calc.ceil = 0 + lineWidth;
d.forEach(function(item, i) {
if (i % lineWidth === 0) {
lineCount++;
/* // alternate scanline type, currently disabled to figure out how to succesfully reverse image
if (lineCount > 1 && lineCount % limit.one === 0) {
// val = !val;
}
*/
if (lineCount === 1) {
val = !val; // setting alt scanline check to true initially
} else if (calc.floor + lineWidth < b.data.length - 1) {
calc.floor += lineWidth;
calc.ceil += lineWidth;
}
currLine = c.data.slice(calc.floor, calc.ceil); // current line
track = val ? lineWidth : 0; // tracking variable for reading from scanline
track2 = val ? 4 : 0; // tracking variable for writing from scanline
}
//check if reversed and writing variable has written 4 bytes for RGBA
//if so, set writing source to 4 bytes at end of line and read from there incrementally
if (val && track2 === 4) {
track2 = 0; // reset writing count
curr = currLine.slice(track - 4, track); // store 4 previous bytes as writing source
if (lineCount === 1 && lineWidth - track < 30) console.log(curr); //debug
} else {
curr = currLine; //set normal scanline
}
d[i] = curr[track2];
// check if there is no match between data source and decoded image
if (d[i] !== curr[track2]) {
if (track3 < 50) {
console.log(i);
}
track3++;
}
track2++; //update tracking variable
track = val ? track - 1 : track + 1; //update tracking variable
});
var rawImageData = {
data: d,
width: b.width,
height: b.height
};
console.log(b.data.length);
console.log('errors\t', track3);
var jpegImageData = jpeg.encode(rawImageData, 100);
fs.writeFile('foo2223.jpg', jpegImageData.data);
Alas, the reversed scanline code I've written does not properly. Unfortunately, I've only been able successfully reverse the red channel of my test image (see below left), with the blue and green channels just turning into vague blurs. The color scheme should look something like the right image.
What am I doing wrong here?
For reversed lines, you stored slices of 4 bytes(4 bytes = 1 pixel), then write the first value of the pixel(red) correctly.
But in the next iteration, you overwrite the slice curr with currLine, rest of channels gets wrong values.
if (val && track2 === 4) {
track2 = 0; // reset writing count
curr = currLine.slice(track - 4, track); // store 4 previous bytes as writing source
if (lineCount === 1 && lineWidth - track < 30) console.log(curr); //debug
} else {
curr = currLine; //set normal scanline
}
Iteration 0: val == true, track2 == 4, set curr to next pixel, write red channel.
Iteration 1: val == true, track2 == 1, (val && track2 === 4) == false, set curr to currLine, write green channel.
You can move track2 === 4 branch to avoid this:
if (val) {
if (track2 === 4) {
track2 = 0; // reset writing count
curr = currLine.slice(track - 4, track); // store 4 previous bytes as writing source
if (lineCount === 1 && lineWidth - track < 30) console.log(curr); //debug
}
} else {
curr = currLine; //set normal scanline
}
Fixed code should look like this:
function flipAlt(input, output) {
const fs = require('fs');
const jpeg = require('jpeg-js');
let a = fs.readFileSync(input);
let b = jpeg.decode(a);
let d = Buffer.allocUnsafe(b.width * b.height * 4);
let val = false; // track whether normal or reversed scanlines
let lineWidth = b.width * 4;
let lineCount = 0;
let track = 0;
let track2 = 0;
let track3 = 0;
let curr, currLine; // storage for writing/reading scnalines, respectively
let limit = {
one: Math.floor(Math.random() * 141),
two: Math.floor(Math.random() * 151),
three: Math.floor(Math.random() * 121)
};
if (limit.one < 30) {
limit.one = 30;
}
if (limit.two < 40) {
limit.two = 40;
}
if (limit.two < 20) {
limit.two = 20;
}
let calc = {};
calc.floor = 0;
calc.ceil = 0 + lineWidth;
d.forEach(function(item, i) {
if (i % lineWidth === 0) {
lineCount++;
if (lineCount > 1) {
val = !val;
}
if (lineCount === 1) {
val = !val; // setting alt scanline check to true initially
} else if (calc.floor + lineWidth < b.data.length - 1) {
calc.floor += lineWidth;
calc.ceil += lineWidth;
}
currLine = b.data.slice(calc.floor, calc.ceil); // current line
track = val ? lineWidth : 0; // tracking variable for reading from scanline
track2 = val ? 4 : 0; // tracking variable for writing from scanline
}
//check if reversed and writing variable has written 4 bytes for RGBA
//if so, set writing source to 4 bytes at end of line and read from there incrementally
if (val) {
if (track2 === 4) {
track2 = 0; // reset writing count
curr = currLine.slice(track - 4, track); // store 4 previous bytes as writing source
if (lineCount === 1 && lineWidth - track < 30) console.log(curr); //debug
}
} else {
curr = currLine; //set normal scanline
}
d[i] = curr[track2];
// check if there is no match between data source and decoded image
if (d[i] !== curr[track2]) {
if (track3 < 50) {
console.log(i);
}
track3++;
}
track2++; //update tracking variable
track = val ? track - 1 : track + 1; //update tracking variable
});
var rawImageData = {
data: d,
width: b.width,
height: b.height
};
console.log(b.data.length);
console.log('errors\t', track3);
var jpegImageData = jpeg.encode(rawImageData, 100);
fs.writeFile(output, jpegImageData.data);
}
flipAlt('input.jpg', 'output.jpg');
Instead of tracking array indices, you can use utility library like lodash, it should make things easier:
function flipAlt(input, output) {
const fs = require('fs');
const jpeg = require('jpeg-js');
const _ = require('lodash');
const image = jpeg.decode(fs.readFileSync(input));
const lines = _.chunk(image.data, image.width*4);
const flipped = _.flatten(lines.map((line, index) => {
if (index % 2 != 0) {
return line;
}
const pixels = _.chunk(line, 4);
return _.flatten(pixels.reverse());
}));
const imageData = jpeg.encode({
width: image.width,
height: image.height,
data: new Buffer(flipped)
}, 100).data;
fs.writeFile(output, imageData);
}
flipAlt('input.jpg', 'output.jpg');

Resources