Vertically centered text in p5.js is not aligned properly - javascript

I recently started using p5.js more and noticed that when I the textAlign() function to center text, it is offset higher than the coordinates I specify when calling text(). The amount it is offset seems to be constant between different font sizes, being approximately fontSize()/12 above the y-coordinate I entered.
An example of this can be seen at this p5.js sketch, showing the default centering on the left, and the text being offset by fontSize()/12 on the right, having it be properly centered at varying text sizes.
Is there a way I could fix this issue and have all text be aligned properly without having to specify in each call of text that I want the text to be shifted down the amount of pixels needed for it to be properly centered? Is there a way I can modify the text function to have this functionality built in?
(Edit: I was able to "solve" this issue by overwriting the default text function with my own code, though I don't know if this is very good practice. A link to the fix can be found here, with the fixed text alignment in black and the default p5.js alignment in green.)

This has to do with what you think of as the vertical "center" of the glyphs. From you example it is evident that you think of the "center" as the center line of the capital letters. However, by this metric all lower case letters would appear mostly below center and it also fails to take into account glyph descent (that is, the amount of certain lower case glyphs that appear below the font baseline).
Here is an example that I think clearly demonstrates how the various vertical positioning types work.
function setup() {
createCanvas(windowWidth, windowHeight);
noLoop();
}
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
redraw();
}
function draw() {
let w = width / 4;
push();
stroke('blue');
line(0, height / 2, width, height / 2);
pop();
drawText(0, 0, w, height, TOP);
drawText(w, 0, w, height, CENTER);
drawText(w * 2, 0, w, height, BASELINE);
drawText(w * 3, 0, w, height, BOTTOM);
}
const str = 'Age';
const size = 48;
function drawText(x, y, w, h, valign) {
push();
textSize(size);
textAlign(LEFT, valign);
let tw = textWidth(str);
let yOff = 0;
switch (valign) {
case TOP:
yOff = -size / 2;
break;
case BOTTOM:
yOff = size / 2;
break;
case BASELINE:
yOff = size / 2 - textDescent();
}
stroke('limegreen');
rectMode(CENTER);
noFill();
rect(x + w / 2, y + h / 2, tw, size);
translate(x + w / 2 - tw / 2, height / 2 + yOff);
stroke('red');
line(0, 0, tw, 0);
strokeWeight(4);
point(0, 0);
noStroke();
fill('black');
text(str, 0, 0);
pop();
}
body, html {
margin: 0;
padding: 0;
overflow: hidden;
}
<script src="https://cdn.jsdelivr.net/npm/p5#1.3.1/lib/p5.js"></script>

Related

Show data labels inside donut pie chart p5js

