How to vertically center align Multi-Line-Text on Canvas (in NodeJS) - javascript

I am trying to vertically center align multi-line text on a canvas, using canvas-multiline-text on top of the npm module canvas. I cannot figure out how to (programmatically / automatically) vertically center the text.
The text might be 1 word or it might be a sentence with 10+ words. Is there a way to figure out its height and calculate the y position accordingly?
Note: I am not in a browser, I am using node.js.
const fs = require('fs')
const { registerFont, createCanvas, loadImage } = require('canvas')
const drawMultilineText = require('canvas-multiline-text')
const width = 2000;
const height = 2000;
let fontSize = 250;
const canvas = createCanvas(width, height)
const context = canvas.getContext('2d')
//Filling the square
context.fillStyle = '#fff'
context.fillRect(0, 0, width, height)
//Text Styles
context.font = "normal 700 250px Arial";
context.textAlign = 'center'
context.textBaseline = 'middle';
context.fillStyle = '#000'
//Drawing Multi Line Text
const fontSizeUsed = drawMultilineText(
context,
"Hello World, this is a test",
{
rect: {
x: 1000,
y: 1000, // <-- not sure what to put here / how to calculate this?
width: context.canvas.width - 20,
height: context.canvas.height - 20
},
font: 'Arial',
verbose: true,
lineHeight: 1.4,
minFontSize: 100,
maxFontSize: 250
}
)

I put this as an answer as I can't comment yet, but the module you are using has no support for this. What you can do is modify the module (or just copy the function into your own code). The function has a variable y which stores the height of the text. You can initialize the variable outside of the for loop so that you can use it when drawing the text. Then you can subtract opts.rect.height from your new y variable and divide by two to get the correct offset, now the text will be centered in that y variable you specify when calling the function. Oh and finally get "context.textBaseline = 'middle';" as the module you use makes it calculations assuming a bottom baseLine. Here is the final code for the drawMultiLineText function
...
const words = require('words-array')(text)
if (opts.verbose) opts.logFunction('Text contains ' + words.length + ' words')
var lines = []
let y; //New Line
...
var x = opts.rect.x;
y = fontSize; //modified line
lines = []
...
if (opts.verbose) opts.logFunction("Font used: " + ctx.font);
const offset = opts.rect.y + (opts.rect.height - y) / 2; //New line, calculates offset
for (var line of lines)
// Fill or stroke
if (opts.stroke)
ctx.strokeText(line.text.trim(), line.x, line.y + offset) //modified line
else
ctx.fillText(line.text.trim(), line.x, line.y + offset) //modified line
...
This module you use isn't exactly the cleanest code (uses var instead of let, has a dependency just to split a string into words), so I would highly recommend you rewrite this function yourself. But those should be the required changes in order for it to center the text.
EDIT: After looking more closely at this dependency's code, it looks like there are more mistakes then I initially thought, the code treats height like the bottom of the rectangle, not like height. To fix this remove opts.rect.y from the line that assigns y, and add it to the offset variable (I already made these changes to the code)

Related

Matter.js How can I make a box that is correctly landing?

