Canvas, draw (stretch) image between few points with Javascript without libraries - javascript

is there any way that you can stretch an image between few given points on the canvas (similar to Transform). When I use Transform , I can definitely stretch it, however it is not giving all the options that I need for what I have in mind. For example, if I am to do a game that should generate objects , such as boxes, I want to make sure that depending on where the player is located, to turn the image on the side of the boxes and transform them as if you are moving around them. In transform, I can do that, however it will always keep the same height on both sides of the image (left and right) while I need to have more control to decrease the height on the right side of the image only (per say). Much appreciated if you can give a hint on how to do that without using any library.
Here is an example on what I have so far (though again the second image on the right still keeps the same height on the it's far right side same as the one on it's left side):
<!DOCTYPE html>
<html lang="en-us">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Reshape images</title>
</head>
<body style="margin:10px;">
<canvas id="canvas" style="background:#000;"></canvas>
<script type="text/javascript">
// INITIAL SETUP
var canvas = document.getElementById('canvas'),
ctx = canvas.getContext('2d');
var innerWidth = 1000,
innerHeight = 650;
canvas.width = innerWidth;
canvas.height = innerHeight;
var img = new Image;
img.onload = function() {
var tile1 = [
{x: 10, y: 10}, // upper left corner
{x: 210, y: 50}, // upper right
{x: 230, y: 150}, // bottom right
{x: 30, y: 110} // bottom left
],
tile2 = [
{x: 210, y: 50},
{x: 410, y: 5},
{x: 430, y: 105},
{x: 230, y: 150}
];
renderTile(this, tile1);
renderTile(this, tile2);
function renderTile(img, tile) {
var dx, dy, a1, a2, w, h, i = 1;
// calc horizontal angle
dx = tile[1].x - tile[0].x; // horizontal diff.
dy = tile[1].y - tile[0].y; // vertical diff.
a1 = Math.atan2(dy, dx); // angle, note dy,dx order here
w = dx|0; // width based on diff for x
// calc vertical angle
dx = tile[3].x - tile[0].x;
dy = tile[3].y - tile[0].y;
a2 = Math.atan2(dx, dy); // note dx,dy order here
h = dy|0;
// draw image to fit parallelogram
// ctx.setTransform(1, a1, a2, 1, tile[0].x, tile[0].y);
ctx.setTransform(1, a1, a2, 1, tile[0].x, tile[0].y);
ctx.drawImage(img, 0, 0, w, h);
}
};
img.src = "http://i.imgur.com/rUeQDjE.png";
</script>
<script src="src.js"></script>
</body>
</html>

Related

How to Draw Lines if a Condition is Satisfied and Save All Lines

