Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 2 years ago.
Improve this question
I'm making a game using HTML Canvas and vanilla javascript. I'm new to javascript so this may be easier than I think. I have two array's of objects that represent cards, each object has a 'text' property of between 40-100 characters that's drawn onto the screen dynamically onto a card that is 130 x 70px.
I need to format the text to fit the width restriction of the card (130px) and create a new line whenever necessary.
Any help is appreciated
Edited to make clearer
You can use the measureText() method from the canvas API.
As noted by Ken Fyrstenberg in this awesome answer,
canvas' measureText doesn't currently support measuring height (ascent + descent).
Below attempt uses an hardcoded lineHeight value that you'd have to find before rendering text. Ken's answer does provide a way to programmatically find it.
[ Edit: thanks to markE's comment below, it now uses an approximation of 1.286*the font-size. ]
So here it is, dirty and there must be better ways to do so but anyway...
var input = document.querySelector('input');
input.addEventListener('keyup', write, false);
var c = document.createElement('canvas'),ctx = c.getContext('2d');
c.width = 400, c.height = 150; document.body.appendChild(c);
// simple box object for the card
var card = {x: 25, y: 25, w: 130, h: 70};
ctx.fillStyle = "#CCCCCC";
ctx.fillRect(card.x, card.y, card.w, card.h);
var fontSize = 12;
ctx.font=fontSize+'px arial';
// Margins multipliers are chosen arbitrarly here, just because they fit well to my eyes
var margins = {t: 1.25*fontSize, l: .7*fontSize, b: 2*fontSize, r: .7*fontSize},
marginsX = margins.l+margins.r,
marginsY = margins.t+margins.b;
// As suggested by markE, lineHeight is set to 1.286* the fontSize,
// for a calculated way, see Ken's answer here : https://stackoverflow.com/a/17631567/3702797
var lineHeight = 1.286*fontSize;
// just a shortcut
var lineWidth = function(text) {
return ctx.measureText(text).width;
};
function write() {
var txt = this.value;
// Redraw our card
ctx.fillStyle = "#CCCCCC";
ctx.fillRect(card.x, card.y, card.w, card.h);
// Split the input at any white space, you might want to change it
var txtLines = txt.split(/\s/);
var i = 0, maxWidth = card.w - marginsX;
if(lineWidth(txt[0])>card.w || lineHeight>card.h-(margins.t/4) ){
console.warn('TOO BIG FONT!!');
return;
}
while(txtLines[i]) {
// If our current line merged with the next line width isn't greater than the card one
if (txtLines[i+1] && lineWidth(txtLines[i] + ' ' + txtLines[i+1]) < maxWidth) {
// Merge them
txtLines[i] += ' ' + txtLines.splice(i + 1, 1);
}
else {
// Is the one word too big? --> Dirtyphenation !
if (lineWidth(txtLines[i]) > maxWidth) {
// Add a new index with the two last chars since we'll add a dash
txtLines.splice(i+1, 0, "");
// If it's still too big
while (lineWidth(txtLines[i]) > maxWidth) {
var lastChars = txtLines[i].length - 2;
// Append those two last chars to our new array index
txtLines[i+1] = txtLines[i].substring(lastChars) + txtLines[i+1];
// Remove them from our current index
txtLines[i] = txtLines[i].substring(0, lastChars);
}
// Add the final dash
txtLines[i] += "-";
}
// Is our text taller than the card height?
if (lineHeight*i > card.h-marginsY){
// If there is more text coming after...
if (txtLines[i+1]){
// ...and it fits in the line
if(lineWidth(txtLines[i]+' '+txtLines[i+1])<maxWidth){
continue;
}
// ...and it doesn't fit in the line
else{
// Does a single char fit with the ellipsis ?
if(lineWidth(txtLines[i][0]+'...')<maxWidth){
// remove a char until we can put our ellipsis
while (lineWidth(txtLines[i]+'...') > maxWidth){
txtLines[i] = txtLines[i].substring(0,txtLines[i].length-1)
}
}else{
return;
}
txtLines[i] += '...';
// remove the surplus from the array
txtLines = txtLines.slice(0,i+1);
}
}
// stop looping here since we don't have space anymore
break;
}
// Go to next line
i++;
}
}
ctx.fillStyle = "#000";
// Where to draw
var x = card.x + (margins.l);
var y = card.y + (margins.t);
// Iterate through our lines
for (var i = 0; i < txtLines.length; i++) {
ctx.fillText(txtLines[i], x, y + (i * lineHeight));
}
}
canvas {border: 1px solid}
<input type="text" />
Related
first-time/long-time (quack, quack).
I'm a bit frustrated, and just about stumped, by this riddle I can't quite solve in Snap.svg. It's probably an oversight that I'll kick myself for missing, but I'm not seeing it at this point.
I have x & y data that I draw from a DOM element and store into a series of arrays, filter based on certain values in certain columns, and eventually create multiple instances of the ChartLine object in js. Basically, it sorts a certain column by quantity, assigns each value's row a color from a RainbowVis.js object, pushes all the relevant values from each row into an array for y, and draws a path on the line chart where y is the value and x is a steadily-increasing integer in a For loop.
What I'm currently doing here, in the draw() function, is: for each relevant column, create a <circle> with the variable "dot" with the object's x & y variables, assign the attributes, animate the radius from 0 to 8 in a quarter-second, and add the x & y values of i to a string to be used in a <path> I create right after the For loop. I then animate the path, etc., etc.
Without the setTimeout(), it works well. The circles and paths all animate simultaneously on load. However, I want to add a delay to each .animate with the number of milliseconds increasing by polyDelayInterval in each iteration, so each "dot" animates as the line arrives at it. At the VERY least, I want to animate all of the "dots" after the path is done animating.
The problem is, no matter what I've tried so far, I can only get the last set of "dots" (at the highest x value for each line) to animate; the rest stay at r:0. I've read several somewhat-similar posts, both here and elswhere; I've searched up and down the Snap.svg's docs on their site. I just cannot find what I'm doing wrong. Thanks in advance!
var svgMFLC = Snap('svg#ElementID');
function ChartLine(x, y, color, row) {
this.x = x;
this.y = y;
this.color = color;
this.row = row;
var propNames = Object.keys(this.row);
var yAdjust;
var resetX = this.x;
for (var i = 0; i < propNames.length; i++) { // get only the calculated score columns and their values
if (propNames[i].toLowerCase().includes(calcKeyword)) {
yAdjustedToChartArea = chartBottom - (this.row[propNames[i]] * yInterval);
this.y.push(yAdjustedToChartArea); // returns the value of that score column and pushes it to the y array
}
}
this.draw = function () {
var points = "M"; // the string that will determine the coordinates of each line
var dotShadow = svgMFLC.filter(Snap.filter.shadow(0, 0, 2, "#000000", 0.4));
var polyTime = 1500; // in milliseconds
var dot;
var polyDelayInterval = polyTime / (semesterCols.length - 1);
for (var i = 0; i < semesterCols.length; i++) { // for each data point, create a "dot"
dot = svgMFLC.circle(this.x, this.y[i], 0);
dot.attr({
fill: this.color,
stroke: "none",
filter: dotShadow,
class: "chartPointMFLC"
});
setTimeout(function () {
dot.animate({ r: 8 }, 250, mina.easeout);
}, polyDelayInterval * i);
points += this.x + " " + this.y[i] + " L";
this.x = this.x + xInterval;
}
points = points.slice(0, -2); // take away the excessive " L" from the end of the points string
var poly = svgMFLC.path(points);
var polyLength = poly.getTotalLength();
poly.attr({
fill: "none",
stroke: this.color,
class: "chartLineMFLC",
strokeDasharray: polyLength + " " + polyLength, // setting the strokeDash attributes will help create the "drawing the line" effect when animated
strokeDashoffset: polyLength
});
poly.animate({ strokeDashoffset: 0.00 }, polyTime);
this.x = resetX;
}
}
I can't put a tested solution up without the full code to test, but the problem is almost certainly that you at least need to get a closure for your 'dot' element.
So this line...
setTimeout(function () {
dot.animate({ r: 8 }, 250, mina.easeout);
}, polyDelayInterval * i);
When it comes to call that function, 'dot' will be the last 'dot' from the loop. So you need to create a closure (create functional scope for dot).
So something a bit like...
(function() {
var laterDot = dot;
setTimeout(function () {
laterDot.animate({ r: 8 }, 250, mina.easeout);
}, polyDelayInterval * i)
})();
I am creating a multiple choice quiz, however, the questions are too many characters and won't display on 1 line. I understand I have to use the wrapText function in order to paragraph it, I am just unsure how to implement this within my code. The Questions are set as strings in a variable (Questions), with the answers being strings in variable (Options). textpos1, defines the coordinates where I want my question to start, with textpos2 - textpos4 defining the coordinates where the separate answers start. These coordinates are in these positions to align to my background image.
JSFiddle here with full code however it doesn't seem to like my BG image on there... http://jsfiddle.net/danielparry8/6U9Rn/
var canvas = document.getElementById("myCanvas");
var context = canvas.getContext("2d");
var quizbg = new Image();
var Question = new String;
var Option1 = new String;
var Option2 = new String;
var Option3 = new String;
var mx=0;
var my=0;
var CorrectAnswer = 0;
var qnumber = 0;
var rightanswers=0;
var wronganswers=0;
var QuizFinished = false;
var lock = false;
var textpos1=25;
var textpos2=145;
var textpos3=230;
var textpos4=325;
var Questions = ["Which Manchester United Player won \n the 2008 Golden Boot with 31 Goals?","At which club did Bobby Charlton start his football career?","Which year did Wayne Rooney win the BBC Young Sports Personality of the year award?"];
var Options = [["Cristiano Ronaldo","Wayne Rooney","Ryan Giggs"],["Manchester United","Manchester City","Chelsea"],["2002","2003","2004"]];
The 'setQuestions' function grabs the appropriate question and answers and uses fillText to apply them to the canvas, my only issue being that the Question is displayed on one continuos line.
SetQuestions = function(){
Question=Questions[qnumber];
CorrectAnswer=1+Math.floor(Math.random()*3);
if(CorrectAnswer==1){Option1=Options[qnumber][0];Option2=Options[qnumber][1];Option3=Options[qnumber][2];}
if(CorrectAnswer==2){Option1=Options[qnumber][2];Option2=Options[qnumber][0];Option3=Options[qnumber][1];}
if(CorrectAnswer==3){Option1=Options[qnumber][1];Option2=Options[qnumber][2];Option3=Options[qnumber][0];}
context.textBaseline = "middle";
context.font = "16pt sans-serif,Arial";
context.fillText(Question,20,textpos1);
context.font = "14pt sans-serif,Arial";
context.fillText(Option1,20,textpos2);
context.fillText(Option2,20,textpos3);
context.fillText(Option3,20,textpos4);
}
Below is a wrapText function i have tried to implement into my design with no avail, If anyone can help that is greatly appreciated. Thanks!
function wrapText(context, text, x, y, maxWidth, fontSize, fontFace){
var words = text.split(' ');
var Questions = '';
var lineHeight=fontSize;
context.font=fontSize+" "+fontFace;
for(var n = 0; n < words.length; n++) {
var testLine = Questions + words[n] + ' ';
var metrics = context.measureText(testLine);
var testWidth = metrics.width;
if(testWidth > maxWidth) {
context.fillText(Questions, x, y);
Questions = words[n] + ' ';
y += lineHeight;
}
else {
Questions = testLine;
}
}
context.fillText(Questions, x, y);
return(y);
}
I experimented with your example code. You don't indicate exactly what you expect to be passed in the 'fontSize' argument to your 'wrapText' function, but I assumed it would be something like '16pt'. So you have this code:
var lineHeight=fontSize;
context.font=fontSize+" "+fontFace;
so now...
lineHeight === '16pt'
context.font === '16 pt sans-serif,Arial'
Then later your code says:
y+= lineHeight;
Now you have a problem, because adding '16pt' (lineHeight) to y doesn't make any sense. The value for 'y' is is a simple integer value (representing pixels). Trying to add the string, '16pt' to it results in a strange value and strange results.
I used the code from JSFiddle, copied it to my own sandbox. Then I cut/pasted from above, the code for wrapText.
I substituted for:
context.fillText(Question, 20, textpos1);
with:
wrapText(context,Question,20,textpos1,500,'16pt','sans-serif,Arial');
inside wrapText where
y = lineHeight;
instead use:
y = 16;
You should see your wrapped line. y = 16 is just an arbitrary number I chose. It is representing pixels...it isn't the same thing as 16pt.
Here's an example and a Demo: http://jsfiddle.net/m1erickson/QGz2R/
If you create a "class" then you can reuse that "class" to store and draw as many questions as you need.
First, create a Question & Answer class that holds:
the question text
each answer text
the bounding box of each answer that's drawn on the canvas
the correct answer
Example Code:
// a Question & Answer "class"
function QA(question,options,correctOption){
this.QBB=null; // the bounding box for the wrapped question text
this.Q=question;
this.A=[];
this.correct=correctOption;
for(var i=0;i<options.length;i++){
this.A.push({text:options[i],BB:null});
}
}
// Draw the question and possible answers on the canvas
// At the same time calculate the bounding boxes(BB)
// of each answer so that the BB can be used to test
// if the user clicked in the correct answer
QA.prototype.draw=function(x,y,maxWidth){
var BB=this.QBB=wrapText(this.Q,x,y,maxWidth,14,"verdana");
for(var i=0;i<this.A.length;i++){
y+=BB.height;
BB=this.A[i].BB=wrapText(i+". "+this.A[i].text,x,y,maxWidth,14,"verdana");
ctx.strokeRect(BB.x,BB.y,BB.width,BB.height);
}
return({x:x,y:y});
}
// test if the supplied mouseX/mouseY is inside any answer
QA.prototype.hitAnswer=function(x,y){
for(var i=0;i<this.A.length;i++){
var bb=this.A[i].BB;
if(x>bb.x && x<bb.x+bb.width && y>bb.y && y<bb.y+bb.height){
return(i);
}
}
return(-1);
}
Next,
Listen for mousedown events
Test if the user has clicked in any answer. You do this by testing if the mouse was clicked inside the bounding box of any of the answers.
If the user clicked in an answer alert whether they selected the correct answer or not.
Example code:
// Listen for mousedown and check if the user has
// clicked in an answer to the question
function handleMouseDown(e){
e.preventDefault();
e.stopPropagation();
mouseX=parseInt(e.clientX-offsetX);
mouseY=parseInt(e.clientY-offsetY);
var answer=q.hitAnswer(mouseX,mouseY);
if(answer==q.correct){
$results.text("Correct! #"+q.correct+" is the answer");
}else{
$results.text("Sorry! #"+answer+" is not the answer");
}
}
And here is the slightly modified wrapText code that now returns the bounding box of the text it just wrapped on the canvas:
// wrap the specified text and draw it to the canvas
// and return the bounding box of that wrapped text
function wrapText(text, x, y, maxWidth, fontSize, fontFace){
var startY=y;
var words = text.split(' ');
var line = '';
var lineHeight=fontSize+2;
ctx.font=fontSize+" "+fontFace;
y+=fontSize;
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) {
ctx.fillText(line, x, y);
line = words[n] + ' ';
y += lineHeight;
}
else {
line = testLine;
}
}
ctx.fillText(line, x, y);
return({x:x,y:startY,width:maxWidth,height:y-startY+4});
}
I'm building a turn based HTML game based on a 2D square grid. Each grid square could take a variable number of movement points to cross (IE: 1 MP for roads, 1.5 MP for grasslands, 2 MP for forests, etc). When the user clicks on a unit I want to determine all possible movable spaces with said unit's allotted movement points so that I can highlight them and make them clickable.
Is there a free library available to do this? I've seen a few pathing algorithms but nothing about determining movable area. How do other game developers handle this problem? I'm open to both vanilla JS and JQuery solutions.
Well, I decided to try and attack this myself. I've never been great at these sorts of algorithms so I'm sure there's a more efficient way to handle it than what I've done. However, for my purposes it runs quickly enough and is very simple and easy to understand.
In case it's helpful to anyone else looking to do the same, I've included the code below. This is an updated version of my original answer, which I modified to also store the path taken so that you can show the units moving through the correct spaces. This answer uses JQuery in the lower examples, but only in a few places; you can easily enough replace them with vanilla JS. And the first block of code, containing the actual path/area finding functionality, is pure JS.
<script>
var possibleMovementAreaArray = new Array(); // This array will hold our allowable movement tiles. Your other functions can access this after running possibleMovementArea().
function possibleMovementArea(unitIndex) {
// I'm storing each unit in my game in an array. So I pass in the index of the unit I want to determine the movement area for.
var x = unitList[unitIndex][10]; // x coordinate on the playgrid
var y = unitList[unitIndex][11]; // y coordinate on the playgrid
var mp = unitList[unitIndex][15]; // number of movement points
possibleMovementAreaArray.length = 0; // Clear our array so previous runs don't interfere.
findPossibleMovement(x, y, mp);
}
function findPossibleMovement(x, y, mp, prevStepX, prevStepY) {
// This is a recursive function; something I'm not normally too good at.
for (var d=1; d<=4; d++) {
// We run through each of the four cardinal directions. Bump this to 8 and add 4 more cases to include corners.
if (d == 1) {
// Check Up
var newX = x;
var newY = y - 1;
} else if (d == 2) {
// Check Down
var newX = x;
var newY = y + 1;
} else if (d == 3) {
// Check Left
var newX = x - 1;
var newY = y;
} else if (d == 4) {
// Check Right
var newX = x + 1;
var newY = y;
}
// Check to see if this square is occupied by another unit. Two units cannot occupy the same space.
spaceOccupied = false;
for (var j=1; j<=numUnits; j++) {
if (unitList[j][10] == newX && unitList[j][11] == newY)
spaceOccupied = true;
}
if (!spaceOccupied) {
// Modify this for loop as needed for your usage. I have a 2D array called mainMap that holds the ID of a type of terrain for each tile.
// I then have an array called terList that holds all the details for each type of terrain, such as movement points needed to get past.
// This for loop is just looking up the ID of the terrain for use later. Sort of like a "SELECT * FROM terrainInfo WHERE ID=terrainOfCurrentTile".
for (var j=1; j<=numTerrains; j++) {
if (newX > 0 && newX <= mapWidth && newY > 0 && newY <= mapHeight && terList[j][1] == mainMap[newX][newY])
break; // After finding the index of terList break out of the loop so j represents the correct index.
}
if (j <= numTerrains) { // Run if an actual terrain is found. No terrain is found if the search runs off the sides of the map.
var newMp = mp - terList[j][7]; // Decrement the movement points for this particular path.
if (newMp >= 0) { // Only continue if there were enough movement points to move to this square.
// Check to see if this square is already logged. For both efficiency and simplicity we only want each square logged once.
var newIndex = possibleMovementAreaArray.length
var alreadyLogged = false
if (possibleMovementAreaArray.length > 0) {
for (var j=0; j<possibleMovementAreaArray.length; j++) {
if (possibleMovementAreaArray[j][1] == newX && possibleMovementAreaArray[j][2] == newY) {
alreadyLogged = true;
var alreadyLoggedIndex = j;
}
}
}
if (!alreadyLogged) {
// This adds a row to the array and records the x and y coordinates of this tile as movable
possibleMovementAreaArray[newIndex] = new Array(6);
possibleMovementAreaArray[newIndex][1] = newX;
possibleMovementAreaArray[newIndex][2] = newY;
possibleMovementAreaArray[newIndex][3] = prevStepX; // This tracks the x coords of the steps taken so far to get here.
possibleMovementAreaArray[newIndex][4] = prevStepY; // This tracks the y coords of the steps taken so far to get here.
possibleMovementAreaArray[newIndex][5] = newMp; // Records remaining MP after the previous steps have been taken.
}
if (alreadyLogged && newMp > possibleMovementAreaArray[alreadyLoggedIndex][5]) {
// If this tile was already logged, but there was less MP remaining on that attempt, then this one is more efficient. Update the old path with this one.
possibleMovementAreaArray[alreadyLoggedIndex][3] = prevStepX;
possibleMovementAreaArray[alreadyLoggedIndex][4] = prevStepY;
possibleMovementAreaArray[alreadyLoggedIndex][5] = newMp;
}
if (newMp > 0) {
// Now update the list of previous steps to include this tile. This list will be passed along to the next call of this function, thus building a path.
if (prevStepX == '') {
var newPrevStepX = [newX];
var newPrevStepY = [newY];
} else {
// This code is required to make a full copy of the array holding the existing list of steps. If you use a simple equals then you just create a reference and
// subsequent calls are all updating the same array which creates a chaotic mess. This way we store a separate array for each possible path.
var newPrevStepX = prevStepX.slice();
newPrevStepX.push(newX);
var newPrevStepY = prevStepY.slice();
newPrevStepY.push(newY);
}
// If there are still movement points remaining, check and see where we could move next.
findPossibleMovement(newX, newY, newMp, newPrevStepX, newPrevStepY);
}
}
}
}
}
}
</script>
After running the above, you can then loop through the array to find all usable tiles. Here is how I did it:
<script>
// Shows the movement area based on the currently selected unit.
function showMovement() {
var newHTML = "";
curAction = "move";
possibleMovementArea(curUnit); // See above code
for (x=0; x<possibleMovementAreaArray.length; x++) {
// Loop over the array and do something with each tile. In this case I'm creating an overlay that I'll fade in and out.
var tileLeft = (possibleMovementAreaArray[x][1] - 1) * mapTileSize; // Figure out where to absolutely position this tile.
var tileTop = (possibleMovementAreaArray[x][2] - 1) * mapTileSize; // Figure out where to absolutely position this tile.
newHTML = newHTML + "<img id='path_" + possibleMovementAreaArray[x][1] + "_" + possibleMovementAreaArray[x][2] + "' onClick='mapClk(" + possibleMovementAreaArray[x][1] + ", " + possibleMovementAreaArray[x][2] + ", 0);' src='imgs/path.png' class='mapTile' style='left:" + tileLeft + "px; top:" + tileTop + "px;'>";
}
$("#movementDiv").html(newHTML); // Add all those images into a preexisting div.
$("#movementDiv").css("opacity", "0.5"); // Fade the div to 50%
$("#movementDiv").show(); // Make the div visible.
startFading(); // Run a routine to fade the div in and out.
}
</script>
Since we determined the path, we can easily show movement as well by looping through the stored information:
<script>
for (j=0; j<possibleMovementAreaArray[areaIndex][3].length; j++) {
// This loop moves the unit img to each tile on its way to its destination. The final destination tile is not included.
var animSpeed = 150; // Time in ms that it takes to move each square.
var animEase = "linear"; // We want movement to remain a constant speed through each square in this case.
var targetLeft = (possibleMovementAreaArray[areaIndex][3][j]-1) * mapTileSize; // This looks at each step in the path array and multiplies it by tile size to determine the new horizonal position.
var targetTop = (possibleMovementAreaArray[areaIndex][4][j]-1) * mapTileSize; // This looks at each step in the path array and multiplies it by tile size to determine the new vertical position.
$("#char_"+curUnit).animate({"left":targetLeft, "top":targetTop}, animSpeed, animEase); // Do the animation. Subsequent animations get queued.
}
// Now we need to move to that last tile.
newLeft = (x-1) * mapTileSize;
newTop = (y-1) * mapTileSize;
$("#char_"+curUnit).animate({"left":newLeft, "top":newTop}, 400, "easeOutCubic"); // Slow unit at the end of journey for aesthetic purposes.
$("#char_"+curUnit).addClass("unitMoved", 250); // Turns the image grayscale so it can easily be seen that it has already moved.
</script>
Hopefully this is helpful to others.
I am creating a new "whack-a-mole" style game where the children have to hit the correct numbers in accordance to the question. So far it is going really well, I have a timer, count the right and wrong answers and when the game is started I have a number of divs called "characters" that appear in the container randomly at set times.
The problem I am having is that because it is completely random, sometimes the "characters" appear overlapped with one another. Is there a way to organize them so that they appear in set places in the container and don't overlap when they appear.
Here I have the code that maps the divs to the container..
function randomFromTo(from, to) {
return Math.floor(Math.random() * (to - from + 1) + from);
}
function scramble() {
var children = $('#container').children();
var randomId = randomFromTo(1, children.length);
moveRandom('char' + randomId);
}
function moveRandom(id) {
var cPos = $('#container').offset();
var cHeight = $('#container').height();
var cWidth = $('#container').width();
var pad = parseInt($('#container').css('padding-top').replace('px', ''));
var bHeight = $('#' + id).height();
var bWidth = $('#' + id).width();
maxY = cPos.top + cHeight - bHeight - pad;
maxX = cPos.left + cWidth - bWidth - pad;
minY = cPos.top + pad;
minX = cPos.left + pad;
newY = randomFromTo(minY, maxY);
newX = randomFromTo(minX, maxX);
$('#' + id).css({
top: newY,
left: newX
}).fadeIn(100, function () {
setTimeout(function () {
$('#' + id).fadeOut(100);
window.cont++;
}, 1000);
});
I have a fiddle if it helps.. http://jsfiddle.net/pUwKb/8/
As #aug suggests, you should know where you cannot place things at draw-time, and only place them at valid positions. The easiest way to do this is to keep currently-occupied positions handy to check them against proposed locations.
I suggest something like
// locations of current divs; elements like {x: 10, y: 40}
var boxes = [];
// p point; b box top-left corner; w and h width and height
function inside(p, w, h, b) {
return (p.x >= b.x) && (p.y >= b.y) && (p.x < b.x + w) && (p.y < b.y + h);
}
// a and b box top-left corners; w and h width and height; m is margin
function overlaps(a, b, w, h, m) {
var corners = [a, {x:a.x+w, y:a.y}, {x:a.x, y:a.y+h}, {x:a.x+w, y:a.y+h}];
var bWithMargins = {x:b.x-m, y:b.y-m};
for (var i=0; i<corners.length; i++) {
if (inside(corners[i], bWithMargins, w+2*m, h+2*m) return true;
}
return false;
}
// when placing a new piece
var box;
while (box === undefined) {
box = createRandomPosition(); // returns something like {x: 15, y: 92}
for (var i=0; i<boxes.length; i++) {
if (overlaps(box, boxes[i], boxwidth, boxheight, margin)) {
box = undefined;
break;
}
}
}
boxes.push(box);
Warning: untested code, beware the typos.
The basic idea you will have to implement is that when a random coordinate is chosen, theoretically you SHOULD know the boundaries of what is not permissible and your program should know not to choose those places (whether you find an algorithm or way of simply disregarding those ranges or your program constantly checks to make sure that the number chosen isn't within the boundary is up to you. the latter is easier to implement but is a bad way of going about it simply because you are entirely relying on chance).
Let's say for example coordinate 50, 70 is selected. If the picture is 50x50 in size, the range of what is allowed would exclude not only the dimensions of the picture, but also 50px in all directions of the picture so that no overlap may occur.
Hope this helps. If I have time, I might try to code an example but I hope this answers the conceptual aspect of the question if that is what you were having trouble with.
Oh and btw forgot to say really great job on this program. It looks awesome :)
You can approach this problem in at least two ways (these two are popped up in my head).
How about to create a 2 dimensional grid segmentation based on the number of questions, the sizes of the question panel and an array holding the position of each question coordinates and then on each time frame to position randomly these panels on one of the allowed coordinates.
Note: read this article for further information: http://eloquentjavascript.net/chapter8.html
The second approach follow the same principle, but this time to check if the panel overlap the existing panel before you place it on the canvas.
var _grids;
var GRID_SIZE = 20 //a constant holding the panel size;
function createGrids() {
_grids = new Array();
for (var i = 0; i< stage.stageWidth / GRID_SIZE; i++) {
_grids[i] = new Array();
for (var j = 0; j< stage.stageHeight / GRID_SIZE; j++) {
_grids[i][j] = new Array();
}
}
}
Then on a separate function to create the collision check. I've created a gist for collision check in Actionscript, but you can use the same principle in Javascript too. I've created this gist for inspirational purposes.
Just use a random number which is based on the width of your board and then modulo with the height...
You get a cell which is where you can put the mole.
For the positions the x and y should never change as you have 9 spots lets say where the mole could pop up.
x x x
x x x
x x x
Each cell would be sized based on % rather then pixels and would allow re sizing the screen
1%3 = 1 (x)
3%3 = 0 (y)
Then no overlap is possible.
Once the mole is positioned it can be show or hidden or moved etc based on some extended logic if required.
If want to keep things your way and you just need a quick re-position algorithm... just set the NE to the SW if the X + width >= x of the character you want to check by setting the x = y+height of the item which overlaps. You could also enforce that logic in the drawing routine by caching the last x and ensuring the random number was not < last + width of the item.
newY = randomFromTo(minY, maxY);
newX = randomFromTo(minX, maxX); if(newX > lastX + characterWidth){ /*needful*/}
There could still however be overlap...
If you wanted to totally eliminate it you would need to keep track of state such as where each x was and then iterate that list to find a new position or position them first and then all them to move about randomly without intersecting which would would be able to control with just padding from that point.
Overall I think it would be easier to just keep X starting at 0 and then and then increment until you are at a X + character width > greater then the width of the board. Then just increase Y by character height and Set X = 0 or character width or some other offset.
newX = 0; newX += characterWidth; if(newX + chracterWidth > boardWidth) newX=0; newY+= characterHeight;
That results in no overlap and having nothing to iterate or keep track of additional to what you do now, the only downside is the pattern of the displayed characters being 'checker board style' or right next to each other (with possible random spacing in between horizontal and vertical placement e.g. you could adjust the padding randomly if you wanted too)
It's the whole random thing in the first place that adds the complexity.
AND I updated your fiddle to prove I eliminated the random and stopped the overlap :)
http://jsfiddle.net/pUwKb/51/
I need to calculate the exact size of the letter in javascript. The letter can have different font-size or font-family attributes, etc.
I tried to use div element for this, but this method gives only the size of the div, not letter.
<div style="display: inline; background-color: yellow; font-size: 53px; line-height:32px">A</div>
Does anybody know how to solve this issue?
This is basically not possible for the general case. Font kerning will result in variable "widths" of letters depending on the operating system, browser, etc etc, and based on which letters are next to each other. Font substitution may happen if the os+browser don't have the font you specify.
Perhaps re-asking the question with the higher-level goal you're shooting for might result in proposed other approaches to your problem that might be more fruitful?
As others have mentioned, this isn't possible to measure directly. But you can get at it in a more roundabout way: draw the letter onto a canvas and determine which pixels are filled in.
Here's a demo that does this. The meat is this function:
/**
* Draws a letter in the given font to an off-screen canvas and returns its
* size as a {w, h, descenderH} object.
* Results are cached.
*/
function measureLetter(letter, fontStyle) {
var cacheKey = letter + ' ' + fontStyle;
var cache = measureLetter.cache;
if (!cache) {
measureLetter.cache = cache = {};
}
var v = cache[cacheKey];
if (v) return v;
// Create a reasonably large off-screen <canvas>
var cnv = document.createElement('canvas');
cnv.width = '200';
cnv.height = '200';
// Draw the letter
var ctx = cnv.getContext('2d');
ctx.fillStyle = 'black';
ctx.font = fontStyle;
ctx.fillText(letter, 0.5, 100.5);
// See which pixels have been filled
var px = ctx.getImageData(0, 0, 200, 200).data;
var minX = 200, minY = 200, maxX = 0, maxY = 0;
var nonZero = 0;
for (var x = 0; x < 200; x++) {
for (var y = 0; y < 200; y++) {
var i = 4 * (x + 200 * y);
var c = px[i] + px[i + 1] + px[i + 2] + px[i + 3];
if (c === 0) continue;
nonZero++;
minX = Math.min(x, minX);
minY = Math.min(y, minY);
maxX = Math.max(x, maxX);
maxY = Math.max(y, maxY);
}
}
var o = {w: maxX - minX, h: maxY - minY, descenderH: maxY - 100};
cache[cacheKey] = o;
return o;
}
Note that this is sensitive to antialiasing—the results might be off by a pixel.
#Irongaze.com is right that your fonts, depending on conditions, will have varying actual sizes.
If you want to calibrate for a specific letter, I believe element.getBoundingClientRect() will give you useful coordinates. Be sure to fully reset the container wich you are using as a control box. Mind that on different systems you might get different results.
jsFiddle
Please note that this will not give you the size of the actual visible part of the letter, but the size of the container it determines. line-height for example, will not change the actual letter size, but it will affect other letters' positioning. Be aware of that.
It will help us if you describe the problem you are trying to solve. There might be better solutions.