I created two rectangle as follows through Matter.js
I am trying to move the "a" box over the "b" box.
I think I should write applyForce at this time, how can I calculate the appropriate X, Y Force values to put in the factor?
The code below is a code that is made up of the rough approximation value. How can I calculate it with being adaptable?
var Engine = Matter.Engine,
Render = Matter.Render,
Bodies = Matter.Bodies,
Body = Matter.Body,
Composite = Matter.Composite,
Runner = Matter.Runner;
var canvas = document.createElement('canvas'),
context = canvas.getContext('2d');
var bodies = [];
canvas.width = 500;
canvas.height = 200;
var count = 0;
document.body.appendChild(canvas);
var engine = Engine.create();
var render = Render.create({
canvas: canvas,
engine: engine,
options: {
width: canvas.width,
height: canvas.height,
showAngleIndicator: true,
showCollisions: true,
showVelocity: true
}
})
var world = engine.world
Render.run(render)
var runner = Runner.create();
Runner.run(runner, engine)
Composite.add(world, Bodies.rectangle(250, 180, 500, 20, { isStatic: true }));
// random position
var b = Bodies.rectangle(200, 80, 50, 25, { isStatic: true });
Composite.add(world, b);
var a = Bodies.rectangle(50, 50, 30, 30);
Composite.add(world, a);
setTimeout(function() {
// how to calculate it???
Body.applyForce(a, a.position, {x: 0.012, y: -0.032});
}, 1000)
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.17.1/matter.min.js"></script>
I don't think there are many matter.js experts here:
https://stackoverflow.com/questions/tagged/matter.js?tab=Newest
looking at questions tagged with that, there are just a few and a lot of them have no answers...
And the owner of the project: https://github.com/liabru has not been very active lately, so your only option is to reverse engineer what is going on there, good thing the code is open source.
Now to your question
If I understood correctly, you hardcoded the force:
Body.applyForce(a, a.position, {x: 0.012, y: -0.032})
and you are looking for a way to calculate those values instead of just hardcodeing it.
I was playing with that code to see how it behaves, after increasing the size of box "A" and applying the same force, it does not make it over to box "B", so there is a hidden weight/mass component that is not clearly defined here, and evidently that is associated with the size of those boxes.
var Bodies = Matter.Bodies
var Body = Matter.Body
var Composite = Matter.Composite
var canvas = document.createElement('canvas')
canvas.width = canvas.height = 200;
var context = canvas.getContext('2d')
document.body.appendChild(canvas)
var engine = Matter.Engine.create()
var runner = Matter.Runner.create()
Matter.Render.run(Matter.Render.create({
canvas: canvas,
engine: engine,
options: {
width: canvas.width,
height: canvas.height,
showAngleIndicator: true,
showVelocity: true
}
}))
Matter.Runner.run(runner, engine)
Composite.add(engine.world, Bodies.rectangle(250, 180, 500, 20, { isStatic: true }))
Composite.add(engine.world, Bodies.rectangle(200, 80, 50, 25, { isStatic: true }))
var a = Bodies.rectangle(50, 130, 40, 40)
Composite.add(engine.world, a)
setTimeout(function() {
Body.applyForce(a, a.position, { x: 0.012, y: -0.032 })
}, 2000)
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.17.1/matter.min.js"></script>
Newton's Second Law:
The net force equals mass times acceleration
F = M * A
Acceleration and Velocity
Final velocity equals initial velocity plus acceleration times elapsed time
Vf = Vi + A * T
Matter.js is a 2D physics engine, to create a realistic experience it must have those in the code, you need to determine how/where they are setting those (mass, acceleration & velocity) and start breaking down this problem.
Expect a lot of reading, and if you don't remember your physics projectile motion class, you are up for a good refreshing.
Start answering simple questions, just try to calculate something like:
How is the mass calculated for the box ?
How much force on the x to move 100px ?
How much force on the y to jump 100px ?
Once you have those correctly, you can start combining jumps and moves
There is a bit of documentation on the techniques used by the engine here:
https://github.com/liabru/matter-js/wiki/References
Looking in the code looks like this is where the magic happens:
https://github.com/liabru/matter-js/blob/master/src/body/Body.js#L642-L644
// update velocity with Verlet integration
body.velocity.x = (velocityPrevX * frictionAir * correction) + (body.force.x / body.mass) * deltaTimeSquared;
body.velocity.y = (velocityPrevY * frictionAir * correction) + (body.force.y / body.mass) * deltaTimeSquared;
I was doing a bit of digging on the code here is what I got:
https://heldersepu.github.io/hs-scripts/HTML/matter-js.html
Here I'm calculating the height of the jump given the force,
the calculation goes like this:
var force = 1 // change this to the value you need
var frictionAir = 0.99
var gravityScale = 0.001
var deltaTimeSquared = Math.pow((1000 / 60), 2)
var gravity = a.mass * gravityScale
var netForce = force + gravity
var velocity = (netForce / a.mass) * deltaTimeSquared
var height = 0
do {
height += velocity
velocity = velocity * frictionAir + (gravity / a.mass) * deltaTimeSquared
} while (velocity < -0.05)
console.log(height) // this is the height of the jump
I got all that from reading and debugging matter-js
frictionAir
https://github.com/liabru/matter-js/blob/master/src/body/Body.js#L638
gravityScale
https://github.com/liabru/matter-js/blob/master/src/core/Engine.js#L266
deltaTimeSquared
https://github.com/liabru/matter-js/blob/master/src/body/Body.js#L635
https://github.com/liabru/matter-js/blob/master/src/core/Engine.js#L92
Full disclosure
I never worked with Matter.js before, this is my first experience with it...
This is a complex question, my objective here was not spend hours (possibly days) to give you the final formula/function to calculate what you need, but instead to show you how to can tackle this problem that way next time you come across something similar you know where to start.

