How to rotate images (360 degree with up and down) in javascript - javascript

I try to create a images rotate 360 degree in javascript which is working with left to right perfectly but when I try to move it with bottom to top and top to bottom then it didn't work perfectly I want to create such a demo which show in example
http://www.ajax-zoom.com/examples/example28_clean.php
e(f).mousemove(function(e)
{
if (s == true) dx(e.pageX - this.offsetLeft,e.pageY - this.offsetTop);
else o = e.pageX - this.offsetLeft; f = e.pageY- this.offsetTop;
});
function dx(t,q) {
console.log("t.....x. px.."+t+" -"+ px +"-----q---------y------"+q);
if(f - q > 0.1)
{
f = q;
a="left-top/";
i=43;
r = --r < 1 ? i : r;
e(u).css("background-image", "url(" + a + r + "." + c + ")")
//r = --r < 1 ? i : r;
// e(u).css("background-image", "url(" + a + 73 + "." + c + ")")
}else if (f - q < -0.1) {
f = q;
a="left-top/";
i=43;
r = ++r > i ? 1 : r;
e(u).css("background-image", "url(" + a + r + "." + c + ")")
}
if (o - t > 0.1) {
o = t;
r = --r < 1 ? i : r;
e(u).css("background-image", "url(" + a + r + "." + c + ")")
} else if (o - t < -0.1) {
o = t;
r = ++r > i ? 1 : r;
e(u).css("background-image", "url(" + a + r + "." + c + ")")
}
}
Where : a is path of images folder, r is number of images(1,2,3,4....) and c is .png file
But it is not working perfectly so can Anyone help me...

I think u r pointing out the glitchy movement... U just have to add more images with more perspective

