I'm trying to draw speech buble with dragable handler.
That's what I have:
(x,y) - coordinates of the lowest left corner of buble
length of the buble
width of the buble
(x1,y1) coorinates of the handler end
Here is the picture for better understanding:
I know how to draw it in canvas when all coordinates are known. It's pretty simple.
Tutorial
function drawBubble(ctx, x, y, w, h, radius)
{
var r = x + w;
var b = y + h;
ctx.beginPath();
ctx.strokeStyle="black";
ctx.lineWidth="2";
ctx.moveTo(x+radius, y);
ctx.lineTo(x+radius/2, y-10);
ctx.lineTo(x+radius * 2, y);
ctx.lineTo(r-radius, y);
ctx.quadraticCurveTo(r, y, r, y+radius);
ctx.lineTo(r, y+h-radius);
ctx.quadraticCurveTo(r, b, r-radius, b);
ctx.lineTo(x+radius, b);
ctx.quadraticCurveTo(x, b, x, b-radius);
ctx.lineTo(x, y+radius);
ctx.quadraticCurveTo(x, y, x+radius, y);
ctx.stroke();
}
Example in jsFiddle
But the trouble is - how to find coordinates of red dots shown on the picture. Both (x,y) and (x1,y1) are known but change when user drags buble or handler. And in all cases handler should look pretty.
Whould be great if anyone could share the code, it's kinda complicated for me.
Thanks in advance!
You can preserve the corners and draw the pointing bit fixed to a given point. You just need to calculate the correct connection points.
// This is an example with the connection points 20px apart.
// The px and py variables here come from the onmousemove event.
// Finally, this part here is only for the top part of the bubble,
// you have watch for 4 different scenarios, depending on where
// the mouse is and thus what the pointing bit should aim for.
...
var con1 = Math.min(Math.max(x+radius,px-10),r-radius-20);
var con2 = Math.min(Math.max(x+radius+20,px+10),r-radius);
...
if(py < y) dir = 2;
...
ctx.moveTo(x+radius,y);
if(dir==2){
ctx.lineTo(con1,y);
ctx.lineTo(px,py);
ctx.lineTo(con2,y);
ctx.lineTo(r-radius,y);
}
else ctx.lineTo(r-radius,y);
ctx.quadraticCurveTo(r,y,r,y+radius);
...
Like this:
Draggable Bubble
Try clicking on the bubble to drag the pointer.
The handle is already calculated for you so it's simply a matter of preserving its coordinates by doing for example this modification:
function drawBubble(ctx, x, y, w, h, radius) {
...snipped...
var handle = {
x1: x + radius,
y1: y,
x2: x + radius / 2,
y2: y - 10,
x3: x + radius * 2,
y3: y
}
ctx.moveTo(handle.x1, handle.y1);
ctx.lineTo(handle.x2, handle.y2);
ctx.lineTo(handle.x3, handle.y3);
...snipped...
return handle;
}
Modified fiddle here
This is one way to get the coordinates for the handle.
To take it one step further we can modify the above function to also take a handle parameter.
This way you can choose to feed a handle setting or use a default calculated one:
function drawBubble(ctx, x, y, w, h, radius, handle) {
...snipped...
/// use given handle settings or calculate a default one:
handle = handle || {
x1: x + radius,
y1: y,
x2: x + radius / 2,
y2: y - 10,
x3: x + radius * 2,
y3: y
}
ctx.moveTo(handle.x1, handle.y1);
ctx.lineTo(handle.x2, handle.y2);
ctx.lineTo(handle.x3, handle.y3);
...snipped...
return handle;
}
In order to use this you first obtain a default calculate handle by passing in for example null or false to the function.
Then use those coordinates to draw the positions around. For each move clear and redraw the canvas but this time feed the modified handle parameters to the function:
/// first time:
var handle = null, /// first time use default handle
...;
handle = drawBubble(ctx, x, y, w, h, radius, handle);
Then in your mouse operations:
/// modify and update bubble:
handle = drawBubble(ctx, x, y, w, h, radius, handle);
Hope this helps!
Related
I have an image which is a background containing a boxed area like this:
I know the exact positions of the corners of that shape, and I'd like to place another image within it. (So it appears to be inside the box).
I'm aware of the drawImage method for HTML5 canvas, but it seems to only support x, y, width, height parameters rather than exact coordinates. How might I draw an image onto a canvas at a specific set of coordinates, and ideally have the browser itself handle stretching the image.
Quadrilateral transform
One way to go about this is to use Quadrilateral transforms. They are different than 3D transforms and would allow you to draw to a canvas in case you want to export the result.
The example shown here is simplified and uses basic sub-divison and "cheats" on the rendering itself - that is, it draws in a small square instead of the shape of the sub-divided cell but because of the small size and the overlap we can get away with it in many non-extreme cases.
The proper way would be to split the shape into two triangles, then scan pixel wise in the destination bitmap, map the point from destination triangle to source triangle. If the position value was fractional you would use that to determine pixel interpolation (f.ex. bi-linear 2x2 or bi-cubic 4x4).
I do not intend to cover all this in this answer as it would quickly become out of scope for the SO format, but the method would probably be suitable in this case unless you need to animate it (it is not performant enough for that if you want high resolution).
Method
Lets start with an initial quadrilateral shape:
The first step is to interpolate the Y-positions on each bar C1-C4 and C2-C3. We're gonna need current position as well as next position. We'll use linear interpolation ("lerp") for this using a normalized value for t:
y1current = lerp( C1, C4, y / height)
y2current = lerp( C2, C3, y / height)
y1next = lerp(C1, C4, (y + step) / height)
y2next = lerp(C2, C3, (y + step) / height)
This gives us a new line between and along the outer vertical bars.
Next we need the X positions on that line, both current and next. This will give us four positions we will fill with current pixel, either as-is or interpolate it (not shown here):
p1 = lerp(y1current, y2current, x / width)
p2 = lerp(y1current, y2current, (x + step) / width)
p3 = lerp(y1next, y2next, (x + step) / width)
p4 = lerp(y1next, y2next, x / width)
x and y will be the position in the source image using integer values.
We can use this setup inside a loop that will iterate over each pixel in the source bitmap.
Demo
The demo can be found at the bottom of the answer. Move the circular handles around to transform and play with the step value to see its impact on performance and result.
The demo will have moire and other artifacts, but as mentioned earlier that would be a topic for another day.
Snapshot from demo:
Alternative methods
You can also use WebGL or Three.js to setup a 3D environment and render to canvas. Here is a link to the latter solution:
Three.js
and an example of how to use texture mapped surface:
Three.js texturing (instead of defining a cube, just define one place/face).
Using this approach will enable you to export the result to a canvas or an image as well, but for performance a GPU is required on the client.
If you don't need to export or manipulate the result I would suggest to use simple CSS 3D transform as shown in the other answers.
/* Quadrilateral Transform - (c) Ken Nilsen, CC3.0-Attr */
var img = new Image(); img.onload = go;
img.src = "https://i.imgur.com/EWoZkZm.jpg";
function go() {
var me = this,
stepEl = document.querySelector("input"),
stepTxt = document.querySelector("span"),
c = document.querySelector("canvas"),
ctx = c.getContext("2d"),
corners = [
{x: 100, y: 20}, // ul
{x: 520, y: 20}, // ur
{x: 520, y: 380}, // br
{x: 100, y: 380} // bl
],
radius = 10, cPoint, timer, // for mouse handling
step = 4; // resolution
update();
// render image to quad using current settings
function render() {
var p1, p2, p3, p4, y1c, y2c, y1n, y2n,
w = img.width - 1, // -1 to give room for the "next" points
h = img.height - 1;
ctx.clearRect(0, 0, c.width, c.height);
for(y = 0; y < h; y += step) {
for(x = 0; x < w; x += step) {
y1c = lerp(corners[0], corners[3], y / h);
y2c = lerp(corners[1], corners[2], y / h);
y1n = lerp(corners[0], corners[3], (y + step) / h);
y2n = lerp(corners[1], corners[2], (y + step) / h);
// corners of the new sub-divided cell p1 (ul) -> p2 (ur) -> p3 (br) -> p4 (bl)
p1 = lerp(y1c, y2c, x / w);
p2 = lerp(y1c, y2c, (x + step) / w);
p3 = lerp(y1n, y2n, (x + step) / w);
p4 = lerp(y1n, y2n, x / w);
ctx.drawImage(img, x, y, step, step, p1.x, p1.y, // get most coverage for w/h:
Math.ceil(Math.max(step, Math.abs(p2.x - p1.x), Math.abs(p4.x - p3.x))) + 1,
Math.ceil(Math.max(step, Math.abs(p1.y - p4.y), Math.abs(p2.y - p3.y))) + 1)
}
}
}
function lerp(p1, p2, t) {
return {
x: p1.x + (p2.x - p1.x) * t,
y: p1.y + (p2.y - p1.y) * t}
}
/* Stuff for demo: -----------------*/
function drawCorners() {
ctx.strokeStyle = "#09f";
ctx.lineWidth = 2;
ctx.beginPath();
// border
for(var i = 0, p; p = corners[i++];) ctx[i ? "lineTo" : "moveTo"](p.x, p.y);
ctx.closePath();
// circular handles
for(i = 0; p = corners[i++];) {
ctx.moveTo(p.x + radius, p.y);
ctx.arc(p.x, p.y, radius, 0, 6.28);
}
ctx.stroke()
}
function getXY(e) {
var r = c.getBoundingClientRect();
return {x: e.clientX - r.left, y: e.clientY - r.top}
}
function inCircle(p, pos) {
var dx = pos.x - p.x,
dy = pos.y - p.y;
return dx*dx + dy*dy <= radius * radius
}
// handle mouse
c.onmousedown = function(e) {
var pos = getXY(e);
for(var i = 0, p; p = corners[i++];) {if (inCircle(p, pos)) {cPoint = p; break}}
}
window.onmousemove = function(e) {
if (cPoint) {
var pos = getXY(e);
cPoint.x = pos.x; cPoint.y = pos.y;
cancelAnimationFrame(timer);
timer = requestAnimationFrame(update.bind(me))
}
}
window.onmouseup = function() {cPoint = null}
stepEl.oninput = function() {
stepTxt.innerHTML = (step = Math.pow(2, +this.value));
update();
}
function update() {render(); drawCorners()}
}
body {margin:20px;font:16px sans-serif}
canvas {border:1px solid #000;margin-top:10px}
<label>Step: <input type=range min=0 max=5 value=2></label><span>4</span><br>
<canvas width=620 height=400></canvas>
You can use CSS Transforms to make your image look like that box. For example:
img {
margin: 50px;
transform: perspective(500px) rotateY(20deg) rotateX(20deg);
}
<img src="https://via.placeholder.com/400x200">
Read more about CSS Transforms on MDN.
This solution relies on the browser performing the compositing. You put the image that you want warped in a separate element, overlaying the background using position: absolute.
Then use CSS transform property to apply any perspective transform to the overlay element.
To find the transform matrix you can use the answer from: How to match 3D perspective of real photo and object in CSS3 3D transforms
I am looking for a way to draw multiple rectangles around a circle at an angle facing the centre. What i have so far, currently just drawing the rectangles around a circle facing one direction lacking the angle inclination towards the centre -
https://thysultan.com/projects/thyplayer/
What i want is for the rectangles to incline at an angle such that each rectangle is facing the centre of the circle at it's designated position.
How would one do that?
Trigonometry approach
To animate the lines towards the center you can use either transforms or trigonometry math. Personally I find it easier with math in cases like this so here is such an example.
See #markE's answer for an example on how to use transforms (transforms can be easier on the eye and in the code in general).
Some prerequisites:
We know canvas is oriented so that a 0° angle points towards right. This is essential if you want to mark the frequency range somehow.
We need to calculate the length of the line from outer radius towards center (inner radius)
We need to calculate the end points of the line based on the angle.
An easy solution can be to make a function that calculates a single line at a center (cx, cy) at a certain angle (in radians), with t as length, t being in the range [0, 1] (as the FFT floating point buffer bins). We also provide an outer and inner radius to enable limiting the line:
function getLine(cx, cy, angle, t, oRadius, iRadius) {
var radiusDiff = oRadius - iRadius, // calc radius diff to get max length
length = radiusDiff * t; // now we have the line length
return {
x1: oRadius * Math.cos(angle), // x1 point (outer)
y1: oRadius * Math.sin(angle), // y1 point (outer)
x2: (oRadius - length) * Math.cos(angle), // x2 point (inner)
y2: (oRadius - length) * Math.sin(angle) // y2 point (inner)
}
}
All we need to do now is to feed it data from the FFT analyzer.
NB: Since all lines points towards center, you will have a crowded center area. Something to have in mind when determine the line widths and inner radius as well as number of bins to use.
Example demo
For the example I will just use some random data for the "FFT" and plot 64 bins.
// angle - in radians
function getLine(cx, cy, angle, t, oRadius, iRadius) {
var radiusDiff = oRadius - iRadius, // calc radius diff to get max length
length = radiusDiff * t; // now we have the line length
return {
x1: cx + oRadius * Math.cos(angle), // x1 point (outer)
y1: cy + oRadius * Math.sin(angle), // y1 point (outer)
x2: cx + (oRadius - length) * Math.cos(angle), // x2 point (inner)
y2: cy + (oRadius - length) * Math.sin(angle) // y2 point (inner)
}
}
// calculate number of steps based on bins
var ctx = document.querySelector("canvas").getContext("2d"),
fftLength = 64,
angleStep = Math.PI * 2 / fftLength,
angle = 0,
line;
ctx.beginPath(); // not needed in demo, but when animated
while(angle < Math.PI*2) {
// our line function in action:
line = getLine(250, 250, angle, getFFT(), 240, 50);
ctx.moveTo(line.x1, line.y1); // add line to path
ctx.lineTo(line.x2, line.y2);
angle += angleStep // get next angle
}
ctx.lineWidth = 5; // beware of center area
ctx.stroke(); // stroke all lines at once
// to smooth the "FFT" random data
function getFFT() {return Math.random() * 0.16 + 0.4}
<canvas width=500 height=500></canvas>
I am trying to label some circles in a circle pack layout with text that flows along the circle itself.
Here is one experimental jsfiddle:
As you can see, it is possible to render text along the circle, centered at its top. Though browser's rendering of curved SVG text is terrible. But let's say we don't care about it.
Here is another jsfiddle
I would like to place curved labels on this graph, under these conditions:
The circle represents a province (only depth==1) (BRITISH COLUMBIA, ALBERTA, and so forth)
The sum of sizes of all children (in other words, number of parliament seats allotted) of the province is greater than 5.
The name of the province should be all UPPERCASE.
You can see some of my attempts in the code itself. I have been trying for hours. My main problem is that circles in the circle are now somewhere in X Y space, whereas, in the first jsfiddle, all circles have centers in coordinate system origin.
Maybe you can help me by taking a fresh look at this.
Underlying data is based on this table:
(NOTE: This is somewhat related to the question 'Circle packs as nodes of a D3 force layout' I asked the other day, however this is an independent experiment.)
I decided to use regular SVG arcs instead of d3.svg.arc(). I still think it is a right decision. However, here is what I have now: :) jsfiddle
NOTE (since I am answering my question): If some of you already spent time on this problem and found another solution, please post it, and I will accept your answer. Thanks to #FernOfTheAndes for participating in process of finding this solution, as it was filled with pain and misery of working with svg arcs.
Here is jsfiddle of the solution:
As mentioned in comments, the key part was generating arcs as plain vanilla svg arcs, not via d3.svg.arc().
SVG rules for defining arcs are clear, but a little hard to manage. Here is an interactive explorer of svg syntax for arcs.
Also, these two functions helped me during this process of defining the right arcs:
function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
var angleInRadians = (angleInDegrees-90) * Math.PI / 180.0;
return {
x: centerX + (radius * Math.cos(angleInRadians)),
y: centerY + (radius * Math.sin(angleInRadians))
};
}
function describeArc(x, y, radius, startAngle, endAngle){
var start = polarToCartesian(x, y, radius, endAngle);
var end = polarToCartesian(x, y, radius, startAngle);
var arcSweep = endAngle - startAngle <= 180 ? "0" : "1";
var d = [
"M", start.x, start.y,
"A", radius, radius, 0, 1, 1, end.x, end.y
].join(" ");
return d;
}
This is the code that actually directly generates curved labels:
var arcPaths = vis.append("g")
.style("fill","navy");
var labels = arcPaths.append("text")
.style("opacity", function(d) {
if (d.depth == 0) {
return 0.0;
}
if (!d.children) {
return 0.0;
}
var sumOfChildrenSizes = 0;
d.children.forEach(function(child){sumOfChildrenSizes += child.size;});
//alert(sumOfChildrenSizes);
if (sumOfChildrenSizes <= 5) {
return 0.0;
}
return 0.8;
})
.attr("font-size",10)
.style("text-anchor","middle")
.append("textPath")
.attr("xlink:href",function(d,i){return "#s"+i;})
.attr("startOffset",function(d,i){return "50%";})
.text(function(d){return d.name.toUpperCase();})
Fortunately, centering text on an arc was just a matter of setting the right property.
Just updating some of VividD's code.
The describeArc function was not working correctly for arcs with less than 180 degrees, and the start and end variables was flipped on the function and when calling it.
Here is an updated describeArc function that handles all alternatives of arcs, including small arcs and upside-down arcs:
function describeArc(x, y, radius, startAngle, endAngle) {
var start = polarToCartesian(x, y, radius, startAngle);
var end = polarToCartesian(x, y, radius, endAngle);
var arcLength = endAngle - startAngle;
if (arcLength < 0) arcLength += 360;
var longArc = arcLength >= 180 ? 1 : 0;
var d = [
"M", start.x, start.y,
"A", radius, radius, 0, longArc, 1, end.x, end.y
].join(" ");
return d;
}
With this change the function should be called with reversed start and end values that make more sense:
describeArc(d.x, d.y, d.r, -160, 160);
0 to 0,-70 by this :
ctx.strokeStyle = "red";
ctx.lineWidth = 2;
ctx.rotate(Math.PI/-10;);
ctx.beginPath();
ctx.moveTo(0,0);
ctx.lineTo(0,-70);
ctx.stroke();
And I can rotate that by 'PI/-10', and that works.
How i can get the x,y points of this after using rotate?
Your x and y points will still be 0 and -70 as they are relative to the translation (rotation). It basically means you would need to "reverse engineer" the matrix to get the resulting position you see on the canvas.
If you want to calculate a line which goes 70 pixels at -10 degrees you can use simple trigonometry to calculate it yourself instead (which is easier than going sort of backwards in the matrix).
You can use a function like this that takes your context, the start position of the line (x, y) the length (in pixels) and angle (in degrees) of the line you want to draw. It draw the line and returns an object with x and y for the end point of that line:
function lineToAngle(ctx, x1, y1, length, angle) {
angle *= Math.PI / 180;
var x2 = x1 + length * Math.cos(angle),
y2 = y1 + length * Math.sin(angle);
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
return {x: x2, y: y2};
}
Then just call it as:
var pos = lineToAngle(ctx, 0, 0, 70, -10);
//show result of end point
console.log('x:', pos.x.toFixed(2), 'y:', pos.y.toFixed(2));
Result:
x: 68.94 y: -12.16
Or you can instead extend the canvas' context by doing this:
if (typeof CanvasRenderingContext2D !== 'undefined') {
CanvasRenderingContext2D.prototype.lineToAngle =
function(x1, y1, length, angle) {
angle *= Math.PI / 180;
var x2 = x1 + length * Math.cos(angle),
y2 = y1 + length * Math.sin(angle);
this.moveTo(x1, y1);
this.lineTo(x2, y2);
return {x: x2, y: y2};
}
}
And then use it directly on your context like this:
var pos = ctx.lineToAngle(100, 100, 70, -10);
ctx.stroke(); //we stroke separately to allow this being used in a path
console.log('x:', pos.x.toFixed(2), 'y:', pos.y.toFixed(2));
(0 degrees will point to the right).
So you're asking "after I set a transform, how can I run points through that transform"?
In that case, see HTML5 Canvas get transform matrix? . The question and answers are somewhat old, but seem up-to-date. I can't find anything in the current HTML5 spec that lets you access and use the transform matrix. (I see that it's theoretically accessable through context.currentTransform, but I don't see any functionality to let you use the matrix - you'd have to multiply your point through the matrix yourself, or fake it by creating a full SVGMatrix for your point vector.)
The top answer shows a transform class you can use. Track your changes through that, and use their transformPoint function to get the point you want transformed to its endpoint.
I'd like to create a circle ctx.arc(10, 10, 15, 0, Math.PI*2, true); and hence make it flow downwards without losing its traces..
You can see this clearly in the below image -
Here as we can see on the dark side...the circle is actually moving as time passes.. I want to control the speed by entering the time...like from top to bottom it should reach in 2 seconds (or some other way to control its speed of flow)
EDIT: Sorry for some buddies the question is: whats the most efficient and "NON-memory-hogging" method to do this, (I know there is this loop method but its very memory hogging method)
Javascript knows setTimeout(fn, ms), which is going to call the given function after the given number of milliseconds. However, the browser will need some time to render your drawings, and when you want the 2 seconds to be exakt you will have to watch about your frames/seconds rate.
Theres also a new specification draft called requestAnimationFrame, see http://paulirish.com/2011/requestanimationframe-for-smart-animating/ for that.
You can draw the top semicircle, a rectangle, and the bottom semicircle. You can calculate how much it should be moved down at a particular moment in time with:
(Date.now() - startTime) / t * (y2 - y1)
where t is the time it should take for the complete movement.
http://jsfiddle.net/eGjak/231/.
var ctx = $('#cv').get(0).getContext('2d');
var y1 = 100, // start y
y2 = 300, // end y
x = 200, // x
r = 50, // radius
t = 2000; // time
var dy = y2 - y1, // difference in y
pi = Math.PI,
startTime = Date.now();
var f = function() {
var y = y1 + (Date.now() - startTime) / t * dy;
ctx.beginPath();
ctx.arc(x, y1, r, pi, 0); // top semicircle
ctx.rect(x - r, y1, r * 2, y - y1); // rectangle
ctx.arc(x, y, r, 0, -pi); // bottom semicircle
ctx.fill();
if(y < y2) { // if should be moved further
webkitRequestAnimationFrame(f);
}
};
f();