How can I find out the size, a rect has to be, to fit all rects inside a canvas, using a fixed amout of rects?

Context
I'm creating a coloring pixels game clone using canvas
I save the state of a canvas inside an array that looks like this:
[{\"x\":0,\"y\":0,\"pickedColor\":\"white\",\"colorCode\":null},{\"x\":0,\"y\":1,\"pickedColor\":\"white\",\"colorCode\":null},{\"x\":0,\"y\":2,\"pickedColor\":\"white\",\"colorCode\":null},{\"x\":0,\"y\":3,\"pickedColor\":\"#8bc34a\",\"colorCode\":null},{\"x\":0,\"y\":4,\"pickedColor\":\"#8bc34a\",\"colorCode\":null},{\"x\":0,\"y\":5,\"pickedColor\":\"#8bc34a\",\"colorCode\":null},{\"x\":0,\"y\":6,\"pickedColor\":\"#8bc34a\",\"colorCode\":null},{\"x\":0,\"y\":7,\"pickedColor\":\"#8bc34a\",\"colorCode\":null},{\"x\":0,\"y\":8,\"pickedColor\":\"#8bc34a\",\"colorCode\":null},{\"x\":0,\"y\":9,\"pickedColor\":\"#8bc34a\",\"colorCode\":null},{\"x\":0,\"y\":10,\"pickedColor\":\"#8bc34a\",\"colorCode\":null},{\"x\":0,\"y\":11,\"pickedColor\":\"#8bc34a\",\"colorCode\":null},{\"x\":0,\"y\":12,\"pickedColor\":\"#8bc34a\",\"colorCode\":null},{\"x\":0,\"y\":13,\"pickedColor\":\"white\",\"colorCode\":null},{\"x\":0,\"y\":14,\"pickedColor\":\"white\",\"colorCode\":null},{\"x\":0,\"y\":15,\"pickedColor\":\"white\",\"colorCode\":null},{\"x\":0,\"y\":16,\"pickedColor\":\"white\",\"colorCode\":null},{\"x\":0,\"y\":17,\"pickedColor\":\"white\",\"colorCode\":null},{\"x\":0,\"y\":18,\"pickedColor\":\"white\",\"colorCode\":null},{\"x\":0,\"y\":19,\"pickedColor\":\"white\",\"colorCode\":null},{\"x\":1,\"y\":0,\"pickedColor\":\"white\",\"colorCode\":null},{\"x\":1,\"y\":1,\"pickedColor\":\"white\",\"colorCode\":null},{\"x\":1,\"y\":2,\"pickedColor\":\"white\",\"colorCode\":null},{\"x\":1,\"y\":3,\"pickedColor\":\"white\",\"colorCode\":null},{\"x\":1,\"y\":4,\"pickedColor\":\"white\",\"colorCode\":null},{\"x\":1,\"y\":5,\"pickedColor\":\"white\",\"colorCode\":null},{\"x\":1,\"y\":6,\"pickedColor\":\"white\",\"colorCode\":null},{\"x\":1,\"y\":7,\"pickedColor\":\"white\",\"colorCode\":null},{\"x\":1,\"y\":8,\"pickedColor\":\"white\",\"colorCode\":null},{\"x\":1,\"y\":9,\"pickedColor\":\"white\",\"colorCode\":null},{\"x\":1,\"y\":10,\"pickedColor\":\"white\",\"colorCode\":null},{\"x\":1,\"y\":11,\"pickedColor\":\"white\",\"colorCode\":null},{\"x\":1,\"y\":12,\"pickedColor\":\"#8bc34a\",\"colorCode\":null},{\"x\":1,\"y\":13,\"pickedColor\":\"#8bc34a\",\"colorCode\":null},{\"x\":1,\"y\":14,\"pickedColor\":\"white\",\"colorCode\":null},{\"x\":1,\"y\":15,\"pickedColor\":\"white\",\"colorCode\":null},{\"x\":1,\"y\":16,\"pickedColor\":\"white\",\"colorCode\":null},{\"x\":1,\"y\":17,\"pickedColor\":\"white\",\"colorCode\":null},{\"x\":1,\"y\":18,\"pickedColor\":\"white\",\"colorCode\":null},{\"x\":1,\"y\":19,\"pickedColor\":\"white\",\"colorCode\":null},{\"x\":2,\"y\":0,\"pickedColor\":\"white\",\"colorCode\":null},{\"x\":2,\"y\":1,\"pickedColor\":\"white\",\"colorCode\":null},{\"x\":2,\"y\":2,\"pickedColor\":\"white\",\"colorCode\":null},{\"x\":2,\"y\":3,\"pickedColor\":\"white\",\"colorCode\":null}]
So each rect has the x and y coordinates on it.
To draw the rect on the screen I use this function to calculate how big each "rect" has to be to fit inside the canvas bounds:
// width / height comes from props and rectSize comes from props
const [rectCountX, setRectCountX] = useState(Math.floor(width / rectSize));
const [rectCountY, setRectCountY] = useState(Math.floor(height / rectSize));
For example width and height might be 800 and 600 and the rectSize might be 30.
That calculates how many rects I can draw in each direction.
Here is how I draw the initial board:
const generateDrawingBoard = (ctx) => {
// Generate an Array of pixels that have all the things we need to redraw
for (var i = 0; i < rectCountX; i++) {
for (var j = 0; j < rectCountY; j++) {
// this is the quint essence whats saved in a huge array. 1000's of these pixels.
// With the help of this, we can redraw the whole canvas although canvas has not state or save functionality :)
const pixel = {
x: i,
y: j,
pickedColor: "white",
// we don't know the color code yet, we generate that afterwards
colorCode: null,
};
updateBoardData(pixel);
ctx.fillStyle = "white";
ctx.strokeRect(i * rectSize, j * rectSize, rectSize, rectSize);
}
}
};
That works perfectly. The user draws the canvas and saves it into the database.
The Problem
I have a pixelArtPreview components. This gets the data from the database and for each pixelArt it will draw a rect but in a smaller size, so I can fit many rects on the page to present the user like a list of pixel Arts.
Therefore I need to recalculate the rectSize of each rect in the array to fit in the new width and height. Thats exactly where I'm banging my head at currently.
So here is the component I was mentioning:
import { useEffect, useRef, useState } from "react";
import { drawPixelArtFromState } from "../utils/drawPixelArtFromState";
const PixelArtPreview = ({ pixelArt }) => {
const canvasRef = useRef(null);
const [ctx, setCtx] = useState(null);
const [canvas, setCanvas] = useState(null);
useEffect(() => {
const canvas = canvasRef.current;
// This is where I scale the original size
// the whole pixelArt comes from the database and looks like this (example data):
// { pixelArtTitle: "some title", pixelArtWidth: 1234, pixelArtHeight: 1234, pixels: [... (the array I shows above with pixels)]}
canvas.width = pixelArt.pixelArtWidth * 0.5;
canvas.height = pixelArt.pixelArtHeight * 0.5;
setCanvas(canvas);
const context = canvas.getContext("2d");
setCtx(context);
}, []);
useEffect(() => {
if (!ctx) return;
drawPixelArtFromState(pixelArt, ctx);
}, [pixelArt, ctx]);
return <canvas className="m-4 border-4" ref={canvasRef} />;
};
export default PixelArtPreview;
But the magic happens inside the imported function drawPixelFromState(pixelArt, ctx)
This is said function (with comments what my thaught process was):
export const drawPixelArtFromState = (pixelArt, ctx) => {
// how much pixels have been saved from the original scale when the art has been created
const canvasCount= JSON.parse(pixelArt.pixels).length;
// how many pixels we have on X
const xCount = JSON.parse(pixelArt.pixels)[canvasCount- 1].x;
// how many pixels we have on Y
const yCount = JSON.parse(pixelArt.pixels)[canvasCount- 1].y;
// total pixles (canvas height * canvas.width with the scale of 0.5 so it matches the canvas from the component before)
// this should give me all the pixels inside the canvas
const canvasPixelsCount =
pixelArt.pixelArtWidth * 0.5 * (pixelArt.pixelArtHeight * 0.5);
// now i try to find out how big each pixel has to be
const newRectSize = canvasPixelsCount / canvasCount;
// this is for example 230 rects which can't be I see only 2 rects on the canvas with that much of a rectSize
console.log(newRectSize);
// TODO: Parse it instantly where we fetch it
JSON.parse(pixelArt.pixels).forEach((pixel) => {
ctx.fillStyle = "white";
ctx.strokeRect(
pixel.x * newRectSize,
pixel.y * newRectSize,
newRectSize,
newRectSize
);
ctx.fillStyle = pixel.pickedColor;
ctx.fillRect(
pixel.x * newRectSize,
pixel.y * newRectSize,
newRectSize,
newRectSize
);
});
};
Here is how that example looks like on screen (these are 4 separate canvas and can be seen on the grey border - I expect to see the whole pixel art inside the little canvas):
The Question:
I need to figure out the correct formula to calculate the new rectSize so all rects in the array can fit inside the new canvas width and height.
Is this even possible or do I need the old rectSize for the calculation to work?
So TL;DR: how big has every rect x to be, to fit all x rects in y canvas.
Thank you very much!
Sorry guys after struggling for hours it finally clicked.
The calculation I made:
// now i try to find out how big each pixel has to be
const newRectSize = canvasPixelsCount / rectCount;
Gives me the area of the pixel. But I just need one side of it (since canvas.fillRect only cares for the x value and thakes care of the area). So I need the square root of it.
// now i try to find out how big each pixel has to be
const newRectSize = Math.sqrt(canvasPixelsCount / rectCount);
This now works perfectly.
Screenshot:

How can I get the visible height of a font? [duplicate]

I know how to get this height of a font:
By placing the text in a div and getting offset height of the div.
But I would like to get this actual height (Which will depend on font family):
Is that in any way possible using web based programming?
Is there a simple solution? I think the answer is no.
If you're ok with a more involved (and processor-intensive) solution, you could try this:
Render the text to a canvas, then use canvasCtx.getImageData(..) to retrieve pixel information. Next you would do something similar to what this pseudo code describes:
first_y : null
last_y : null
for each y:
for each x:
if imageData[x][y] is black:
if first_y is null:
first_y = y
last_y = y
height = last_y - first_y
This basically looks for the top (lowest y-index) of the lettering (black pixels) and the bottom (highest y-index) then subtracts to retrieve the height.
I was writing the code while Jason answered, but I decided to post it anyway:
http://jsfiddle.net/adtn8/2/
If you follow the comments you should get the idea what's going on and why. It works pretty fast and it's not so complicated as it may sound. Checked with GIMP and it is accurate.
(code to be sure it wont be lost):
// setup variables
var c = document.createElement('canvas'),
div = document.getElementsByTagName('div')[0],
out = document.getElementsByTagName('output')[0];
// set canvas's size to be equal with div
c.width = div.offsetWidth;
c.height = div.offsetHeight;
var ctx = c.getContext('2d');
// get div's font from computed style and apply it to context
ctx.font = window.getComputedStyle(div).font;
// use color other than black because all pixels are 0 when black and transparent
ctx.fillStyle = '#bbb';
// draw the text near the bottom of the canvas
ctx.fillText(div.innerText, 0, div.offsetHeight);
// loop trough the canvas' data to find first colored pixel
var data = ctx.getImageData(0, 0, c.width, c.height).data,
minY = 0, len = data.length;
for (var i = 0; i < len; i += 4) {
// when you found it
if (data[i] != 0) {
// get pixel's y position
minY = Math.floor(i / 4 / c.width);
break;
}
}
// and print out the results
out.innerText = c.height - minY + 'px';
EDIT:
I even made jQuery plugin for this: https://github.com/maciek134/jquery-textHeight
Enjoy.

Turn list into polygon that scales

I have used latex and in particular tikz quite a bit. Using this I was able to create the image shown below.
The following short code was used to create the image.
\documentclass[tikz]{standalone}
\begin{document}
\usetikzlibrary{shapes.geometric}
\usetikzlibrary{backgrounds}
\begin{tikzpicture}[background rectangle/.style={fill=black},
show background rectangle]
\def\pages{
Home,
Events,
Pictures,
Video,
Contact,
About,
Map
}
\def\ngon{7}
\node[regular polygon,regular polygon sides=\ngon,minimum size=3cm] (p) {};
\foreach\page [count=\x] in \pages{\node[color=white, shift={(\x*360/7+35:0.4)}] (p\x) at (p.corner \x){\page};}
\foreach\i in {1,...,\numexpr\ngon-1\relax}{
\foreach\j in {\i,...,\x}{
\draw[thin, orange, dashed] (p\i) -- (p\j);
}
}
\end{tikzpicture}
\end{document}
I have tried for the last few hours to recreate the same image using 'HTMLæ, 'CSS' and 'Javascript'. I used the 'canvas' element to draw the lines, however I ran into a series of problems as can be seen in the image below
Which was made with the following code. I tried to the best of my abilities to minimize the code. The code can be found at the bottom of the post. The code has the following problems
Scalability. The text in the image is not the same as in the 'body' of the page.
The image hides the rest of the text in the body
To place the text outside the figure is hardcoded
The last minor problem is that the first element in the list is not drawn
I would like to address the problems above, but I am unsure how to proceed. Again I am not married to the idea of using canvas (can a better result be done using nodes and elements instead). However, the output should mimic the first image as closely as possible.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Canvas octagon</title>
<style>
* {
margin: 0;
padding: 0;
color:white;
background:black;
}
canvas {
display: block;
}
html,
body {
width: 100%;
height: 100%;
margin: 0px;
border: 0;
overflow: hidden;
/* Disable scrollbars */
display: block;
/* No floating content on sides */
}
</style>
</head>
<body>
<canvas id="polygon"></canvas>
<h2>more space</h2>
<ol id="poly">
<li>About</li>
<li>Home</li>
<li>Pictures</li>
<li>Video</li>
<li>Events</li>
<li>Map</li>
<li>Apply?</li>
<li>Recepies</li>
</ol>
some more text here
<script>
(function() {
var canvas = document.getElementById('polygon'),
context = canvas.getContext('2d');
// resize the canvas to fill browser window dynamically
window.addEventListener('resize', resizeCanvas, false);
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
/**
* Your drawings need to be inside this function otherwise they will be reset when
* you resize the browser window and the canvas goes will be cleared.
*/
drawStuff();
}
resizeCanvas();
function drawStuff() {
// do your drawing stuff here
context.beginPath();
context.translate(120, 120);
context.textAlign = "center";
var edges = document.getElementById("poly").getElementsByTagName("li");
var sides = edges.length
var angle = (Math.PI * 2) / sides;
var radius = 50;
context.save();
for (var i = 0, item; item = edges[i]; i++) {
console.log("Looping: index ", i, "item " + item.innerText);
var start_x = radius * Math.cos(angle * i);
var start_y = radius * Math.sin(angle * i);
context.lineTo(start_x, start_y);
var new_x_text = 1.4 * radius * Math.cos(angle * i);
var new_y_text = 1.4 * radius * Math.sin(angle * i);
context.fillText(item.innerText, new_x_text, new_y_text);
context.strokeStyle = 'orange';
for (var j = 0; j < i; j++) {
var new_x = radius * Math.cos(angle * j);
var new_y = radius * Math.sin(angle * j);
context.moveTo(start_x, start_y);
context.lineTo(new_x, new_y);
console.log(new_x, new_y);
}
context.fillStyle = 'white'
}
var new_x = radius * Math.cos(0);
var new_y = radius * Math.sin(0);
context.lineTo(new_x, new_y);
context.stroke();
}
})();
</script>
</body>
</html>
Using the canvas to render content
First I will say that using javascript will be longer than if you use some symbolic representation language like Latex. It is designed to do graphical representations with the minimum of fuss. The actual code base that makes it work is substantial but hidden for the general user.
Using the DOM
As the content for the canvas is stored in the DOM it also a good idea to store as much information as you can in the DOM, the colors, fonts, etc can all be stored in an element`s dataset.
For this I have put the settings in the ordered list. It contains all the settings, but there is also a default set of settings in the rendering function. The elements dataset will overwrite the defaults, or you can not add any dataset properties and let it all use the defaults.
Vetting settings
In the example below I have only put a minimum of vetting. People tend to put quotes around everything in the DOM as numbers can sometimes not work if represented as a string, I force all the numbers to the correct type. Though to be safe I should have checked to see if indeed they are valid numbers, the same for the other settings. I have just assumed that they will be correctly formatted.
The function
All the work is done in a function, you pass it the query string needed to find the list and canvas. It then uses the list items to render to the canvas.
Relative sizes
As the canvas size is not always known (it could be scaled via CSS) you need to have some way to specify size independent of pixels. For this I use a relative size. Thus the font size is as a fraction of the canvas size eg data-font-size = 16 means that the font will be 1/16th of the canvas height. The same for the line width, and the dash size is a multiple of the line width. eg data-line-dash = 4 means that the dashes are 4 times the length of the line width.
Element's data properties
To use data set you add the property to the element in the HTML prefixed with the word data- then the property name/s separated by "-". In javascript you can not use "-" directly as part of a variable name (it's a subtract operator) so the property names are converted to camelcase (the same as CSS properties), and stored in the element's dataset property.
<!-- HTML -->
<div id="divElement" data-my-Value = "some data"></div>
<script>
// the property of divElement is available as
console.log(divElement.dataset.myValue); // output >> "some data"
</script>
Scaling & rendering
The canvas is rendered at a ideal size (512 in this case) but the transform is set to ensure that the render fits the canvas. In this example I scale the x and y axis) the result is that the image does not have a fixed aspect.
Background
The canvas is transparent by default, but I do clear it in case you rerender to it. Anything under the canvas should be visible.
I first render the lines, then the text, clearing a space under the text to remove the lines. ctx.clearRect ensure the a canvas rect is transparent.
Drawing lines
To draw the lines you have two loops, From each item you draw a line to every other item. You don't want to draw a line more than once, so the inner loop starts at the current outer loops position + 1. This ensures a line is only rendered one.
Example
The example shows what I think you are after. I have add plenty of comments, but if you have questions do ask in the comments below.
I assumed you wanted the ordered list visible. If not use a CSS rule to hide it, it will not affect the canvas rendering.
Also if you size the canvas via CSS you may get a mismatch between canvas resolution and display size. This can result in blurred pixels, and also some high res displays will set canvas pixels to large. If this is a problem there are plenty of answers on SO on how to deal with blurred canvas rendering and hi res displays (like retina).
function drawConnected(listQ, canvasQ) {
const list = document.querySelector(listQ);
if(list === null){
console.warn("Could not find list '" + listQ +"'");
return;
}
const canvas = document.querySelector(canvasQ);
if(canvas === null){
console.warn("Could not find canvas '" + canvasQ + "'");
return;
}
const ctx = canvas.getContext("2d");
const size = 512; // Generic size. This is scaled to fit the canvas
const xScale = canvas.width / size;
const yScale = canvas.height / size;
// get settings or use dsefault
const settings = Object.assign({
fontSize : 16,
lineWidth : 128,
lineDash : 4,
textColor : "White",
lineColor : "#F90", // orange
startAngle : -Math.PI / 2,
font : "arial",
}, list.dataset);
// calculate relative sizes. convert deg to randians
const fontSize = size / Number(settings.fontSize) | 0; // (| 0 floors the value)
const lineWidth = size / Number(settings.lineWidth) | 0;
const lineDash = lineWidth * Number(settings.lineDash);
const startAngle = Number(settings.startAngle) * Math.PI / 180; // -90 deg is top of screen
// get text in all the list items
const items = [...list.querySelectorAll("li")].map(element => element.textContent);
// Set up the canvas
// Scale the canvas content to fit.
ctx.setTransform(xScale,0,0,yScale,0,0);
ctx.clearRect(0,0,size,size); // clear as canvas may have content
ctx.font = fontSize + "px " + settings.font;
// align text to render from its center
ctx.textAlign = "center";
ctx.textBaseline = "middle";
// set the line details
ctx.lineWidth = lineWidth;
ctx.lineCap = "round";
ctx.setLineDash([lineDash, lineDash]);
// need to make room for text so calculate all the text widths
const widths = [];
for(let i = 0; i < items.length; i ++){
widths[i] = ctx.measureText(items[i]).width;
}
// use the max width to find a radius that will fit all text
const maxWidth = Math.max(...widths);
const radius = (size/2 - maxWidth * 0.6);
// this function returns the x y position on the circle for item at pos
const getPos = (pos) => {
const ang = pos / items.length * Math.PI * 2 + startAngle;
return [
Math.cos(ang) * radius + size / 2,
Math.sin(ang) * radius + size / 2
];
};
// draw lines first
ctx.strokeStyle = settings.lineColor;
ctx.beginPath();
for(let i = 0; i < items.length; i ++){
const [x,y] = getPos(i);
for(let j = i+1; j < items.length; j ++){
const [x1,y1] = getPos(j);
ctx.moveTo(x,y);
ctx.lineTo(x1,y1);
}
}
ctx.stroke();
// draw text
ctx.fillStyle = settings.textColor;
for(let i = 0; i < items.length; i ++){
const [x,y] = getPos(i);
ctx.clearRect(x - widths[i] * 0.6, y - fontSize * 0.6, widths[i] * 1.2, fontSize * 1.2);
ctx.fillText(items[i],x,y);
}
// restore default transform;
ctx.setTransform(1,0,0,1,0,0);
}
// draw the diagram with selector query for ordered list and canvas
drawConnected("#poly","#polygon");
* {
margin: 0;
padding: 0;
color:white;
background:black;
}
canvas {
display: block;
}
html,
body {
font-family : arial;
width: 100%;
height: 100%;
margin: 0px;
border: 0;
display: block;
}
<canvas id="polygon" width = "256" height = "256"></canvas>
<h2>more space</h2>
<ol id="poly"
data-font-size = 16
data-line-width = 128
data-line-dash = 2
data-text-color = "white"
data-line-color = "#F80"
data-start-angle = "-90"
data-font = "arial"
>
<li>About</li>
<li>Home</li>
<li>Pictures</li>
<li>Video</li>
<li>Events</li>
<li>Map</li>
<li>Apply?</li>
<li>Recepies</li>
</ol>

html5 canvas - How can I stretch text?

In my html 5 canvas, I draw text (that has to be on 1 line) but it needs to be a constant font size and style and the width of the space I have is also constant. Therefore the text needs to fit in that space, but the problem occurs when the text is too long and goes past the space.
So is there a way I can horizontally stretch/compress text? (like in PhotoShop)
Something like convert it to an image then change the width of the image? Not sure if this is the best way...
Thanks
You can use measureText to determine the size of the text first, then scale the canvas if needed: http://jsfiddle.net/eGjak/887/.
var text = "foo bar foo bar";
ctx.font = "30pt Arial";
var width = ctx.measureText(text).width;
if(width <= 100) {
ctx.fillText(text, 0, 100);
} else {
ctx.save();
ctx.scale(100 / width, 1);
ctx.fillText(text, 0, 100);
ctx.restore();
}
A simple solution is to draw your text in another (in memory) canvas and then use drawImage to paste the canvas content in the real destination canvas.
Something like this (let it be parameterizable depending on your needs, here stretching with a ratio of 100 to 80) :
var tempimg = document.createElement('canvas');
tempimg.width = 100;
tempimg.height = 10;
oc = tempimg.getContext('2d');
oc.fillText(...)
yourdestcontext.drawImage(tempimg, 0, 0, 100, 10, x, y, 80, 10);

Categories