I'm building a p5js donut chart, but I'm struggling to show the data labels in the middle. I think I have managed to get the boundaries right for it, but how would match the angle that I'm in? Or is there a way of matching just through the colours?
https://i.stack.imgur.com/enTBo.png
I have started by trying to match the boundaries of the chart to the pointer, which I managed to do using mouseX and mouseY. Any suggestions, please?
if(mouseX >= width / 2 - width * 0.2 && mouseY >= height / 2 - width * 0.2
&& mouseX <= width / 2 + width * 0.2 && mouseY <= height / 2 + width * 0.2)
{
//console.log("YAY!!! I'm inside the pie chart!!!");
}
else
{
textSize(14);
text('Hover over to see the labels', width / 2, height / 2);
}
};
[1]: https://i.stack.imgur.com/enTBo.png
While you could theoretically use the get() function to check the color of the pixel under the mouse cursor and correlate that with one of the entries in your dataset, I think you would be much better off doing the math to determine which segment the mouse is currently over. And conveniently p5.js provides helper functions that make it very easy.
In the example you showed you are only checking if the mouse cursor is in a rectangular region. But in reality you want to check if the mouse cursor is within a circle. To do this you can use the dist(x1, y1, x2, y2) function. Once you've established that the mouse cursor is over your pie chart, you'll want to determine which segment it is over. This can be done by finding the angle between a line draw from the center of the chart to the right (or whichever direction is where you started drawing the wedges), and a line drawn from the center of the chart to the mouse cursor. This can be accomplished using the angleBetween() function of p5.Vector.
Here's a working example:
const colors = ['red', 'green', 'blue'];
const thickness = 40;
let segments = {
foo: 34,
bar: 55,
baz: 89
};
let radius = 80, centerX, centerY;
function setup() {
createCanvas(windowWidth, windowHeight);
noFill();
strokeWeight(thickness);
strokeCap(SQUARE);
ellipseMode(RADIUS);
textAlign(CENTER, CENTER);
textSize(20);
centerX = width / 2;
centerY = height / 2;
}
function draw() {
background(200);
let keys = Object.keys(segments);
let total = keys.map(k => segments[k]).reduce((v, s) => v + s, 0);
let start = 0;
// Check the mouse distance and angle
let mouseDist = dist(centerX, centerY, mouseX, mouseY);
// Find the angle between a vector pointing to the right, and the vector
// pointing from the center of the window to the current mouse position.
let mouseAngle =
createVector(1, 0).angleBetween(
createVector(mouseX - centerX, mouseY - centerY)
);
// Counter clockwise angles will be negative 0 to PI, switch them to be from
// PI to TWO_PI
if (mouseAngle < 0) {
mouseAngle += TWO_PI;
}
for (let i = 0; i < keys.length; i++) {
stroke(colors[i]);
let angle = segments[keys[i]] / total * TWO_PI;
arc(centerX, centerY, radius, radius, start, start + angle);
// Check mouse pos
if (mouseDist > radius - thickness / 2 &&
mouseDist < radius + thickness / 2) {
if (mouseAngle > start && mouseAngle < start + angle) {
// If the mouse is the correct distance from the center to be hovering over
// our "donut" and the angle to the mouse cursor is in the range for the
// current slice, display the slice information
push();
noStroke();
fill(colors[i]);
text(`${keys[i]}: ${segments[keys[i]]}`, centerX, centerY);
pop();
}
}
start += angle;
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.js"></script>
I think I know the source of the problem was that #thenewbie experienced: it is the p5 library being used. I was using the p5.min.js and experiencing the same problem. Once I started using the full p5.js library, the issue was resolved and #Paul's script worked.
Here is a link I came across while researching this which put me onto the solution:
https://github.com/processing/p5.js/issues/3973
Thanks Paul for the clear explanations and code above.

How to draw one image multiple times in different direcitons?

I have an array like this, the idea is to output images in different directions and flip them vertically and horizonatly on a canvas by scaling.
[{
"pos":{
"x":411,
"y":401.5
},
"scale":{
"x":1,
"y":1
}
},{
"pos":{
"x":411,
"y":271.59625
},
"scale":{
"x":-1,
"y":1
}
}]
The problem is that I'm scaling the canvas instead of the images, the canvas is multiple times bigger than the images i'm placing on it.
images.forEach((image) => {
// center borde köars innan loopen egentligen
let pos = center(image.pos)
cc.save()
cc.scale(image.scale.x, image.scale.y)
cc.drawImage(window.video, pos.x, pos.y)
cc.restore()
})
How do I scale the image, called window.video, instead of the entire canvas?
To render a scaled image on the canvas.
function drawImage(image,x,y,scaleX,scaleY){
ctx.setTransform(scaleX, 0, 0, scaleY, x, y); // set scale and translation
ctx.drawImage(image,0,0);
}
// when done drawing images you need to reset the transformation back to default
ctx.setTransform(1,0,0,1,0,0); // set default transform
If you want the image flipped but still drawn at the same top left position use
function drawImage(image, x, y, scaleX, scaleY){
ctx.setTransform(scaleX, 0, 0, scaleY, x, y); // set scale and translation
x = scaleX < 0 ? -image.width : 0; // move drawing position if flipped
y = scaleY < 0 ? -image.height : 0;
ctx.drawImage(image, x, y);
}
And to draw about the center of the image
function drawImage(image, x, y, scaleX, scaleY){ // x y define center
ctx.setTransform(scaleX, 0, 0, scaleY, x, y); // set scale and translation
ctx.drawImage(image, -image.width / 2, -image.height / 2);
}
EDIT: As noted below, this doesn't work with negative values...
drawImage can take 2 extra arguments for sizing the image, so the following should work:
images.forEach((image) => {
// center borde köars innan loopen
let pos = center(image.pos)
cc.save()
cc.drawImage(window.video, pos.x, pos.y, window.video.width * image.scale.x, window.video.height * image.scale.y)
cc.restore()
})
Documentation: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Using_images

Creating a Gradient Path Fill JavaScript

I've been recently adding shadows to a project. I've ended up with something that I like, but the shadows are a solid transparent color throughout. I would prefer them to be a fading gradient as they go further.
What I currently have:
What I'd like to achieve:
Right now I'm using paths to draw my shadows on a 2D Canvas. The code that is currently in place is the following:
// Check if edge is invisible from the perspective of origin
var a = points[points.length - 1];
for (var i = 0; i < points.length; ++i, a = b)
{
var b = points[i];
var originToA = _vec2(origin, a);
var normalAtoB = _normal(a, b);
var normalDotOriginToA = _dot(normalAtoB, originToA);
// If the edge is invisible from the perspective of origin it casts
// a shadow.
if (normalDotOriginToA < 0)
{
// dot(a, b) == cos(phi) * |a| * |b|
// thus, dot(a, b) < 0 => cos(phi) < 0 => 90° < phi < 270°
var originToB = _vec2(origin, b);
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(a.x + scale * originToA.x,
a.y + scale * originToA.y);
ctx.lineTo(b.x + scale * originToB.x,
b.y + scale * originToB.y);
ctx.lineTo(b.x, b.y);
ctx.closePath();
ctx.globalAlpha = _shadowIntensity / 2;
ctx.fillStyle = 'black';
ctx.fillRect(_innerX, _innerY, _innerWidth, _innerHeight);
ctx.globalAlpha = _shadowIntensity;
ctx.fill();
ctx.globalAlpha = 1;
}
}
Suggestions on how I could go about achieving this? Any and all help is highly appreciated.
You can use composition + the new filter property on the context which takes CSS filters, in this case blur.
You will have to do it in several steps - normally this falls under the 3D domain, but we can "fake" it in 2D as well by rendering a shadow-map.
Here we render a circle shape along a line represented by length and angle, number of iterations, where each iteration increasing the blur radius. The strength of the shadow is defined by its color and opacity.
If the filter property is not available in the browser it can be replaced by a manual blur (there are many out there such as StackBoxBlur and my own rtblur), or simply use a radial gradient.
For multiple use and speed increase, "cache" or render to an off-screen canvas and when done composite back to the main canvas. This will require you to calculate the size based on max blur radius as well as initial radius, then render it centered at angle 0°. To draw use drawImage() with a local transform transformed based on start of shadow, then rotate and scale (not shown below as being a bit too broad).
In the example below it is assumed that the main object is drawn on top after the shadow has been rendered.
The main function takes the following arguments:
renderShadow(ctx, x, y, radius, angle, length, blur, iterations)
// ctx - context to use
// x/y - start of shadow
// radius - shadow radius (assuming circle shaped)
// angle - angle in radians. 0° = right
// length - core-length in pixels (radius/blur adds to real length)
// blur - blur radius in pixels. End blur is radius * iterations
// iterations - line "resolution"/quality, also affects total end blur
Play around with shape, shadow color, blur radius etc. to find the optimal result for your scene.
Demo
Result if browser supports filter:
var ctx = c.getContext("2d");
// render shadow
renderShadow(ctx, 30, 30, 30, Math.PI*0.25, 300, 2.5, 20);
// show main shape
ctx.beginPath();
ctx.moveTo(60, 30);
ctx.arc(30, 30, 30, 0, 6.28);
ctx.fillStyle = "rgb(0,140,200)";
ctx.fill();
function renderShadow(ctx, x, y, radius, angle, length, blur, iterations) {
var step = length / iterations, // calc number of steps
stepX = step * Math.cos(angle), // calc angle step for x based on steps
stepY = step * Math.sin(angle); // calc angle step for y based on steps
for(var i = iterations; i > 0; i--) { // run number of iterations
ctx.beginPath(); // create some shape, here circle
ctx.moveTo(x + radius + i * stepX, y + i * stepY); // move to x/y based on step*ite.
ctx.arc(x + i * stepX, y + i * stepY, radius, 0, 6.28);
ctx.filter = "blur(" + (blur * i) + "px)"; // set filter property
ctx.fillStyle = "rgba(0,0,0,0.5)"; // shadow color
ctx.fill();
}
ctx.filter = "none"; // reset filter
}
<canvas id=c width=450 height=350></canvas>

Display bottom canvas with a shadow effect - shadow within clipped area

I`m new with canvas so thanks for your patience.
I wrote an engine that is creating 2 different layers in 2 canvas elements which are one over another. They contain some generated pictures, which aren`t important here.
I'm trying to create an effect which will display bottom layer when I move mouse over the top layer and click.
Something like this:
This is what I have tried so far:
To use transparency on canvas element and display bottom canvas (fast but not usable)
Re-create a clipping region.
Whenever I press the mouse I store current coordinates and re-render the canvas with updated clipping region
Updating clipping region is slow if I use stroke to create shadows + I`m not sure how to remove lines from it (see picture).
If I remove shadow effect, it works really fast, but I need to have it.
The only thing that comes on my mind how to speed this, is to save coordinates of every click, and then to re-calculate that into 1 shape and drop a shadow on it - I`ll still have lines, but it will be faster because there won`t be thousand of circles to draw...
Any help will be most appreciated!
You can take advantage of the browser's built in interpolation by using it as a pseudo low-pass filter, but first by painting it black:
Copy the top layer to the bottom layer
Set source-in comp. mode
Draw all black
Set source-in comp. mode
Scale down image to 25%
Scale the 25% region back up to 50% of original (or double of current)
Scale the now 50% region back up to 100% of original. It will be blurred.
Depending on how much blur you want you can add additional steps. That being said: blurred shadow is an intensive operation no matter how it is twisted and turned. One can make compromise to only render the shadow on mouse up for example (as in the demo below).
Example
Example using two layers. Top layer let you draw anything, bottom will show shadow version at the bottom later while drawing.
var ctx = document.getElementById("top").getContext("2d"),
bctx = document.getElementById("bottom").getContext("2d"),
bg = new Image(),
isDown = false;
bg.src = "http://i.imgur.com/R2naCpK.png";
ctx.fillStyle = "#27f";
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.globalCompositeOperation = "destination-out"; // "eraser"
ctx.canvas.onmousedown = function(e) {isDown = true};
window.onmousemove = function(e) {
if (!isDown) return;
var pos = getPos(ctx.canvas, e);
ctx.beginPath();
ctx.moveTo(pos.x + 10, pos.y);
ctx.arc(pos.x, pos.y, 10, 0, 2*Math.PI); // erase while drawing
ctx.fill();
};
window.onmouseup = function(e) {
if (isDown) {
isDown = false;
makeShadow();
}
};
function makeShadow(){
var w = bctx.canvas.width,
h = bctx.canvas.height,
offset = 7,
alpha = 0.75;
// reset alpha
bctx.globalAlpha = 1;
// normal comp mode to clear as it is faster than using "copy"
bctx.globalCompositeOperation = "source-over";
bctx.clearRect(0, 0, w, h);
// copy top-layer to bottom-layer
bctx.drawImage(ctx.canvas, 0, 0);
// comp. mode will only draw in to non-alpha pixels next
bctx.globalCompositeOperation = "source-in";
// black overlay
bctx.fillRect(0, 0, w, h);
// copy mode so we don't need an extra canvas
bctx.globalCompositeOperation = "copy";
// step 1: reduce to 50% (quality related - create more steps to increase blur/quality)
bctx.drawImage(bctx.canvas, 0, 0, w, h, 0, 0, w * 0.5, h * 0.5);
bctx.drawImage(bctx.canvas, 0, 0, w * 0.5, h * 0.5, 0, 0, w * 0.25, h * 0.25);
bctx.drawImage(bctx.canvas, 0, 0, w * 0.25, h * 0.25, 0, 0, w * 0.5, h * 0.5);
// shadow transparency
bctx.globalAlpha = alpha;
// step 2: draw back up to 100%, draw offset
bctx.drawImage(bctx.canvas, 0, 0, w * 0.5, h * 0.5, offset, offset, w, h);
// comp in background image
bctx.globalCompositeOperation = "destination-over";
bctx.drawImage(bg, 0, 0, w, h);
}
function getPos(canvas, e) {
var r = canvas.getBoundingClientRect();
return {x: e.clientX - r.left, y: e.clientY - r.top};
}
div {position:relative;border:1px solid #000;width:500px;height:500px}
canvas {position:absolute;left:0;top:0}
#bottom {background:#eee}
<div>
<canvas id="bottom" width=500 height=500></canvas>
<canvas id="top" width=500 height=500></canvas>
</div>

Issues in canvas image rotation

I've created a basic HTML5 image slider where images move from top to bottom in a canvas.
I want all the images rotated at angle of 5 degrees. When I tried it out there seems to be some
distortion to the canvas and the image is not properly rotated.
I've tried the method for rotation mentioned in the below post
How do I rotate a single object on an html 5 canvas?
Fiddle - http://jsfiddle.net/DS2Sb/
Code
this.createImage = function (image, width, height) {
var fbWallImageCanvas = document.createElement('canvas');
var fbWallImageCanvasContext = fbWallImageCanvas.getContext('2d');
fbWallImageCanvas.width = width;
fbWallImageCanvas.height = height;
fbWallImageCanvasContext.save();
fbWallImageCanvasContext.globalAlpha = 0.7;
this.rotateImage(image, 0, 0, width, height, 5, fbWallImageCanvasContext);
fbWallImageCanvasContext.drawImage(image, width, height);
fbWallImageCanvasContext.restore();
return fbWallImageCanvas;
};
this.rotateImage = function (image, x, y, width, height, angle, context) {
var radian = angle * Math.PI / 180;
context.translate(x + width / 2, y + height / 2);
context.rotate(radian);
context.drawImage(image, width / 2 * (-1), height / 2 * (-1), width, height);
context.rotate(radian * (-1));
context.translate((x + width / 2) * (-1), (y + height / 2) * (-1));
};
The distortion you see is due to the fact that a rotated image will only fit in a larger canvas. So what we see is a rectangle view on a rotated image.
The computations are not that easy to get things done properly, but instead of pre-computing the rotated image, you might rotate them just when you draw them, which lets you also change the angle whenever you want (and opacity also btw).
So i simplified createImage, so that it just stores the image in a canvas (drawing a canvas is faster than drawing an image) :
this.createImage = function(image , width, height) {
var fbWallImageCanvas = document.createElement('canvas');
fbWallImageCanvas.width = width;
fbWallImageCanvas.height = height;
var fbWallImageCanvasContext = fbWallImageCanvas.getContext('2d');
fbWallImageCanvasContext.drawImage(image,0,0);
return fbWallImageCanvas;
};
And i changed drawItem so it draws the image rotated :
this.drawItem = function(ct) {
var angle = 5;
var radian = angle * Math.PI/180;
ct.save();
ct.translate(this.x + this.width/2 , this.y + this.height/2);
ct.rotate(radian);
ct.globalAlpha = 0.7;
ct.drawImage(fbc, - this.width/2, -this.height/2 , this.width, this.height);
ct.restore();
this.animate();
};
You'll probably want to refactor this, but you see the idea.
fiddle is here :
http://jsfiddle.net/DS2Sb/1/
Here is a link to a small html5 tutorial I created a while ago:
https://bitbucket.org/Garlov/html5-sidescroller-game-source
And here is the rotate code:
// save old coordinate system
ctx.save();
// move to the middle of where we want to draw our image
ctx.translate(canvas.width/2, canvas.height-64);
// rotate around that point
ctx.rotate(0.02 * (playerPosition.x));
//draw playerImage
ctx.drawImage(playerImage, -playerImage.width/2, -playerImage.height/2);
//and restore coordniate system to default
ctx.restore();

Categories

Resources