This is one way of doing it by creating a function that converts a view into a Image url. The view has the raw viewing angles and knows nothing about the image URL format or limits. The function createImageURL converts the view to the image URL and applies limits to the view if needed.
An animation function uses the mouse movement to update the view which then calls the URL function to get the current URL. I leave it to you to do the preloading, T
So first Create the vars to hold the current view
const view = {
rotUp : 0,
rotLeftRigh : 0,
speedX : 0.1, // converts from pixels to deg. can reverse with neg val
speedY : 0.1, // converts from pixels to deg
};
Create a function that will take the deg rotate (left right) and the deg rotate up (down) and convert it to the correct image URL.
// returns the url for the image to fit view
function createImageURL(view){
var rotate = view.rotLeftRight;
var rotateUp = view.rotUp;
const rSteps = 24; // number of rotate images
const rStepStringWidth = 3; // width of rotate image index
const upStep = 5; // deg step of rotate up
const maxUp = 90; // max up angle
const minUp = 0; // min up angle
const rotateUpToken = "#UP#"; // token to replace in URL for rotate up
const rotateToken = "#ROT#"; // token to replace in URL for rotate
// URL has token (above two lines) that will be replaced by this function
const url = "http://www.ajax-zoom.com/pic/zoomthumb/N/i/Nike_Air_#UP#_#ROT#_720x480.jpg";
// make rotate fit 0-360 range
rotate = ((rotate % 360) + 360) % 360);
rotate /= 360; // normalize
rotate *= rSteps; // adjust for number of rotation images.
rotate = Math.floor(rotate); // round off value
rotate += 1; // adjust for start index
rotate = "" + rotate; // convert to string
// pad with leading zeros
while(rotate.length < rStepStringWidth) {rotate = "0" + rotate }
// set min max of rotate up;
rotateUp = rotateUp < upMin ? upMin : rotateUp > upMax ? upMax : rotateUp;
view.rotUp = rotateUp; // need to set the view or the movement will
// get stuck at top or bottom
// move rotate up to the nearest valid value
rotateUp = Math.round(rotateUp / upStep) * upStep;
// set min max of rotate again as the rounding may bring it outside
// the min max range;
rotateUp = rotateUp < upMin ? upMin : rotateUp > upMax ? upMax : rotateUp;
url = url.replace(rotateUpToken,rotateUP);
url = url.replace(rotateToken,rotate);
return url;
}
Then in the mouse event you capture the movement of the mouse.
const mouse = {x : 0, y : 0, dx : 0, dy : 0, button : false}
function mouseEvents(e){
mouse.x = e.pageX;
mouse.y = e.pageY;
// as we dont process the mouse events here the movements must be cumulative
mouse.dx += e.movementX;
mouse.dY += e.movementY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
And then finally the animation function.
function update(){
// if there is movement
if(mouse.dx !== 0 || mouse.dy !== 0){
view.rotUp += mouse.dy * view.speedY;
view.rotLeftRight += mouse.dx * view.speedX;
mouse.dx = mouse.dy = 0;
// get the URL
const url = createImageURL(view);
// use that to load or find the image and then display
// it if loaded.
}
requestAnimationFrame(update);
}
requestAnimationFrame(update);
he createImageURL could also be used to create a referance to an image in an object.
const URLPart = "http://www.ajax-zoom.com/pic/zoomthumb/N/i/Nike_Air_"
const allImages = {
I_90_001 : (()=>{const i=new Image; i.src=URLPart+"_90_001_720x480.jpg"; return i;})(),
I_90_002 : (()=>{const i=new Image; i.src=URLPart+"_90_002_720x480.jpg"; return i;})(),
I_90_003 : (()=>{const i=new Image; i.src=URLPart+"_90_003_720x480.jpg"; return i;})(),
... and so on Or better yet automate it.
And in the createImageURL use the URL to get the property name for allImages
replacing
const url = "http://www.ajax-zoom.com/pic/zoomthumb/N/i/Nike_Air_#UP#_#ROT#_720x480.jpg";
with
const url = "I_#UP#_#ROT#";
then you can get the image
const currentImage = allImages[createImageURL(view)];
if(currentImage.complete){ // if loaded then
ctx.drawImage(currentImage,0,0); // draw it
}

Related

Fabric js: when button is clicked center canvas and zoom to object

I'm trying to center the viewport on an object and zoom in/out when i click a button. I want to have an animation. To do this I'm using setInterval and moving the viewport in increments like so:
const objectCenterCoordenates = {
x: Math.round(rect1.left + rect1.width / 2),
y: Math.round(rect1.top + rect1.height / 2)
};
const centeredCanvasCoordenates = {
x: objectCenterCoordenates.x - canvas.width / 2,
y: objectCenterCoordenates.y - canvas.height / 2
};
let currentPoint = {
x: Math.round(canvas.viewportTransform[4]) * -1,
y: Math.round(canvas.viewportTransform[5]) * -1
};
console.log("Start animation");
let animation = setInterval(() => {
console.log("New frame");
if (canvas.getZoom() !== 2) {
let roundedZoom = Math.round(canvas.getZoom() * 100) / 100;
let zoomStep = roundedZoom > 2 ? -0.01 : 0.01;
let newZoom = roundedZoom + zoomStep;
canvas.zoomToPoint(
new fabric.Point(currentPoint.x, currentPoint.y),
newZoom
);
}
let step = 5;
let vpCenter = {
x: Math.round(canvas.getVpCenter().x),
y: Math.round(canvas.getVpCenter().y)
};
if (
vpCenter.x === objectCenterCoordenates.x &&
vpCenter.y === objectCenterCoordenates.y
) {
console.log("Animation Finish");
clearInterval(animation);
}
let xDif = Math.abs(vpCenter.x - objectCenterCoordenates.x);
let yDif = Math.abs(vpCenter.y - objectCenterCoordenates.y);
let stepX =
Math.round(vpCenter.x) > Math.round(objectCenterCoordenates.x)
? -step
: step;
if (Math.abs(xDif) < step)
stepX =
Math.round(vpCenter.x) > Math.round(objectCenterCoordenates.x)
? -xDif
: xDif;
let stepY =
Math.round(vpCenter.y) > Math.round(objectCenterCoordenates.y)
? -step
: step;
if (Math.abs(yDif) < step)
stepY =
Math.round(vpCenter.y) > Math.round(objectCenterCoordenates.y)
? -yDif
: yDif;
currentPoint = {
x: currentPoint.x + stepX,
y: currentPoint.y + stepY
};
canvas.absolutePan(new fabric.Point(currentPoint.x, currentPoint.y));
}, 4);
But sometimes, I haven't been able to determine why, it gets stuck in a loop moving a few pixels one way then going back the other. And the zoom is well implemented, since it's possible to get to the object before it finished zooming in/out.
Here is a codepen with my code: https://codepen.io/nilsilva/pen/vYpPrgq
I would appreciate any advice on making my algorithm better.
Looks like you have an exact match requirement for your clearInterval ...
if (
vpCenter.x === objectCenterCoordenates.x &&
vpCenter.y === objectCenterCoordenates.y
) {
console.log("Animation Finish");
clearInterval(animation);
}
The case that you describe stuck in a loop moving one way then going back the other is because the exact match it's never done, could be because the step you are taking or it could be a rounding issue
You can use Math.hypot to check if the two points are close enough, read more here:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/hypot
The condition will look like:
if (Math.hypot(
vpCenter.x - objectCenterCoordenates.x,
vpCenter.y - objectCenterCoordenates.y
) < step) {
console.log("Animation Finish");
clearInterval(animation);
}

Make Chart.js Radar labels clickable

Has anyone managed to make the labels around the Chart.js Radar perimeter clickable?
There doesn't seem to be an immediately obvious solution.
I came up with a solution for this for version 2.8.0 by copying the label position calculations from the RadialLinear scale into an event handler.
document.getElementById("myChart").onclick = function (e) {
var helpers = Chart.helpers;
var scale = myRadarChart.scale;
var opts = scale.options;
var tickOpts = opts.ticks;
// Position of click relative to canvas.
var mouseX = e.offsetX;
var mouseY = e.offsetY;
var labelPadding = 5; // number pixels to expand label bounding box by
// get the label render position
// calcs taken from drawPointLabels() in scale.radialLinear.js
var tickBackdropHeight = (tickOpts.display && opts.display) ?
helpers.valueOrDefault(tickOpts.fontSize, Chart.defaults.global.defaultFontSize)
+ 5: 0;
var outerDistance = scale.getDistanceFromCenterForValue(opts.ticks.reverse ? scale.min : scale.max);
for (var i = 0; i < scale.pointLabels.length; i++) {
// Extra spacing for top value due to axis labels
var extra = (i === 0 ? tickBackdropHeight / 2 : 0);
var pointLabelPosition = scale.getPointPosition(i, outerDistance + extra + 5);
// get label size info.
// TODO fix width=0 calc in Brave?
// https://github.com/brave/brave-browser/issues/1738
var plSize = scale._pointLabelSizes[i];
// get label textAlign info
var angleRadians = scale.getIndexAngle(i);
var angle = helpers.toDegrees(angleRadians);
var textAlign = 'right';
if (angle == 0 || angle == 180) {
textAlign = 'center';
} else if (angle < 180) {
textAlign = 'left';
}
// get label vertical offset info
// also from drawPointLabels() calcs
var verticalTextOffset = 0;
if (angle === 90 || angle === 270) {
verticalTextOffset = plSize.h / 2;
} else if (angle > 270 || angle < 90) {
verticalTextOffset = plSize.h;
}
// Calculate bounding box based on textAlign
var labelTop = pointLabelPosition.y - verticalTextOffset - labelPadding;
var labelHeight = 2*labelPadding + plSize.h;
var labelBottom = labelTop + labelHeight;
var labelWidth = plSize.w + 2*labelPadding;
var labelLeft;
switch (textAlign) {
case 'center':
var labelLeft = pointLabelPosition.x - labelWidth/2;
break;
case 'left':
var labelLeft = pointLabelPosition.x - labelPadding;
break;
case 'right':
var labelLeft = pointLabelPosition.x - labelWidth + labelPadding;
break;
default:
console.log('ERROR: unknown textAlign '+textAlign);
}
var labelRight = labelLeft + labelWidth;
// Render a rectangle for testing purposes
ctx.save();
ctx.strokeStyle = 'red';
ctx.lineWidth = 1;
ctx.strokeRect(labelLeft, labelTop, labelWidth, labelHeight);
ctx.restore();
// compare to the current click
if (mouseX >= labelLeft && mouseX <= labelRight && mouseY <= labelBottom && mouseY >= labelTop) {
alert(scale.pointLabels[i]+' clicked');
// Break loop to prevent multiple clicks, if they overlap we take the first one.
break;
}
}
};
JSFiddle here:
https://jsfiddle.net/simoncoggins/7r08uLk9/
The downside of this approach is it that it will break if the core labelling implementation changes in the future. It would be better if the library separated the calculation of label position from its rendering and started exposing the position info via the API. Then this solution could be greatly simplified and would be more robust to library changes.
I've opened a ticket offering to make that change here:
https://github.com/chartjs/Chart.js/issues/6549
Please comment on that issue if it would be useful to you.
Thanks Pogrindis. Answer here works for Chart.js v2.1: Chart.js click on labels, using bar chart
To make that work for Chart.js v5.0+, add the following function back into the Chart.js code.
LinearRadialScale = Chart.LinearScaleBase.extend({...})
getValueCount: function() {
return this.chart.data.labels.length;
}

Algorithm - locating enough space to draw a rectangle given the x and y axis of all other rectangles

Every rectangle has x and y coordinates, width and height.
The total width of the screen is maxWidth and total height is maxHeight.
I have an array containing all the already drawn rectangles.
I am working on an web App where users will be drawing rectangles on the screen using their mouse. For that I am using Javascript to draw on the canvas element.
The challenge is that the rectangles must not intersect at any given point.
I am trying to avoid this kind of case:
or this:
This is how the output I am aiming for should look like:
What I basically need is an Algorithm (preferably in JavaScript) that can help locating enough space to draw a rectangle knowing its axis, height and width.
BM67 Box packing.
This is a method I use to pack rectangles. I made it up myself to create sprite sheets.
How it works.
You maintain two arrays, one holds rectangles of available spaces (space array), and the other rectangles you have placed.
You start by adding to the space array a rectangle that covers the whole area to be filled. This rectangle represents available space.
When you add a rectangle to fit you search the available space rectangles for a rectangle that will fit the new rectangle. If you can not find a rectangle that is bigger or sane size as the one you want to add there is no room.
Once you have found a place to put the rectangle, check all the available space rectangles to see if any of them overlap the new added rectangle. If any overlap you slice it up along the top, bottom, left and right, resulting in up to 4 new space rectangles. There are some optimisation when you do this to keep the number of rectangles down but it will work without the optimisations.
It's not that complicated and reasonably efficient compared to some other methods. It is particularly good when the space starts to run low.
Example
Below is a demo of it filling the canvas with random rectangles. It's on a animation loop to show the process, so is very much slowed down.
Gray boxes are the ones to fit. Red show the current spacer boxes. Each box has a 2 pixel margin. See top of code for demo constants.
Click the canvas to restart.
const boxes = []; // added boxes
const spaceBoxes = []; // free space boxes
const space = 2; // space between boxes
const minW = 4; // min width and height of boxes
const minH = 4;
const maxS = 50; // max width and height
// Demo only
const addCount = 2; // number to add per render cycle
const ctx = canvas.getContext("2d");
canvas.width = canvas.height = 1024;
// create a random integer
const randI = (min, max = min + (min = 0)) => (Math.random() * (max - min) + min) | 0;
// itterates an array
const eachOf = (array, cb) => { var i = 0; const len = array.length; while (i < len && cb(array[i], i++, len) !== true ); };
// resets boxes
function start(){
boxes.length = 0;
spaceBoxes.length = 0;
spaceBoxes.push({
x : space, y : space,
w : canvas.width - space * 2,
h : canvas.height - space * 2,
});
}
// creates a random box without a position
function createBox(){
return { w : randI(minW,maxS), h : randI(minH,maxS) }
}
// cuts box to make space for cutter (cutter is a box)
function cutBox(box,cutter){
var b = [];
// cut left
if(cutter.x - box.x - space > minW){
b.push({
x : box.x, y : box.y,
w : cutter.x - box.x - space,
h : box.h,
})
}
// cut top
if(cutter.y - box.y - space > minH){
b.push({
x : box.x, y : box.y,
w : box.w,
h : cutter.y - box.y - space,
})
}
// cut right
if((box.x + box.w) - (cutter.x + cutter.w + space) > space + minW){
b.push({
x : cutter.x + cutter.w + space,
y : box.y,
w : (box.x + box.w) - (cutter.x + cutter.w + space),
h : box.h,
})
}
// cut bottom
if((box.y + box.h) - (cutter.y + cutter.h + space) > space + minH){
b.push({
x : box.x,
y : cutter.y + cutter.h + space,
w : box.w,
h : (box.y + box.h) - (cutter.y + cutter.h + space),
})
}
return b;
}
// get the index of the spacer box that is closest in size to box
function findBestFitBox(box){
var smallest = Infinity;
var boxFound;
eachOf(spaceBoxes,(sbox,index)=>{
if(sbox.w >= box.w && sbox.h >= box.h){
var area = sbox.w * sbox.h;
if(area < smallest){
smallest = area;
boxFound = index;
}
}
})
return boxFound;
}
// returns an array of boxes that are touching box
// removes the boxes from the spacer array
function getTouching(box){
var b = [];
for(var i = 0; i < spaceBoxes.length; i++){
var sbox = spaceBoxes[i];
if(!(sbox.x > box.x + box.w + space || sbox.x + sbox.w < box.x - space ||
sbox.y > box.y + box.h + space || sbox.y + sbox.h < box.y - space )){
b.push(spaceBoxes.splice(i--,1)[0])
}
}
return b;
}
// Adds a space box to the spacer array.
// Check if it is insid, too small, or can be joined to another befor adding.
// will not add if not needed.
function addSpacerBox(box){
var dontAdd = false;
// is to small?
if(box.w < minW || box.h < minH){ return }
// is same or inside another
eachOf(spaceBoxes,sbox=>{
if(box.x >= sbox.x && box.x + box.w <= sbox.x + sbox.w &&
box.y >= sbox.y && box.y + box.h <= sbox.y + sbox.h ){
dontAdd = true;
return true;
}
})
if(!dontAdd){
var join = false;
// check if it can be joinded with another
eachOf(spaceBoxes,sbox=>{
if(box.x === sbox.x && box.w === sbox.w &&
!(box.y > sbox.y + sbox.h || box.y + box.h < sbox.y)){
join = true;
var y = Math.min(sbox.y,box.y);
var h = Math.max(sbox.y + sbox.h,box.y + box.h);
sbox.y = y;
sbox.h = h-y;
return true;
}
if(box.y === sbox.y && box.h === sbox.h &&
!(box.x > sbox.x + sbox.w || box.x + box.w < sbox.x)){
join = true;
var x = Math.min(sbox.x,box.x);
var w = Math.max(sbox.x + sbox.w,box.x + box.w);
sbox.x = x;
sbox.w = w-x;
return true;
}
})
if(!join){ spaceBoxes.push(box) }// add to spacer array
}
}
// Adds a box by finding a space to fit.
function locateSpace(box){
if(boxes.length === 0){ // first box can go in top left
box.x = space;
box.y = space;
boxes.push(box);
var sb = spaceBoxes.pop();
spaceBoxes.push(...cutBox(sb,box));
}else{
var bf = findBestFitBox(box); // get the best fit space
if(bf !== undefined){
var sb = spaceBoxes.splice(bf,1)[0]; // remove the best fit spacer
box.x = sb.x; // use it to position the box
box.y = sb.y;
spaceBoxes.push(...cutBox(sb,box)); // slice the spacer box and add slices back to spacer array
boxes.push(box); // add the box
var tb = getTouching(box); // find all touching spacer boxes
while(tb.length > 0){ // and slice them if needed
eachOf(cutBox(tb.pop(),box),b => addSpacerBox(b));
}
}
}
}
// draws a box array
function drawBoxes(list,col,col1){
eachOf(list,box=>{
if(col1){
ctx.fillStyle = col1;
ctx.fillRect(box.x+ 1,box.y+1,box.w-2,box.h - 2);
}
ctx.fillStyle = col;
ctx.fillRect(box.x,box.y,box.w,1);
ctx.fillRect(box.x,box.y,1,box.h);
ctx.fillRect(box.x+box.w-1,box.y,1,box.h);
ctx.fillRect(box.x,box.y+ box.h-1,box.w,1);
})
}
// Show the process in action
ctx.clearRect(0,0,canvas.width,canvas.height);
var count = 0;
var handle = setTimeout(doIt,10);
start()
function doIt(){
ctx.clearRect(0,0,canvas.width,canvas.height);
for(var i = 0; i < addCount; i++){
var box = createBox();
locateSpace(box);
}
drawBoxes(boxes,"black","#CCC");
drawBoxes(spaceBoxes,"red");
if(count < 1214 && spaceBoxes.length > 0){
count += 1;
handle = setTimeout(doIt,10);
}
}
canvas.onclick = function(){
clearTimeout(handle);
start();
handle = setTimeout(doIt,10);
count = 0;
}
canvas { border : 2px solid black; }
<canvas id="canvas"></canvas>
Update
Improving on the above algorithm.
Turned algorithm into an object
Improved speed by finding better fitting spacer via weighting the fit on the aspect ratio
Added placeBox(box) function that adds a box without checking if it fits. It will be placed at its box.x, box.y coordinates
See code example below on usage.
Example
The example is the same as the above example but have added randomly place boxes before fitting boxes.
Demo displays the boxes and spacer boxes as it goes to show how it works. Click the canvas to restart. Hold [shift] key and click canvas to restart without displaying intermediate results.
Pre placed boxes are blue.
Fitted boxes are gray.
Spacing boxes are red and will overlap.
When holding shift the fitting process is stopped at the first box tat does not fit. The red boxes will show area that are available but unused.
When showing progress the function will keep adding boxes ignoring non fitting boxes until out of room.
const minW = 4; // min width and height of boxes
const minH = 4;
const maxS = 50; // max width and height
const space = 2;
const numberBoxesToPlace = 20; // number of boxes to place befor fitting
const fixedBoxColor = "blue";
// Demo only
const addCount = 2; // number to add per render cycle
const ctx = canvas.getContext("2d");
canvas.width = canvas.height = 1024;
// create a random integer randI(n) return random val 0-n randI(n,m) returns random int n-m, and iterator that can break
const randI = (min, max = min + (min = 0)) => (Math.random() * (max - min) + min) | 0;
const eachOf = (array, cb) => { var i = 0; const len = array.length; while (i < len && cb(array[i], i++, len) !== true ); };
// creates a random box. If place is true the box also gets a x,y position and is flaged as fixed
function createBox(place){
if(place){
const box = {
w : randI(minW*4,maxS*4),
h : randI(minH*4,maxS*4),
fixed : true,
}
box.x = randI(space, canvas.width - box.w - space * 2);
box.y = randI(space, canvas.height - box.h - space * 2);
return box;
}
return {
w : randI(minW,maxS),
h : randI(minH,maxS),
}
}
//======================================================================
// BoxArea object using BM67 box packing algorithum
// https://stackoverflow.com/questions/45681299/algorithm-locating-enough-space-to-draw-a-rectangle-given-the-x-and-y-axis-of
// Please leave this and the above two lines with any copies of this code.
//======================================================================
//
// usage
// var area = new BoxArea({
// x: ?, // x,y,width height of area
// y: ?,
// width: ?,
// height : ?.
// space : ?, // optional default = 1 sets the spacing between boxes
// minW : ?, // optional default = 0 sets the in width of expected box. Note this is for optimisation you can add smaller but it may fail
// minH : ?, // optional default = 0 sets the in height of expected box. Note this is for optimisation you can add smaller but it may fail
// });
//
// Add a box at a location. Not checked for fit or overlap
// area.placeBox({x : 100, y : 100, w ; 100, h :100});
//
// Tries to fit a box. If the box does not fit returns false
// if(area.fitBox({x : 100, y : 100, w ; 100, h :100})){ // box added
//
// Resets the BoxArea removing all boxes
// area.reset()
//
// To check if the area is full
// area.isFull(); // returns true if there is no room of any more boxes.
//
// You can check if a box can fit at a specific location with
// area.isBoxTouching({x : 100, y : 100, w ; 100, h :100}, area.boxes)){ // box is touching another box
//
// To get a list of spacer boxes. Note this is a copy of the array, changing it will not effect the functionality of BoxArea
// const spacerArray = area.getSpacers();
//
// Use it to get the max min box size that will fit
//
// const maxWidthThatFits = spacerArray.sort((a,b) => b.w - a.w)[0];
// const minHeightThatFits = spacerArray.sort((a,b) => a.h - b.h)[0];
// const minAreaThatFits = spacerArray.sort((a,b) => (a.w * a.h) - (b.w * b.h))[0];
//
// The following properties are available
// area.boxes // an array of boxes that have been added
// x,y,width,height // the area that boxes are fitted to
const BoxArea = (()=>{
const defaultSettings = {
minW : 0, // min expected size of a box
minH : 0,
space : 1, // spacing between boxes
};
const eachOf = (array, cb) => { var i = 0; const len = array.length; while (i < len && cb(array[i], i++, len) !== true ); };
function BoxArea(settings){
settings = Object.assign({},defaultSettings,settings);
this.width = settings.width;
this.height = settings.height;
this.x = settings.x;
this.y = settings.y;
const space = settings.space;
const minW = settings.minW;
const minH = settings.minH;
const boxes = []; // added boxes
const spaceBoxes = [];
this.boxes = boxes;
// cuts box to make space for cutter (cutter is a box)
function cutBox(box,cutter){
var b = [];
// cut left
if(cutter.x - box.x - space >= minW){
b.push({
x : box.x, y : box.y, h : box.h,
w : cutter.x - box.x - space,
});
}
// cut top
if(cutter.y - box.y - space >= minH){
b.push({
x : box.x, y : box.y, w : box.w,
h : cutter.y - box.y - space,
});
}
// cut right
if((box.x + box.w) - (cutter.x + cutter.w + space) >= space + minW){
b.push({
y : box.y, h : box.h,
x : cutter.x + cutter.w + space,
w : (box.x + box.w) - (cutter.x + cutter.w + space),
});
}
// cut bottom
if((box.y + box.h) - (cutter.y + cutter.h + space) >= space + minH){
b.push({
w : box.w, x : box.x,
y : cutter.y + cutter.h + space,
h : (box.y + box.h) - (cutter.y + cutter.h + space),
});
}
return b;
}
// get the index of the spacer box that is closest in size and aspect to box
function findBestFitBox(box, array = spaceBoxes){
var smallest = Infinity;
var boxFound;
var aspect = box.w / box.h;
eachOf(array, (sbox, index) => {
if(sbox.w >= box.w && sbox.h >= box.h){
var area = ( sbox.w * sbox.h) * (1 + Math.abs(aspect - (sbox.w / sbox.h)));
if(area < smallest){
smallest = area;
boxFound = index;
}
}
})
return boxFound;
}
// Exposed helper function
// returns true if box is touching any boxes in array
// else return false
this.isBoxTouching = function(box, array = []){
for(var i = 0; i < array.length; i++){
var sbox = array[i];
if(!(sbox.x > box.x + box.w + space || sbox.x + sbox.w < box.x - space ||
sbox.y > box.y + box.h + space || sbox.y + sbox.h < box.y - space )){
return true;
}
}
return false;
}
// returns an array of boxes that are touching box
// removes the boxes from the array
function getTouching(box, array = spaceBoxes){
var boxes = [];
for(var i = 0; i < array.length; i++){
var sbox = array[i];
if(!(sbox.x > box.x + box.w + space || sbox.x + sbox.w < box.x - space ||
sbox.y > box.y + box.h + space || sbox.y + sbox.h < box.y - space )){
boxes.push(array.splice(i--,1)[0])
}
}
return boxes;
}
// Adds a space box to the spacer array.
// Check if it is inside, too small, or can be joined to another befor adding.
// will not add if not needed.
function addSpacerBox(box, array = spaceBoxes){
var dontAdd = false;
// is box to0 small?
if(box.w < minW || box.h < minH){ return }
// is box same or inside another box
eachOf(array, sbox => {
if(box.x >= sbox.x && box.x + box.w <= sbox.x + sbox.w &&
box.y >= sbox.y && box.y + box.h <= sbox.y + sbox.h ){
dontAdd = true;
return true; // exits eachOf (like a break statement);
}
})
if(!dontAdd){
var join = false;
// check if it can be joined with another
eachOf(array, sbox => {
if(box.x === sbox.x && box.w === sbox.w &&
!(box.y > sbox.y + sbox.h || box.y + box.h < sbox.y)){
join = true;
var y = Math.min(sbox.y,box.y);
var h = Math.max(sbox.y + sbox.h,box.y + box.h);
sbox.y = y;
sbox.h = h-y;
return true; // exits eachOf (like a break statement);
}
if(box.y === sbox.y && box.h === sbox.h &&
!(box.x > sbox.x + sbox.w || box.x + box.w < sbox.x)){
join = true;
var x = Math.min(sbox.x,box.x);
var w = Math.max(sbox.x + sbox.w,box.x + box.w);
sbox.x = x;
sbox.w = w-x;
return true; // exits eachOf (like a break statement);
}
})
if(!join){ array.push(box) }// add to spacer array
}
}
// Adds a box by finding a space to fit.
// returns true if the box has been added
// returns false if there was no room.
this.fitBox = function(box){
if(boxes.length === 0){ // first box can go in top left
box.x = space;
box.y = space;
boxes.push(box);
var sb = spaceBoxes.pop();
spaceBoxes.push(...cutBox(sb,box));
}else{
var bf = findBestFitBox(box); // get the best fit space
if(bf !== undefined){
var sb = spaceBoxes.splice(bf,1)[0]; // remove the best fit spacer
box.x = sb.x; // use it to position the box
box.y = sb.y;
spaceBoxes.push(...cutBox(sb,box)); // slice the spacer box and add slices back to spacer array
boxes.push(box); // add the box
var tb = getTouching(box); // find all touching spacer boxes
while(tb.length > 0){ // and slice them if needed
eachOf(cutBox(tb.pop(),box),b => addSpacerBox(b));
}
} else {
return false;
}
}
return true;
}
// Adds a box at location box.x, box.y
// does not check if it can fit or for overlap.
this.placeBox = function(box){
boxes.push(box); // add the box
var tb = getTouching(box); // find all touching spacer boxes
while(tb.length > 0){ // and slice them if needed
eachOf(cutBox(tb.pop(),box),b => addSpacerBox(b));
}
}
// returns a copy of the spacer array
this.getSpacers = function(){
return [...spaceBoxes];
}
this.isFull = function(){
return spaceBoxes.length === 0;
}
// resets boxes
this.reset = function(){
boxes.length = 0;
spaceBoxes.length = 0;
spaceBoxes.push({
x : this.x + space, y : this.y + space,
w : this.width - space * 2,
h : this.height - space * 2,
});
}
this.reset();
}
return BoxArea;
})();
// draws a box array
function drawBoxes(list,col,col1){
eachOf(list,box=>{
if(col1){
ctx.fillStyle = box.fixed ? fixedBoxColor : col1;
ctx.fillRect(box.x+ 1,box.y+1,box.w-2,box.h - 2);
}
ctx.fillStyle = col;
ctx.fillRect(box.x,box.y,box.w,1);
ctx.fillRect(box.x,box.y,1,box.h);
ctx.fillRect(box.x+box.w-1,box.y,1,box.h);
ctx.fillRect(box.x,box.y+ box.h-1,box.w,1);
})
}
// Show the process in action
ctx.clearRect(0,0,canvas.width,canvas.height);
var count = 0;
var failedCount = 0;
var timeoutHandle;
var addQuick = false;
// create a new box area
const area = new BoxArea({x : 0, y : 0, width : canvas.width, height : canvas.height, space : space, minW : minW, minH : minH});
// fit boxes until a box cant fit or count over count limit
function doIt(){
ctx.clearRect(0,0,canvas.width,canvas.height);
if(addQuick){
while(area.fitBox(createBox()));
count = 2000;
}else{
for(var i = 0; i < addCount; i++){
if(!area.fitBox(createBox())){
failedCount += 1;
break;
}
}
}
drawBoxes(area.boxes,"black","#CCC");
drawBoxes(area.getSpacers(),"red");
if(count < 5214 && !area.isFull()){
count += 1;
timeoutHandle = setTimeout(doIt,10);
}
}
// resets the area places some fixed boxes and starts the fitting cycle.
function start(event){
clearTimeout(timeoutHandle);
area.reset();
failedCount = 0;
for(var i = 0; i < numberBoxesToPlace; i++){
var box = createBox(true); // create a fixed box
if(!area.isBoxTouching(box,area.boxes)){
area.placeBox(box);
}
}
if(event && event.shiftKey){
addQuick = true;
}else{
addQuick = false;
}
timeoutHandle = setTimeout(doIt,10);
count = 0;
}
canvas.onclick = start;
start();
body {font-family : arial;}
canvas { border : 2px solid black; }
.info {position: absolute; z-index : 200; top : 16px; left : 16px; background : rgba(255,255,255,0.75);}
<div class="info">Click canvas to reset. Shift click to add without showing progress.</div>
<canvas id="canvas"></canvas>
Try the following:
iterate through the existing rectangles from top to bottom, based on the top boundary of each existing rectangle
while proceeding in top-to-bottom order, maintain a list of "active rectangles":
adding each succeeding rectangle based on its top boundary as an active rectangle, and
removing active rectangles based on their bottom boundary
(you can do this efficiently by using a priority queue)
also keep track of the gaps between active rectangles:
adding an active rectangle will end all gaps that overlap it, and (assuming it doesn't overlap any existing rectangles) start a new gap on each side
removing an active rectangle will add a new gap (without ending any)
note that multiple active gaps may overlap each other -- you can't count on having exactly one gap between active rectangles!
Check your new rectangle (the one you want to place) against all gaps. Each gap is itself a rectangle; you can place your new rectangle if it fits entirely inside some gap.
This kind of method is called a sweep-line algorithm.
You may have to check whether your current point is inside the area of any of the current rectangles. You can use the following code to test that (stolen from here)
In the array you are having, store the rectangle details in the following way
var point = {x: 1, y: 2};
var rectangle = {x1: 0, x2: 10, y1: 1, y2: 7};
Following will be your function to test whether any given point is inside any given rectangle.
function isPointInsideRectangle(p, r) {
return
p.x > r.x1 &&
p.x < r.x2 &&
p.y > r.y1 &&
p.y < r.y2;
}
I am not sure how you are going to implement this -
On mouse down
Always during drawing (This may be too much of a work).
On mouse up (this will be my preference. You can cancel the drawing if the test did not pass, with possible explanation for the user somewhere in the canvas)
Hope this will get you starting.

Smooth character movement on an Isometric map

I have an Isometric engine that I am building:
http://jsfiddle.net/neuroflux/09h43kz7/1/
(Arrow keys to move).
I am updating the Engine.player.x and Engine.player.y to move the character, but (obviously) the player just "pops" from one tile to another.
I wondered if there was a way to make him "slide" from tile to tile?
Or better still, free movement...
I've been pulling my hair out trying.
Here's the relevant code:
var Engine = {
// canvas variables
canvas: null,
ctx: null,
// map
map: [
[2,2,2,2,2,2,2,2,2,2,2,2,2,2,2],
[2,1,1,1,1,1,1,1,1,1,1,1,1,1,2],
[2,1,1,0,0,1,1,1,1,0,0,0,1,1,2],
[2,2,1,0,0,0,0,1,0,0,0,0,1,1,2],
[2,2,1,1,1,1,0,0,0,0,1,0,0,1,2],
[2,2,2,2,2,1,0,0,0,1,0,0,0,1,2],
[2,2,1,1,1,1,0,0,0,0,0,0,0,1,2],
[2,1,1,0,1,0,0,0,0,1,1,0,0,1,2],
[2,1,0,0,0,0,0,1,0,0,0,0,0,1,2],
[2,1,0,0,0,0,0,0,0,0,1,0,0,1,2],
[2,1,0,0,0,0,1,0,0,0,0,0,0,1,2],
[2,1,0,1,1,0,0,0,0,1,0,0,0,1,2],
[2,1,0,0,0,0,0,0,0,0,1,0,1,1,2],
[2,1,1,1,1,1,1,1,1,1,1,1,1,1,2],
[2,2,2,2,2,2,2,2,2,2,2,2,2,2,2]
],
// player info
player: {
x:1,
y:1
},
// tile size
tileH: 31,
tileW: 63,
// map position
mapX: window.innerWidth/2,
mapY: window.innerHeight/3,
// tile images
tileSources: [
"images/stone.png",
"images/grass.png",
"images/water.png",
"images/ralph.png"
],
// for pre-loading
tileGraphics: [],
tilesLoaded: 0,
// image preloader
loadImages: function() {
for (var i = 0; i < Engine.tileSources.length; i++) {
Engine.tileGraphics[i] = new Image();
Engine.tileGraphics[i].src = Engine.tileSources[i];
Engine.tileGraphics[i].onload = function() {
Engine.tilesLoaded++;
if (Engine.tilesLoaded === Engine.tileSources.length) {
Engine.draw();
}
}
}
},
// update logic
update: function() {
Engine.draw();
},
// draw the scene
draw: function() {
Engine.ctx.clearRect(0, 0, Engine.canvas.width, Engine.canvas.height);
var drawTile;
for (var i = 0; i < Engine.map.length; i++) {
for (var j = 0; j < Engine.map[i].length; j++) {
drawTile = Engine.map[i][j];
Engine.ctx.drawImage(Engine.tileGraphics[drawTile], (i - j) * Engine.tileH + Engine.mapX, (i + j) * Engine.tileH / 2 + Engine.mapY);
if (Engine.player.x === i && Engine.player.y === j) {
Engine.ctx.drawImage(Engine.tileGraphics[3], (i - j) * Engine.tileH + Engine.mapX, (i + j) * Engine.tileH / 2 + Engine.mapY - Engine.tileH + 10);
}
}
}
Engine.gameLoop();
},
// game loop
gameLoop: function() {
Engine.gameTimer = setTimeout(function() {
requestAnimFrame(Engine.update, Engine.canvas);
}, 1);
},
// start
init: function() {
Engine.canvas = document.getElementById("main");
Engine.canvas.width = window.innerWidth;
Engine.canvas.height = window.innerHeight;
Engine.ctx = Engine.canvas.getContext("2d");
document.addEventListener("keyup", function(e) {
//console.log(e.keyCode);
switch(e.keyCode) {
case 38:
if (Engine.map[Engine.player.x-1][Engine.player.y] !== 2) {
Engine.player.x--;
}
break;
case 40:
if (Engine.map[Engine.player.x+1][Engine.player.y] !== 2) {
Engine.player.x++;
}
break;
case 39:
if (Engine.map[Engine.player.x][Engine.player.y-1] !== 2) {
Engine.player.y--;
}
break;
case 37:
if (Engine.map[Engine.player.x][Engine.player.y+1] !== 2) {
Engine.player.y++;
}
break;
}
});
Engine.loadImages();
}
}
// loaded
window.onload = function() {
Engine.init();
};
// request animation frame
window.requestAnimFrame = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function (callback, element){
fpsLoop = window.setTimeout(callback, 1000 / 60);
};
}());
Thanks in advance!
You are drawing the character at tile positions. What you want is to just add a second set of coordinates for the character representing its destination. To move smoothly you can set the characters position in fractions of a tile. Eg player.x = 2.5 the character is halfway between tiles 2 and 3.
Also you want to get rid of the messing about in isometric space. Off load the transformation from 2d to isometric to the draw function, rather than doing it by hand each time you draw to the playfield.
Create the draw function
// add to the Engine object.
// img is the image to draw. x and y are the tile locations.
// offsetX and offsetY [optional] are pixel offsets for fine tuning;
drawImageIso:function (img,x,y,offsetX,offsetY){
offsetX = offsetX === undefined ? 0: offsetX; // so you dont have to
offsetY = offsetY === undefined ? 0: offsetY; // add the offset if you
// are not using it;
Engine.ctx.drawImage( // draw the image
img,
(x - y) * Engine.tileH + Engine.mapX + offsetX,
(x + y) * Engine.tileH / 2 + Engine.mapY - Engine.tileH+offsetY
);
},
Change the player object to
player: {
x:1,
y:1,
destX:1, // the destination tile
destY:1,
playerAtDest:true, // true if the player has arrived
},
Add this befor the tile render loops
var p = Engine.player; // because I am lazy and dont like typing.
var dx = p.destX;
var dy = p.destY;
var maxPlayerSpeed = 0.1; // max speed in tiles per frame
var mps = maxPlayerSpeed; // because I am lazy
// check if the player needs to move
if( Math.abs(p.x - dx) > mps || Math.abs(p.y - dy) > mps ){
p.x += Math.max( -mps , Math.min( mps , dx - p.x )); // move to destination clamping speed;
p.y += Math.max( -mps , Math.min( mps , dy - p.y ));
p.playerAtDest = false; // flag the player is on the way
}else{
// player directly over a till and not moving;
p.x = dx; // ensure the player positioned correctly;
p.y = dy;
p.playerAtDest = true; // flag the player has arrived
}
Add the following where you used to draw the player. Use the destination x,y to determine when to draw or use Math.round(Engine.player.x) and y to determine when.
// now draw the player at its current position
Engine.drawImageIso( Engine.tileGraphics[3] , p.x , p.y , 0 , 10);
You will have to change the interface to move player destination rather than x and y. You may also want to delay the move until the player has arrived at the current destination.
That covers the basics.

How to detect mouseenter direction on a specific element

I currently have a function which detects the direction of the mouse enter. It returns an integer (0-3) for each section as illustrated below.
I'm trying to figure out how to alter this function to produce an output based on this illustration:
Basically, I just want to figure out if the user is entering the image from the left or the right. I've had a few attempts at trial and error, but I'm not very good at this type of math.
I've set up a JSFiddle with a console output as an example.
The function to determine direction is:
function determineDirection($el, pos){
var w = $el.width(),
h = $el.height(),
x = (pos.x - $el.offset().left - (w/2)) * (w > h ? (h/w) : 1),
y = (pos.y - $el.offset().top - (h/2)) * (h > w ? (w/h) : 1);
return Math.round((((Math.atan2(y,x) * (180/Math.PI)) + 180)) / 90 + 3) % 4;
}
Where pos is an object: {x: e.pageX, y: e.pageY}, and $el is the jQuery object containing the target element.
Replace return Math.round((((Math.atan2(y,x) * (180/Math.PI)) + 180)) / 90 + 3) % 4; with return (x > 0 ? 0 : 1);
Another option which I like better (simpler idea):
function determineDirection($el, pos){
var w = $el.width(),
middle = $el.offset().left + w/2;
return (pos.x > middle ? 0 : 1);
}

Categories