I'm currently working on a project that's using Google Sheets to keep track of/maintain some translations of text (effectively some meta-data and then 2 columns for the original text and the translation).
The final application has some word-wrapping constraints and a variable-width font, so I created a custom function in app script to apply line breaks in order to facilitate visualizing how it would look.
function preview(text)
{
if(text.startsWith('=')) // Copy of something else, so we don't care
return text;
text = text.trimEnd();
text = text.replace("<4A>", "\n\n"); // For the sake of the preview, just treat <4A> as a new box
text = text.replace(/\<&[a-zA-Z0-9]+\>/g,"────────"); // Assume the longest length (8 * 8)
text = text.replace(/<\*.>/g,""); // Get rid of exit codes
text = text.replace(/<S.+?>/g,""); // Get rid of special effects
var output = "";
var boxes = text.split("\n\n"); // Double newline is a forced new text box
for (var i = 0; i < boxes.length; i++)
{ // For each intentional text box
var box = boxes[i];
box = box.replace("<4E>","\n"); // Will technically forcibly always draw on the second line
var lines = box.split('\n');
var newboxFlag = false; // Flag to indicate if we draw a new line or new box
for (var j = 0; j < lines.length; j++)
{ // For each intentional line in this box
words = lines[j].split(' ');
var word = "";
var currentLineLen = 0;
for(var k = 0; k < words.length; k++)
{
word += words[k];
var wordWidth = 0;
for (var l = 0; l < word.length; l++)
{
var char = word.charAt(l);
wordWidth += getCharacterWidth(char);
}
if(wordWidth + currentLineLen > 0x89)
{ // This word won't fit on this line, so it goes to a new line
// Strip the first space, and just assume we don't have a string longer than 136 characters
word = word.substr(1);
wordWidth -= getCharacterWidth(' ');
// Add a new line and flip the newboxFlag
output += '\n';
if(newboxFlag) output += '\n';
newboxFlag ^= 1;
currentLineLen = 0;
}
currentLineLen += wordWidth
output += word;
word = " ";
}
if (j != lines.length - 1)
{
output += '\n'; // line length is reset at the top
if(newboxFlag) output += '\n';
newboxFlag ^= 1;
}
}
if(i != boxes.length - 1) output += '\n\n'; // If we're not on the last box, add the double new line
}
return output;
}
and I call it with =preview(<CELL>) (getCharacterWidth just pulls an integer from a character <-> width mapping).
This function works fine with raw text that does not contain any bold or italic characters.
The problem is, bold and italic characters often take more space than their normal counterparts, and in some cases will cause the previewed output text to incorrectly omit newlines where there should be some.
For example, the original text would be:
This is italic. This is bold. This is bold and italic.
=preview(text) should ideally be something like:
This is italic.
This is bold.
This is bold
and italic.
but instead, it might end up being like this:
This is italic.
This is bold. This
is bold and italic.
Since the additional width of the bold/italic characters is not being accounted for, the function believes it has enough space to fit another word on the line.
It's OK if I can't output it, but I at least need to be able to identify what text is bold or italic. In Google Sheets, how can I write a custom function that is aware of RichText?
Thanks to TheMaster's comments, I was able to create a solution by changing my function to the following:
function preview(startcol, startrow)
{
var str = String.fromCharCode(64 + startcol) + startrow;
var richtext = SpreadsheetApp.getActiveSpreadsheet().getRange(str).getRichTextValue();
var runs = richtext.getRuns();
...
and setting my call to
=preview(COLUMN(<CELL>), ROW(<CELL>))
By looping over the runs accounting for getTextStyle().IsBold() or .IsItalic(), I can account for the RichText attributes.
if(runs[i].getTextStyle().IsBold() && runs[i].getTextStyle().IsItalic())
{
wordWidth += getCharacterWidthBoldItalic(char);
}
else if(runs[i].getTextStyle().IsBold())
{
wordWidth += getCharacterWidthBold(char);
}
else if(runs[i].getTextStyle().IsItalic())
{
wordWidth += getCharacterWidthItalic(char);
}
else
{
wordWidth += getCharacterWidth(char);
}
I would like to have a table with some of its cells covered by a grey intransparent block (which only covers the cell without extending itself beyong the cell). The contents in the cells should also be covered.
This is the relevant JSFiddle.
However, I can not make it such that the content is covered. I have been trying around with "position" of the div element responsible for the blocks in CSS, but the effect is either not covering the cell content or the block extends itself beyond the cell.
The javascript code:
var x = document.createElement("table");
x.setAttribute("id", "table");
document.body.appendChild(x);
var i;
for (i = 0; i < 2; i++) {
var y = document.createElement("tr");
y.setAttribute("id", 'tr' + i);
document.getElementById("table").appendChild(y);
var j;
var temp;
for (j = 0; j < 12; j++) {
var z = document.createElement("th");
z.setAttribute("id", 'th' + i +j);
var t;
var temp;
if(i == 0){
t = document.createTextNode(-(12-1-j));
}else{
t = document.createTextNode('abc');
temp = document.createElement('div');
temp.setAttribute('class', 'first');
console.log(temp)
temp.setAttribute('id', 'shadow'+ j);
z.appendChild(temp);
}
z.appendChild(t);
document.getElementById('tr' + i).appendChild(z);
}
}
You can add a separate class to text and then use position:relative to position your component with respect to grey area.
For working example please refer this fiddle
I am trying to add text on an image using the <canvas> element. First the image is drawn and on the image the text is drawn. So far so good.
But where I am facing a problem is that if the text is too long, it gets cut off in the start and end by the canvas. I don't plan to resize the canvas, but I was wondering how to wrap the long text into multiple lines so that all of it gets displayed. Can anyone point me at the right direction?
Updated version of #mizar's answer, with one severe and one minor bug fixed.
function getLines(ctx, text, maxWidth) {
var words = text.split(" ");
var lines = [];
var currentLine = words[0];
for (var i = 1; i < words.length; i++) {
var word = words[i];
var width = ctx.measureText(currentLine + " " + word).width;
if (width < maxWidth) {
currentLine += " " + word;
} else {
lines.push(currentLine);
currentLine = word;
}
}
lines.push(currentLine);
return lines;
}
We've been using this code for some time, but today we were trying to figure out why some text wasn't drawing, and we found a bug!
It turns out that if you give a single word (without any spaces) to the getLines() function, it will return an empty array, rather than an array with a single line.
While we were investigating that, we found another (much more subtle) bug, where lines can end up slightly longer than they should be, since the original code didn't account for spaces when measuring the length of a line.
Our updated version, which works for everything we've thrown at it, is above. Let me know if you find any bugs!
A possible method (not completely tested, but as for now it worked perfectly)
/**
* Divide an entire phrase in an array of phrases, all with the max pixel length given.
* The words are initially separated by the space char.
* #param phrase
* #param length
* #return
*/
function getLines(ctx,phrase,maxPxLength,textStyle) {
var wa=phrase.split(" "),
phraseArray=[],
lastPhrase=wa[0],
measure=0,
splitChar=" ";
if (wa.length <= 1) {
return wa
}
ctx.font = textStyle;
for (var i=1;i<wa.length;i++) {
var w=wa[i];
measure=ctx.measureText(lastPhrase+splitChar+w).width;
if (measure<maxPxLength) {
lastPhrase+=(splitChar+w);
} else {
phraseArray.push(lastPhrase);
lastPhrase=w;
}
if (i===wa.length-1) {
phraseArray.push(lastPhrase);
break;
}
}
return phraseArray;
}
Here was my spin on it... I read #mizar's answer and made some alterations to it... and with a little assistance I Was able to get this.
code removed, see fiddle.
Here is example usage. http://jsfiddle.net/9PvMU/1/ - this script can also be seen here and ended up being what I used in the end... this function assumes ctx is available in the parent scope... if not you can always pass it in.
edit
the post was old and had my version of the function that I was still tinkering with. This version seems to have met my needs thus far and I hope it can help anyone else.
edit
It was brought to my attention there was a small bug in this code. It took me some time to get around to fixing it but here it is updated. I have tested it myself and it seems to work as expected now.
function fragmentText(text, maxWidth) {
var words = text.split(' '),
lines = [],
line = "";
if (ctx.measureText(text).width < maxWidth) {
return [text];
}
while (words.length > 0) {
var split = false;
while (ctx.measureText(words[0]).width >= maxWidth) {
var tmp = words[0];
words[0] = tmp.slice(0, -1);
if (!split) {
split = true;
words.splice(1, 0, tmp.slice(-1));
} else {
words[1] = tmp.slice(-1) + words[1];
}
}
if (ctx.measureText(line + words[0]).width < maxWidth) {
line += words.shift() + " ";
} else {
lines.push(line);
line = "";
}
if (words.length === 0) {
lines.push(line);
}
}
return lines;
}
context.measureText(text).width is what you're looking for...
Try this script to wrap the text on a canvas.
<script>
function wrapText(ctx, text, x, y, maxWidth, lineHeight) {
var words = text.split(' ');
var line = '';
for(var n = 0; n < words.length; n++) {
var testLine = line + words[n] + ' ';
var metrics = ctx.measureText(testLine);
var testWidth = metrics.width;
if (testWidth > maxWidth && n > 0) {
ctx.fillText(line, x, y);
line = words[n] + ' ';
y += lineHeight;
}
else {
line = testLine;
}
}
ctx.fillText(line, x, y);
}
var canvas = document.getElementById('Canvas01');
var ctx = canvas.getContext('2d');
var maxWidth = 400;
var lineHeight = 24;
var x = (canvas.width - maxWidth) / 2;
var y = 70;
var text = 'HTML is the language for describing the structure of Web pages. HTML stands for HyperText Markup Language. Web pages consist of markup tags and plain text. HTML is written in the form of HTML elements consisting of tags enclosed in angle brackets (like <html>). HTML tags most commonly come in pairs like <h1> and </h1>, although some tags represent empty elements and so are unpaired, for example <img>..';
ctx.font = '15pt Calibri';
ctx.fillStyle = '#555555';
wrapText(ctx, text, x, y, maxWidth, lineHeight);
</script>
</body>
See demo here http://codetutorial.com/examples-canvas/canvas-examples-text-wrap.
From the script here: http://www.html5canvastutorials.com/tutorials/html5-canvas-wrap-text-tutorial/
I've extended to include paragraph support. Use \n for new line.
function wrapText(context, text, x, y, line_width, line_height)
{
var line = '';
var paragraphs = text.split('\n');
for (var i = 0; i < paragraphs.length; i++)
{
var words = paragraphs[i].split(' ');
for (var n = 0; n < words.length; n++)
{
var testLine = line + words[n] + ' ';
var metrics = context.measureText(testLine);
var testWidth = metrics.width;
if (testWidth > line_width && n > 0)
{
context.fillText(line, x, y);
line = words[n] + ' ';
y += line_height;
}
else
{
line = testLine;
}
}
context.fillText(line, x, y);
y += line_height;
line = '';
}
}
Text can be formatted like so:
var text =
[
"Paragraph 1.",
"\n\n",
"Paragraph 2."
].join("");
Use:
wrapText(context, text, x, y, line_width, line_height);
in place of
context.fillText(text, x, y);
I am posting my own version used here since answers here weren't sufficient for me. The first word needed to be measured in my case, to be able to deny too long words from small canvas areas. And I needed support for 'break+space, 'space+break' or double-break/paragraph-break combos.
wrapLines: function(ctx, text, maxWidth) {
var lines = [],
words = text.replace(/\n\n/g,' ` ').replace(/(\n\s|\s\n)/g,'\r')
.replace(/\s\s/g,' ').replace('`',' ').replace(/(\r|\n)/g,' '+' ').split(' '),
space = ctx.measureText(' ').width,
width = 0,
line = '',
word = '',
len = words.length,
w = 0,
i;
for (i = 0; i < len; i++) {
word = words[i];
w = word ? ctx.measureText(word).width : 0;
if (w) {
width = width + space + w;
}
if (w > maxWidth) {
return [];
} else if (w && width < maxWidth) {
line += (i ? ' ' : '') + word;
} else {
!i || lines.push(line !== '' ? line.trim() : '');
line = word;
width = w;
}
}
if (len !== i || line !== '') {
lines.push(line);
}
return lines;
}
It supports any variants of lines breaks, or paragraph breaks, removes double spaces, as well as leading or trailing paragraph breaks. It returns either an empty array if the text doesn't fit. Or an array of lines ready to draw.
look at https://developer.mozilla.org/en/Drawing_text_using_a_canvas#measureText%28%29
If you can see the selected text, and see its wider than your canvas, you can remove words, until the text is short enough. With the removed words, you can start at the second line and do the same.
Of course, this will not be very efficient, so you can improve it by not removing one word, but multiple words if you see the text is much wider than the canvas width.
I did not research, but maybe their are even javascript libraries that do this for you
I modified it using the code from here http://miteshmaheta.blogspot.sg/2012/07/html5-wrap-text-in-canvas.html
http://jsfiddle.net/wizztjh/kDy2U/41/
This should bring the lines correctly from the textbox:-
function fragmentText(text, maxWidth) {
var lines = text.split("\n");
var fittingLines = [];
for (var i = 0; i < lines.length; i++) {
if (canvasContext.measureText(lines[i]).width <= maxWidth) {
fittingLines.push(lines[i]);
}
else {
var tmp = lines[i];
while (canvasContext.measureText(tmp).width > maxWidth) {
tmp = tmp.slice(0, tmp.length - 1);
}
if (tmp.length >= 1) {
var regex = new RegExp(".{1," + tmp.length + "}", "g");
var thisLineSplitted = lines[i].match(regex);
for (var j = 0; j < thisLineSplitted.length; j++) {
fittingLines.push(thisLineSplitted[j]);
}
}
}
}
return fittingLines;
And then get draw the fetched lines on the canvas :-
var lines = fragmentText(textBoxText, (rect.w - 10)); //rect.w = canvas width, rect.h = canvas height
for (var showLines = 0; showLines < lines.length; showLines++) { // do not show lines that go beyond the height
if ((showLines * resultFont.height) >= (rect.h - 10)) { // of the canvas
break;
}
}
for (var i = 1; i <= showLines; i++) {
canvasContext.fillText(lines[i-1], rect.clientX +5 , rect.clientY + 10 + (i * (resultFont.height))); // resultfont = get the font height using some sort of calculation
}
This is a typescript version of #JBelfort's answer.
(By the way, thanks for this brilliant code)
As he mentioned in his answer this code can simulate html element such as textarea,and also the CSS property
word-break: break-all
I added canvas location parameters (x, y and lineHeight)
function wrapText(
ctx: CanvasRenderingContext2D,
text: string,
maxWidth: number,
x: number,
y: number,
lineHeight: number
) {
const xOffset = x;
let yOffset = y;
const lines = text.split('\n');
const fittingLines: [string, number, number][] = [];
for (let i = 0; i < lines.length; i++) {
if (ctx.measureText(lines[i]).width <= maxWidth) {
fittingLines.push([lines[i], xOffset, yOffset]);
yOffset += lineHeight;
} else {
let tmp = lines[i];
while (ctx.measureText(tmp).width > maxWidth) {
tmp = tmp.slice(0, tmp.length - 1);
}
if (tmp.length >= 1) {
const regex = new RegExp(`.{1,${tmp.length}}`, 'g');
const thisLineSplitted = lines[i].match(regex);
for (let j = 0; j < thisLineSplitted!.length; j++) {
fittingLines.push([thisLineSplitted![j], xOffset, yOffset]);
yOffset += lineHeight;
}
}
}
}
return fittingLines;
}
and you can just use this like
const wrappedText = wrapText(ctx, dialog, 200, 100, 200, 50);
wrappedText.forEach(function (text) {
ctx.fillText(...text);
});
}
Is there is a way in CSS to make it pick a random font color from an array? I know I can do this with server side or javascript, but I am wondering if there is pure CSS way to do this.
CSS expressions (allowing for dynamic script content via CSS) were abominations cast in the bowels of the hell of inefficiency alongside Web Forms, only ever supported by IE7 and below. But since you asked.
<style>
blink marquee {
color: expression("rgb(" + Math.floor(Math.random() * 255)
+ "," + Math.floor(Math.random() * 255) + ","
+ Math.floor(Math.random() * 255) + ")");
}
</style>
<blink>
<marquee>
color me beautiful
</marquee>
</blink>
This is not possible in CSS, which is firmly deterministic. You could do this with client-side JavaScript, though:
var colors = ['#ff0000', '#00ff00', '#0000ff'];
var random_color = colors[Math.floor(Math.random() * colors.length)];
document.getElementById('title').style.color = random_color;
If you're using jQuery, the last line could become
$('#title').css('color', random_color);
Simple in JavaScript with JQuery.
You could do something like:
var hexArray = ['#hexVal','#hexVal','#hexval', '#hexval']
var randomColor = hexArray[Math.floor(Math.random() * hexArray.length)];
$("#divId").css("color",randomColor); //A class selector would work too
Which would select a new color every time the page refreshes.
This is how I did it.
The first part is a sequential order, element 1 gets color 1 etc.
Then when you are out of colors it will randomize it.
//Specify the class that you want to select
var x = document.getElementsByClassName("ms-webpart-chrome-title");
var i;
var c;
//specify the colors you want to use
var colors = ["#009933", "#006699", "#33cccc", "#99cc00", "#f60"];
var d = colors.length;
for (i = 0; i < x.length; i++){
while (i < d) {
c = i;
var random_color = colors[c];
x[i].style.borderTopColor = random_color;
i++;
}
while (i >= d) {
var random_color = colors[Math.floor(Math.random() * colors.length)];
x[i].style.borderTopColor = random_color;
i++;
}
}
without using predefined color set, to get a uniformly randomized color function
function randomColor(){
rc = "#";
for(i=0;i<6;i++){
rc += Math.floor(Math.random()*16).toString(16);
}
return rc;
}
or inline
"#"+Math.floor(Math.random() * 0x1000000).toString(16)
I'm making an image gallery and I want to have a bunch of thumbnails on the bottom of the screen that smoothly slide from side to side when the mouse moves.
I'm working with a custom class for the container (Tiles) and a custom class for the thumbnails (ImageIcon).
I have a ComboBox which allows users to to choose a gallery. When the user chooses a gallery, the following code is run and the thumbnails should reload. The problem here is that the icons appear on top of each other instead of side by side, also switching categories should remove the old one (see the first for loop), but it does not. Also, the Icons are not animating properly. The animation code is below as well. Right now, only one of the icons will move. The icons should move in order from side to side, stopping when the last few icons hit the edge of the screen, so that they don't get "lost" somewhere off to the side.
Gallery Loader Code:
public function loadCategory(xml:XML = null) {
if (xml != null) {
dp = new DataProvider(xml);
for (var k:int = 0; k < this.numChildren; k++) {
this.removeChild(this.getChildAt(k));
}
var black:DropShadowFilter = new DropShadowFilter(0, 45, 0x000000, 1, 3, 3, 1, 1);
var white:DropShadowFilter = new DropShadowFilter(0, 45, 0xFFFFFF, 1, 2, 2, 1, 1);
for (var i:int = 0; i < dp.length; i++) {
var imgicon:ImageIcon = new ImageIcon();
imgicon.addEventListener(MouseEvent.CLICK, showImage);
imgicon.width = 100;
imgicon.x = (i * (imgicon.width + 20));
imgicon.path = dp.getItemAt(i).data;
imgicon.loadIcon();
imgicon.filters = [black, white];
stage.addEventListener(Event.ENTER_FRAME, moveIcon);
this.addChild(imgicon);
}
} else {
//Failed to load XML
}
}
Icon Animation Code:
public function moveIcon(e:Event){
var speed = 0;
speed = Math.floor(Math.abs(this.mouseX/20));
var image = this.getChildAt(k);
var imagebox = image.width + 20;
var edge:Number = (800/2);
if (this.mouseX > 0) {
for (var k:int = 0; k < this.numChildren; k++) {
if (image.x - (imagebox/2) + speed < -edge + (k * imagebox)) {
speed = 0;
}
image.rotationY = Math.floor(image.x/20);
image.x -= Math.floor(speed);
}
} else {
for (var j = this.numChildren; j >= 0; j--) {
if (image.x + speed > edge - ((imagebox * j) )) {
speed = 0;
}
image.rotationY = Math.floor(image.x/20);
image.x += Math.floor(speed);
}
}
}
As I see it, you have three questions (You should have put these at the end of your question instead of "What is wrong with my code?"). One of the main principles of programming is breaking problems into smaller parts.
How do I line up the ImageIcon beside each other?
How do I remove the old ImageIcon, when switching categories?
How do I animate ALL the ImageIcons together, based on the mouse position, with constraints on either side?
Question 1
I can't see anything wrong, but just check that when you are setting imgicon.x, that imgicon.width is actually set.
Question 2
Instead of relying on numChildren and getChildAt(), I would create a currentIcons array member variable, and when you create a new ImageIcon, just push it onto the array. Then when you want to remove them, you can just loop through the array like this:
for each (var cIcon:ImageIcon in currentIcons)
{
cIcon.removeEventListener(MouseEvent.CLICK, showImage);
removeChild(cIcon);
}
currentIcons = [];
As you can see, I am also removing any listeners that I have added. This is best practice. Then clearing the array when I have removed all the icons.
Question 3
I can see a few things wrong with your code. First, in the line where image is set, k is not set yet!
Here you can also use the currentIcons array, but you probably can't use a for each in loop, because that gives you the items out of order. Just a normal for loop will be better.
I haven't tested this code for the moveIcon method, but the idea should work. You may have to tweek it though:
public function moveIcon(e:Event):void
{
var speed:Number = Math.floor(this.mouseX / 20); // Removed "abs".
var imageBox:Number = currentIcons[0].width;
var edge:Number = 800 / 2;
for (var i:int = 0; i < currentIcons.length; i++)
{
var image:ImageIcon = currentIcons[i] as ImageIcon;
image.x += speed;
image.rotationY = Math.floor(image.x / 20);
var min:int = -edge + (i * imagebox);
if (image.x < min) image.x = min;
var max:int = edge - (imagebox * i);
if (image.x > max) image.x = max;
}
}
EDIT* Sorry, it was supposed to be a greater than in the last if statement, but I had a less than by accident.