In OpenSCAD, I want to be able to create a module which accepts a string then create a 3-D object with that string embedded in the surface as a text. I want the object to be slightly larger than the text, so I need to know how wide the text is in order to create a properly-sized object.
I'm not sure how to query the width of the text (the height is set by an input variable), or if that's even possible.
If it's not possible, is there a function that will accept a string and a font and predict the width of the rendered text?
There is currently no way to query the actual size of the generated text geometry. However, depending on the model that shall be created, it might be enough to calculate a rough estimation and use scale() to fit the text into the known size.
// Fit text into a randomly generated area
r = rands(10, 20, 2);
length = 3 * r[0];
w = r[1];
difference() {
cube([length, w, 1]);
color("white")
translate([0, w / 2, 0.6])
linear_extrude(1, convexity = 4)
resize([length, 0], auto = true)
text("This is a Test", valign = "center");
}
If you use one of the Liberation fonts bundled with OpenSCAD or the fonts in the Microsoft Core Fonts pack, you can use my font measurement OpenSCAD library. E.g.:
use <fontmetrics.scad>;
length = measureText("This is a Test", font="Arial:style=Italic", size=20.);
The library is here. I used some Python scripts to extract metrics (including kerning pairs) from the ttf file, and you can use the scripts to add information about more fonts.
I have found a way to determine the widths of text characters in OpenSCAD. I made a JavaScript thing that lets you input a font name and style, and it outputs an array of width proportions for ascii and extended ascii characters (codes 0-255). Then for any given character, you multiply this proportion by the font size to get the width of an individual character. From there it's trivial to get the width of a string, or the angular widths of characters wrapped around a cylinder.
The tool to generate the OpenSCAD width array is here: https://codepen.io/amatulic/pen/eYeBLva
...and the code is reproduced below, which you can run from this reply, or paste into your own HTML file and load into your browser locally.
Just input the font properties, click the button, and scroll down to see usage instructions.
The secret sauce lies in the fact that JavaScript's 'canvas' support has a 'measureText()' method that measures the pixel length of any text for a given font, used like this:
canvasContext.measureText(string).width
So what this code does is use a dummy canvas on the page to get a context to which a font is assigned, arbitrarily 20 pixels in size. Then it generates an array of widths for every character from 0 to 255, dividing each by 20 to get a unitless width proportion compared to font size. It then outputs a line of OpenSCAD code that you can paste into your OpenSCAD script. Then you use OpenSCAD's ord() function to convert any character to a numeric code, which then serves as an index of the width array. You then multiply this width by the font size to get the character width.
<html>
<!--
by Alex Matulich, February 2022
Thingiverse: https://www.thingiverse.com/amatulic/designs
Website: https://www.nablu.com
-->
<head>
<script type="text/javascript">
var sctx;
function initialize() {
var canvas = document.getElementById("canvas");
sctx = canvas.getContext("2d");
}
function charwidth(fontname, style) {
sctx.font = (style + " 20px " + fontname).trim();
var charlen = [];
for (i = 0; i < 256; ++i) //{ charlen[i] = 10; console.log(i); }
charlen[i] = sctx.measureText(String.fromCharCode(i)).width / 20;
return charlen;
}
function generate() {
var fontname = document.getElementById("fontname").value;
var fontstyle = document.getElementById("fontstyle").value;
var widths = charwidth(fontname, fontstyle);
var arrayname = toCamelCase(fontname) + toCamelCase(fontstyle);
var outputhtml = arrayname + " = [<br/>\n" + widths[0].toString();
var len = widths.length;
for (i = 1; i < len; ++i) outputhtml += ', ' + widths[i].toString();
outputhtml += "<br/>\n];\n";
document.getElementById("output").innerHTML = outputhtml;
document.getElementById('usage').innerHTML = "<h3>Usage</h3>\n<p>The array above shows character width as a multiple of font size. To get the width of a character <code><char></code> given font size <code><fontsize></code> using the font \"" + fontname + " " + fontstyle + "\":</p>\n<p><code> charwidth = " + arrayname + "[ord(char)] * fontsize;<code></p>\n";
document.getElementById('sample').innerHTML = "<h3>Font sample</h3>\n<p style=\"font: " + fontstyle + " 20px " + fontname + ";\">" + fontname + " " + fontstyle + ": 0123456789 ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz</p>\n";
}
// convert the input array to camel case
function toCamelCase(stringinput) {
if (stringinput.length == 0) return '';
var inputArray = stringinput.match(/[A-Z\xC0-\xD6\xD8-\xDE]?[a-z\xDF-\xF6\xF8-\xFF]+|[A-Z\xC0-\xD6\xD8-\xDE]+(?![a-z\xDF-\xF6\xF8-\xFF])|\d+/g);
result = "";
for (let i = 0, len = inputArray.length; i < len; i++) {
let currentStr = inputArray[i];
let tempStr = currentStr.toLowerCase();
// convert first letter to upper case (the word is in lowercase)
tempStr = tempStr.substr(0, 1).toUpperCase() + tempStr.substr(1);
result += tempStr;
}
return result;
}
</script>
</head>
<body onload="initialize()">
<h1>OpenSCAD proportional font widths</h1>
<form>
<fieldset>
<legend>Identify the font</legend>
<input type="text" id="fontname" name="fontname" value="Liberation Sans">
<label for="fontname">Font name</label><br />
<input type="text" id="fontstyle" name="fontstyle" value="bold">
<label for="fontstyle">Font style (bold, italic, etc. or leave blank)<br />
</fieldset>
<input type="button" onclick="generate()" value="Generate OpenSCAD font width proportions">
</form>
<h2>Copy and paste this code into OpenSCAD</h2>
<div id="output" style="border:5px ridge silver; padding:1em; font-family:monospace;">
</div>
<div id="usage">
</div>
<div id="sample">
</div>
<canvas id="canvas"></canvas>
</body>
</html>
Related
When you render text on an HTML5 canvas (using the fillText command, for example), the text will render anti-aliased, meaning the text looks smoother. The downside is that it becomes very noticable when trying to render small text or specifically non-aliased fonts (such as Terminal). Because of this, what I want to do is render text aliased, rather than anti-aliased.
Is there any way to do so?
Unfortunately, there is no native way to turn off anti-aliasing for text.
The solution is to use the old-school approach of bitmap fonts, that is, in the case of HTML5 canvas a sprite-sheet where you copy each bitmap letter to the canvas. By using a sprite-sheet with transparent background you can easily change the color/gradient etc. of it as well.
An example of such bitmap:
For it to work you need to know what characters it contains ("map"), the width and height of each character, and the width of the font bitmap.
Note: In most cases you'll probably end up with a mono-spaced font where all cells have the same size. You can use a proportional font but in that case you need to be aware of that you need to map each character with an absolute position and include the width and height as well for its cell.
An example with comments:
const ctx = c.getContext("2d"), font = new Image;
font.onload = () => {
// define some meta-data
const charWidth = 12; // character cell, in pixels
const charHeight = 16;
const sheetWidth = (font.width / charWidth)|0; // width, in characters, of the image itself
// map so we can use index of a char. to calc. position in bitmap
const charMap = " !\"#$% '()*+,-./0123456789:;<=>?#ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~ยง";
// Draw some demo text
const timeStart = performance.now();
fillBitmapText(font, "Demo text using bitmap font!", 20, 20);
fillBitmapText(font, "This is line 2...", 20, 45);
const timeEnd = performance.now();
console.log("Text above rendered in", timeEnd - timeStart, "ms");
// main example function
function fillBitmapText(font, text, x, y) {
// always make sure x and y are integer positions
x = x|0;
y = y|0;
// current x position
let cx = x;
// now, iterate over text per char.
for(let char of text) {
// get index in map:
const i = charMap.indexOf(char);
if (i >= 0) { // valid char
// Use index to calculate position in bitmap:
const bx = (i % sheetWidth) * charWidth;
const by = ((i / sheetWidth)|0) * charHeight;
// draw in character on canvas
ctx.drawImage(font,
// position and size from font bitmap
bx, by, charWidth, charHeight,
// position on canvas, same size
cx, y, charWidth, charHeight);
}
cx += charWidth; // increment current canvas x position
}
}
}
font.src = "//i.stack.imgur.com/GeawH.png";
body {background:#fff}
<canvas id=c width=640></canvas>
This should produce an output similar to this:
You can modify this to suit your needs. Notice that the bitmap used here is not transparent - I'll leave that to OP.
I'm trying to use hexadecimal values for colors in p5.js and I'm having trouble using it and using an alpha at the same time. I'd like to set the color with one variable and the alpha with another.
let myColor = '#FF0000';
let myAlpha = 128;
function setup() {
createCanvas(200,200);
}
function draw() {
fill(color(myColor), myAlpha);
noStroke();
ellipse(100, 100, 50, 50);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.16/p5.js"></script>
<html>
<head></head>
<body></body>
</html>
This works for the color, but the alpha ends up being 255 (100%).
setAlpha() is documented on p5.Color and takes values from 0 to 255.
For your code:
let c = color(myColor);
c.setAlpha(myAlpha);
fill(c);
function setup(){
createCanvas(300,300);
}
var alphaValue = 50;
function draw(){
var c = color('#FF0000');
fill(c);
rect(100,100,100,100);
c = color('#0000FF');
c.setAlpha(alphaValue);
fill(c);
rect(150,150,100,100);
}
function mouseClicked(){
if (alphaValue > 50){
alphaValue = 50;
} else {
alphaValue = 255;
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.3/p5.min.js"></script>
From the reference:
If a single string argument is provided, RGB, RGBA and Hex CSS color strings and all named color strings are supported. In this case, an alpha number value as a second argument is not supported, the RGBA form should be used.
In other words, you can't use alpha values with string color codes.
You either have to use an RGBA string or split the values into their individual number values and use the 4-arg fill() function.
Side note: in the future, please post a MCVE that we can run. In the case of P5.js, this means including a setup() and a draw() function instead of a disconnected snippet of code.
Actually, I figured it out. p5 has a color object. The alpha is stored inside the color object at color._array[3]. You first create a color object using the string. Then you can change the alpha by changing the value in color._array[3] and then pass that color object to the fill() method.
Note: Although you typically define alpha as a number between 0-255 in p5, the color object stores the alpha as a number between 0 and 1 in this place.
let myColor = '#FF0000';
let myAlpha = 128;
function setup() {
createCanvas(200,200);
}
function draw() {
background(255);
let c = color(myColor);
c._array[3] = myAlpha / 255;
fill(c);
noStroke();
ellipse(100, 100, 50, 50);
ellipse(110, 110, 50, 50);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.16/p5.js"></script>
<html>
<head></head>
<body></body>
</html>
To incorporate the alpha value to the hexadecimal color representation, you simply add two more digits to the end of the color code. The range is 0-255, but in hexadecimal that will be 00-FF. For example: a solid red would be '#FF0000FF'. See more examples below.
function setup(){
createCanvas(300,300);
}
function draw(){
fill('#FF0000FF');
rect(50,50,100,100);
fill('#00FF0040');
rect(75,75,100,100);
fill('#0000FF80');
rect(100,25,100,100);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.8.0/p5.min.js"></script>
The project in question: https://github.com/matutter/Pixel2 is a personal project to replace some out of date software at work. What it should do is, the user adds an image and it generates a color palette of the image. The color palette should have no duplicate colors. (thats the only important stuff)
My question is: why do larger or hi-res or complex images not work as well? (loss of color data)
Using dropzone.js I have the user put a picture on the page. The picture is a thumbnail. Next I use jquery to find the src out of a <img src="...">. I pass that src to a function that does this
function generate(imgdata) {
var imageObj = new Image();
imageObj.src = imgdata;
convert(imageObj); //the function that traverses the image data pulling out RGB
}
the "convert" function pulls out the data fairly simply by
for(var i=0, n=data.length; i<n; i+=4, pixel++ ) {
r = data[i];
g = data[i+1];
b = data[i+2];
color = r + g + b; // format is a string of **r, g, b**
}
finally, the last part of the main algorithme filters out duplicate colors, I only want just 1 occurrence of each... here's the last part
color = monoFilter(color); // the call
function monoFilter(s) {
var unique = [];
$.each(s, function(i, el){
if($.inArray(el, unique) === -1) unique.push(el);
});
unique.splice(0,1); //remove undefine
unique.unshift("0, 0, 0"); //make sure i have black
unique.push("255, 255, 255"); //and white
return unique;
}
I'm hoping someone can help me identify why there is such a loss of color data in big files.
If anyone is actually interesting enough to look at the github, the relivent files are js/pixel2.js, js/dropzone.js, and ../index.html
This is probably the cause of the problem:
color = r + g + b; // format is a string of **r, g, b**
This simply adds the numbers together and the more pixels you have the higher risk you run to get the same number. For example, these colors generate the same result:
R G B
color = 90 + 0 + 0 = 90;
color = 0 + 90 + 0 = 90;
color = 0 + 0 + 90 = 90;
even though they are completely different colors.
To avoid this you can do it like this if you want a string:
color = [r,g,b].join();
or you can create an integer value of them (which is faster to compare with than a string):
color = (b << 16) + (g << 8) + r; /// LSB byte-order
Even an Euclidean vector would be better:
color = r*r + g*g + b*b;
but with the latter you risk eventually the same scenario as the initial one (but useful for nearest color scenarios).
Anyways, hope this helps.
"The problem was that I wasn't accounting for alpha. So a palette from an image that uses alpha would have accidental duplicate records."
I figured this out after finding this Convert RGBA color to RGB
I created a Tree in D3.js based on Mike Bostock's Node-link Tree. The problem I have and that I also see in Mike's Tree is that the text label overlap/underlap the circle nodes when there isn't enough space rather than extend the links to leave some space.
As a new user I'm not allowed to upload images, so here is a link to Mike's Tree where you can see the labels of the preceding nodes overlapping the following nodes.
I tried various things to fix the problem by detecting the pixel length of the text with:
d3.select('.nodeText').node().getComputedTextLength();
However this only works after I rendered the page when I need the length of the longest text item before I render.
Getting the longest text item before I render with:
nodes = tree.nodes(root).reverse();
var longest = nodes.reduce(function (a, b) {
return a.label.length > b.label.length ? a : b;
});
node = vis.selectAll('g.node').data(nodes, function(d, i){
return d.id || (d.id = ++i);
});
nodes.forEach(function(d) {
d.y = (longest.label.length + 200);
});
only returns the string length, while using
d.y = (d.depth * 200);
makes every link a static length and doesn't resize as beautiful when new nodes get opened or closed.
Is there a way to avoid this overlapping? If so, what would be the best way to do this and to keep the dynamic structure of the tree?
There are 3 possible solutions that I can come up with but aren't that straightforward:
Detecting label length and using an ellipsis where it overruns child nodes. (which would make the labels less readable)
scaling the layout dynamically by detecting the label length and telling the links to adjust accordingly. (which would be best but seems really difficult
scale the svg element and use a scroll bar when the labels start to run over. (not sure this is possible as I have been working on the assumption that the SVG needs to have a set height and width).
So the following approach can give different levels of the layout different "heights". You have to take care that with a radial layout you risk not having enough spread for small circles to fan your text without overlaps, but let's ignore that for now.
The key is to realize that the tree layout simply maps things to an arbitrary space of width and height and that the diagonal projection maps width (x) to angle and height (y) to radius. Moreover the radius is a simple function of the depth of the tree.
So here is a way to reassign the depths based on the text lengths:
First of all, I use the following (jQuery) to compute maximum text sizes for:
var computeMaxTextSize = function(data, fontSize, fontName){
var maxH = 0, maxW = 0;
var div = document.createElement('div');
document.body.appendChild(div);
$(div).css({
position: 'absolute',
left: -1000,
top: -1000,
display: 'none',
margin:0,
padding:0
});
$(div).css("font", fontSize + 'px '+fontName);
data.forEach(function(d) {
$(div).html(d);
maxH = Math.max(maxH, $(div).outerHeight());
maxW = Math.max(maxW, $(div).outerWidth());
});
$(div).remove();
return {maxH: maxH, maxW: maxW};
}
Now I will recursively build an array with an array of strings per level:
var allStrings = [[]];
var childStrings = function(level, n) {
var a = allStrings[level];
a.push(n.name);
if(n.children && n.children.length > 0) {
if(!allStrings[level+1]) {
allStrings[level+1] = [];
}
n.children.forEach(function(d) {
childStrings(level + 1, d);
});
}
};
childStrings(0, root);
And then compute the maximum text length per level.
var maxLevelSizes = [];
allStrings.forEach(function(d, i) {
maxLevelSizes.push(computeMaxTextSize(allStrings[i], '10', 'sans-serif'));
});
Then I compute the total text width for all the levels (adding spacing for the little circle icons and some padding to make it look nice). This will be the radius of the final layout. Note that I will use this same padding amount again later on.
var padding = 25; // Width of the blue circle plus some spacing
var totalRadius = d3.sum(maxLevelSizes, function(d) { return d.maxW + padding});
var diameter = totalRadius * 2; // was 960;
var tree = d3.layout.tree()
.size([360, totalRadius])
.separation(function(a, b) { return (a.parent == b.parent ? 1 : 2) / a.depth; });
Now we can call the layout as usual. There is one last piece: to figure out the radius for the different levels we will need a cumulative sum of the radii of the previous levels. Once we have that we simply assign the new radii to the computed nodes.
// Compute cummulative sums - these will be the ring radii
var newDepths = maxLevelSizes.reduce(function(prev, curr, index) {
prev.push(prev[index] + curr.maxW + padding);
return prev;
},[0]);
var nodes = tree.nodes(root);
// Assign new radius based on depth
nodes.forEach(function(d) {
d.y = newDepths[d.depth];
});
Eh voila! This is maybe not the cleanest solution, and perhaps does not address every concern, but it should get you started. Have fun!
I have a bunch of images, with different resolution.
Also there is a mix of landscape and portrait pictures. I need to resize the images to one resolution (1024x768). If i have a portrait picture, the max height needs to be 768, and my landscape pictures has to have a max width of 1024.
The space that is over, has to be made black.
Right now i use mogrify -resize 1024x768 -verbose *.jpg
I know i can use 1024x!768 , but like i said i'm using different kind of pictures.
My exif information also doesn't contains information about if a picture is landscape or not.
I use ImageMagick for such tasks. When installed, you have the "convert" command, which is very common, and does your task easyly.
You will have to crop the image to get the same aspect ratio, then you can resize the image to get the desired resolution. Example code using nodejs (imagemagick command line tools):
var width = 166;
var height = 117;
var ratio_new = width/height;
var ratio_old = image_file.width_orig/image_file.height_orig;
var pixels_too_much = 0;
var geometry = '';
if (ratio_old > ratio_new)
{
config.debug && console.log ("remove horizontal pixel!");
pixels_too_much = parseInt(image_file.width_orig - (image_file.height_orig * ratio_new))-1;
geometry = parseInt(image_file.height_orig * ratio_new + 0.5) + 'x' + image_file.height_orig;
geometry += "+" + parseInt(pixels_too_much/2) + "+0\!";
}
else if (ratio_old < ratio_new)
{
config.debug && console.log ("remove vertikal pixel");
pixels_too_much = parseInt(image_file.height_orig - (image_file.width_orig / ratio_new));
geometry = image_file.width_orig + 'x' + (image_file.width_orig / ratio_new);
geometry += "+0+" + parseInt(pixels_too_much/2)+"\!";
}
im.convert([image_file.path, '-crop', geometry, '-resize', width + 'x' + height, thumb_path],function(){});