I am using Node.Js and attempting to draw lines around portions of an image whenever a condition proves true. I'm essentially trying to outline objects in an image response from an API. I have a response coming in from an API and whenever that response contains "WORD", I would like to draw two lines that encase a portion of an image. At the end, I would like to save all the lines that were drawn and export the image, now with the lines drawn on it.
I have managed to get the response from the API, loop through the objects in the response, and check to see if the objects match a filtering condition. I have then managed to draw one set of lines, but I cannot determine how to draw the lines everytime the condition is satifised and save all the resulting drawings. The resulting image only has a single group of lines drawn on it. I am using the Images package as well as Canvas.
// get image
var ImageDATA = await getImage()
// Get the height, width of the image
const dimensions = sizeOf(ImageDATA.Body)
const width = dimensions.width
const height = dimensions.height
console.log(ImageDATA.Body)
console.log(width, height)
try{
// Call API and log response
const res = await client.detectDocumentText(params).promise();
// set the response as an image and get width and height
var image = images(ImageDATA.Body).size(width, height)
//console.log(res)
res.Blocks.forEach(block => {
if (block.BlockType.indexOf('WORD') > -1)
{
//console.log("Word Geometry Found.");
console.log("FOUND POLYGONS")
ctx.strokeStyle = 'rgba(0,0,0,0.5)';
console.log(block.Geometry.Polygon[0].X)
ctx.beginPath();
ctx.lineTo(width * block.Geometry.Polygon[3].X, height * block.Geometry.Polygon[3].Y);
ctx.moveTo(width * block.Geometry.Polygon[1].X, height * block.Geometry.Polygon[1].Y);
ctx.lineTo(width * block.Geometry.Polygon[2].X, height * block.Geometry.Polygon[2].Y);
ctx.stroke();
}
console.log("-----")
})
// render image
// convert canvas to buffer
var buffer = canvas.toBuffer("image/png");
// draw the buffer onto the image
image.draw(images(buffer), 10, 10)
// save image
image.save("output.jpg");
} catch (err){
console.error(err);}
Here's a sample of the Polygon array:
[
{ X: 0.9775164723396301, Y: 0.985478401184082 },
{ X: 0.9951508641242981, Y: 0.985478401184082 },
{ X: 0.9951508641242981, Y: 0.9966437816619873 },
{ X: 0.9775164723396301, Y: 0.9966437816619873 }
]
It defines the boundary starting from the top-left and moving clockwise.
If anyone knows how to achieve this, I would be extremely grateful. Thank you so much in advance.
Try this :
ctx.strokeStyle = 'rgba(0,0,0,0.5)';
ctx.beginPath();
block.Geometry.Polygon.forEach(({X, Y}) =>
ctx.lineTo(width * X, height * Y)
);
ctx.closePath();
ctx.stroke();
Here is a working example:
const boudingBoxes = [
{
label: "Pen",
polygon: [
{x: 0.60, y: 0.64},
{x: 0.83, y: 0.66},
{x: 0.82, y: 0.70},
{x: 0.60, y: 0.70},
]
},
{
label: "Camera",
polygon: [
{x: 0.72, y: 0.20},
{x: 0.93, y: 0.25},
{x: 0.88, y: 0.43},
{x: 0.71, y: 0.39},
]
},
]
init();
async function init() {
const image = new Image();
image.crossOrigin = "";
await new Promise(res => {
image.onload = res;
image.src = "https://picsum.photos/id/180/600/400";
});
const [width, height] = [image.naturalWidth, image.naturalHeight];
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
// Draw the image
canvas.width = width;
canvas.height = height;
ctx.drawImage(image, 0, 0);
// Start Drawing the bounding boxes
ctx.fillStyle = "red"
ctx.strokeStyle = "red";
boudingBoxes.forEach(bBox => {
// label
ctx.font = "13px Verdana";
ctx.fillText(bBox.label, width * bBox.polygon[0].x, height * bBox.polygon[0].y - 6);
// Bounding box
ctx.beginPath();
bBox.polygon.forEach(({x, y}) =>
ctx.lineTo(width * x, height * y)
);
ctx.closePath();
ctx.stroke();
});
document.body.appendChild(canvas);
}
// TMP
const p = document.querySelector("p");
window.onmousemove = (e) => {
const x = e.clientX / 600;
const y = e.clientY / 400;
p.innerHTML = `x: ${x} <br/> y: ${y}`;
}
body {
margin: 0
}
p {position: absolute }
<p></p>

PutImageData() not drawing ImageData

I want to make a fractal generator that uses this method: https://www.johndcook.com/blog/2017/07/08/the-chaos-game-and-the-sierpinski-triangle/
It's really interesting and I wanted to try it on squares and pentagons etc.
The result of the Code below should look like a Sierpinski triangle but unfortunately the canvas stays blank :/
Thx for your help :)
<!DOCTYPE html>
<html>
<head>
<title>sierpinksi polygons</title>
</head>
<body>
<canvas width="500" height="500" id="canvas"></canvas>
<script>
let canavas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
let imgdata = ctx.createImageData(500, 500);
let data = imgdata.data;
let vertices = [{x: 0, y: 0}, {x: 500, y: 0}, {x: 250, y: 433}];
function pixel(x, y) {
data[4*y*canvas.width+4*x + 3] = 1; //sets alpha to 1 which makes the pixel black
}
function random() {
return vertices[Math.floor(Math.random()*vertices.length)];
}
let point = {x: 0, y: 0};
for (let i = 0; i < 10000; i++) {
let temp = random();
//algorithm to draw sierpinksi triangle pixel by pixel
point.x = point.x + (temp.x - point.x)/2;
point.y = point.y + (temp.y - point.y)/2;
pixel(Math.round(point.x), Math.round(point.y));
}
console.log(imgdata)
ctx.putImageData(imgdata, 0, 0);
</script>
</body>
</html>
data[4*y*canvas.width+4*x + 3] = 1; //sets alpha to 1 which makes the pixel black
No, it doesn’t - the A component of the RGBA value has to be in the range of 0 to 255 as well, 255 matching what opacity: 1 in CSS would mean.
With 1 you have given your pixels an opacity of 1/255 here, which is really only just 0.00392156862
You want to use 255 here to get full black pixels.

Javascript bezierCurveTo() draws different curvature depending on lineWidth

I`m trying to draw a long (actually very long) bezier lines on my graph. When hover over line it should become thicker. And i encountered an issue. Sometimes lines has different curvature depending on lineWidth in Chrome.
Code exapmle
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
ctx.setTransform(1, 0, 0, 1, -3700, -50)
const gap = 32;
const line = [
{x: 3830, y: 95},
{x: 3830 + gap, y: 95},
{x: 12600 - gap, y: 25895},
{x: 12600, y: 25895}
];
// Draw bezier out of box
function drawLine(p1, a1, a2, p2, width, color) {
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.beginPath();
ctx.moveTo(p1.x,p1.y);
ctx.bezierCurveTo(a1.x, a1.y, a2.x, a2.y, p2.x, p2.y);
ctx.stroke();
}
drawLine.apply(null, line.concat([3, '#f00']));
drawLine.apply(null, line.concat([1, '#00f']));
JSFiddle
In Safari it looks fine, but Chrome and FF draw the same lines with the same points different.
Looks like curvature depending on lineWidth.
May be someone knows how to solve this problem?

How to shade a portion of the background of ChartJS bubble chart?

I managed to figure out how to enhance the solutions I've found here in stackoverflow for adding vertical and horizontal lines to a line chart in ChartJS v2+ to apply to a bubble chart ChartJS Bubble w/Lines (I advise skipping to my Update at the end for a better approach)
var originalBubbleDraw = Chart.controllers.bubble.prototype.draw;
Chart.helpers.extend(Chart.controllers.bubble.prototype, {
draw: function() {
originalBubbleDraw.apply(this, arguments);
var chart = this.chart;
var ctx = chart.chart.ctx;
var xaxis = chart.scales['x-axis-0'];
var yaxis = chart.scales['y-axis-0'];
var xvalue = chart.config.data.queryLimits['x'];
var yvalue = chart.config.data.queryLimits['y'];
var xcolor = chart.config.data.queryLimits['xcolor'];
var ycolor = chart.config.data.queryLimits['ycolor'];
var lineThickness = 3;
function drawLine(x1,y1,x2,y2,color) {
console.log("color="+color+", x1="+x1+", x2="+x2+", y1="+y1+", y2="+y2);
ctx.save();
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.strokeStyle = color;
ctx.lineWidth=lineThickness;
ctx.lineTo(x2, y2);
ctx.stroke();
ctx.restore();
}
// draw vertical line
if (xvalue) {
x1 = xaxis.getPixelForValue(xvalue);
x2 = xaxis.getPixelForValue(xvalue);
y1 = yaxis.top;
y2 = yaxis.bottom;
drawLine(x1,y1,x2,y2,xcolor);
}
// draw horizontal line
if (yvalue) {
x1 = xaxis.left;
x2 = xaxis.right;
y1 = yaxis.getPixelForValue(yvalue);
y2 = yaxis.getPixelForValue(yvalue);
drawLine(x1,y1,x2,y2,ycolor);
}
}
});
var config = {
type: 'bubble',
data: {
queryLimits: {x: 42, y: 21, xcolor: '#00FF00', ycolor: '#0000ff'},
datasets: [
{
label: '',
data: [
{x: 20, y: 30, r: 15},
{x: 40, y: 10, r: 10},
{x: 100, y: 15, r: 10},
{x: 50, y: 22, r: 5},
{x: 80, y: 26, r: 3},
{x: 63, y: 28, r: 10},
{x: 71, y: 18, r: 12}
],
backgroundColor:"#FF6384",
hoverBackgroundColor: "#FF6384",
}]
}
};
var ctx = document.getElementById("myChart").getContext("2d");
new Chart(ctx, config);
I need to take this one step further and shade the area/background of the chart on one side of vertical line and above or below the horizontal line a different color (like a light gray or something subtle).
I am not sure if the approach is to try to change a portion of the background or to add rectangles sized and positioned to simulate the background shading.
Ideas?
Here is an example mockup of the goal:
Update
For what it is worth for those viewing this in the future, I ended up discovering a better way to approach both the drawing of lines and rectangles by using the annotation plugin for chartjs found here https://github.com/chartjs/chartjs-plugin-annotation. It is far easier to work with and doesn't have the consequence of firing the code to draw the lines and rectangles more than necessary. Also I ended up able to use another plugin https://github.com/compwright/chartjs-plugin-draggable for dragging annotations created from that first plugin. I am leaving the accepted answer as-is because it does answer the question I had from the context of how to solve the rectangle shaded area following the original extension approach, however I recommend the plugin approach vs that now after learning more about this.
As you did with your queryLimits attribute, you can do the same way an attribute that fills the portion you want.
By adding the following attribute to your chart dataset :
fillBackground: {
// In this porperty, add the string portion you want to fill
// Inputs are : "tr" for top-right
// "tl" for top-left
// "br" for bottom-right
// "bl" for bottom-left
pos: ["tr", "bl", "br"],
// A single color will be used in all the portions
// But you can also set an array of colors which must have the same length as the pos
// i.e color: ["rgba(0, 0, 0, 0.1)", "rgba(30, 30, 30, 0.3)", "rgba(60, 60, 60, 0.5)"]
color: "rgba(30, 30, 30, 0.15)"
}
Then adding the following piece of code in your draw() function :
function drawRect(x1, y1, x2, y2, color) {
ctx.save();
ctx.fillStyle = color;
ctx.fillRect(x1, y1, x2, y2, color);
ctx.restore();
}
// Checks if you have the attribute in your dataset
if (chart.config.data.fillBackground) {
// Make sure you have portions in your chart
if (!xvalue || !yvalue) return;
var pos = chart.config.data.fillBackground.pos;
var color = chart.config.data.fillBackground.color;
// For every position in your array ..
for (p in pos) {
// Based on the string code, fills the right portion
switch (pos[p]) {
case "tl":
drawRect(xaxis.left, yaxis.top, xaxis.getPixelForValue(xvalue) - lineThickness / 2 - xaxis.left, yaxis.getPixelForValue(yvalue) - lineThickness / 2 - yaxis.top, (Array.isArray(color)) ? color[p] : color);
break;
case "tr":
drawRect(xaxis.getPixelForValue(xvalue) + lineThickness / 2, yaxis.top, xaxis.right, yaxis.getPixelForValue(yvalue) - lineThickness / 2 - yaxis.top, (Array.isArray(color)) ? color[p] : color);
break;
case "bl":
drawRect(xaxis.left, yaxis.getPixelForValue(yvalue) + lineThickness / 2, xaxis.getPixelForValue(xvalue) - lineThickness / 2 - xaxis.left, yaxis.bottom - (yaxis.getPixelForValue(yvalue) + lineThickness / 2), (Array.isArray(color)) ? color[p] : color);
break
case "br":
drawRect(xaxis.getPixelForValue(xvalue) + lineThickness / 2, yaxis.getPixelForValue(yvalue) + lineThickness / 2, xaxis.right, yaxis.bottom - (yaxis.getPixelForValue(yvalue) + lineThickness / 2), (Array.isArray(color)) ? color[p] : color);
break;
}
}
}
This should solve your problem.
You can check your example working with these functions in this fiddle, and here is the result :

Canvas transform on cloth simulation + image

I'm using the verlet.js plugin in order create a cloth simulation on canvas with a texture image.
The only thing (and the most important BTW) part that I haven't arrived is that I need skew the drawImage in order to make it fit the correct position.
jsfiddle with the progress
//Drawing the rectangle
ctx.save();
ctx.beginPath();
ctx.moveTo(cloth.particles[i1].pos.x, cloth.particles[i1].pos.y);
ctx.lineTo(cloth.particles[i1+1].pos.x, cloth.particles[i1+1].pos.y);
ctx.lineTo(cloth.particles[i2].pos.x, cloth.particles[i2].pos.y);
ctx.lineTo(cloth.particles[i2-1].pos.x, cloth.particles[i2-1].pos.y);
ctx.lineTo(cloth.particles[i1].pos.x, cloth.particles[i1].pos.y);
ctx.strokeStyle = "#fff";
ctx.stroke();
ctx.restore();
//Wrapping the image
ctx.save();
var off = cloth.particles[i2].pos.x - cloth.particles[i1].pos.x;
//THIS IS WHAT I TRY TO SOLVE TO FIT TO THE RECTANGLES
//ctx.transform(1,0.5,0,1,0,0);
ctx.drawImage(img, cloth.particles[i1].pos.x,cloth.particles[i1].pos.y, off, off, cloth.particles[i1].pos.x,cloth.particles[i1].pos.y, off ,off);
ctx.restore();
}
I have tried to adapt other cloth simulations but without success. Any clue where I could find some info to accomplish that?
Using skew (or rather shear) to fill tiles only works if the cell is a parallelogram, as 2D affine transforms only support this shape.
Here is one approach:
Calculate angle of upper line
Calculate angle of left line
Calculate width and height of cell
In a parallelogram bottom line will equal upper line, and of course right line equals left line.
Then set these angles as skew arguments for the transform coupled with translate to the upper left corner.
Then just repeat for each cell.
Example
var img = new Image;
img.onload = function() {
var ctx = document.querySelector("canvas").getContext("2d"),
tile1 = [
{x: 10, y: 10}, // upper left corner
{x: 210, y: 50}, // upper right
{x: 230, y: 150}, // bottom right
{x: 30, y: 110} // bottom left
],
tile2 = [
{x: 210, y: 50},
{x: 410, y: 5},
{x: 430, y: 105},
{x: 230, y: 150}
];
renderTile(this, tile1);
renderTile(this, tile2);
function renderTile(img, tile) {
var dx, dy, a1, a2, w, h, i = 1;
// reference shape (remove this section):
ctx.setTransform(1,0,0,1,0,0);
ctx.moveTo(tile[0].x, tile[0].y);
while(i < 4) ctx.lineTo(tile[i].x, tile[i++].y);
ctx.closePath();
ctx.strokeStyle = "#0c0";
ctx.lineWidth = 2;
ctx.stroke();
// calc horizontal angle
dx = tile[1].x - tile[0].x; // horizontal diff.
dy = tile[1].y - tile[0].y; // vertical diff.
a1 = Math.atan2(dy, dx); // angle, note dy,dx order here
w = dx|0; // width based on diff for x
// calc vertical angle
dx = tile[3].x - tile[0].x;
dy = tile[3].y - tile[0].y;
a2 = Math.atan2(dx, dy); // note dx,dy order here
h = dy|0;
// draw image to fit parallelogram
ctx.setTransform(1, a1, a2, 1, tile[0].x, tile[0].y);
ctx.drawImage(img, 0, 0, w, h);
}
};
img.src = "http://i.imgur.com/rUeQDjE.png";
<canvas width=500 height=160/>
Note: if your cloth simulation produces other shapes than parallelograms (ie. quadrilaterals), which is very likely since this is a physics simulation, this approach won't work well. In that case you need different techniques which are more compute heavy. For this reason WebGL is a better fit. Just my two cents..

Categories