requesting Canvas Text and requestAnimationFrame assistance - javascript

I have been working on an issue for a few days now and have been unable to solve it. Please note that I am relatively new to Javascript so not sure if what I have below is the best way to accomplish this but going through this over the last few days has definitely helped me learn some new things.
Also, please note I know I could achieve this very easily using CSS but I wanted to know if there was a Javascript/JQuery solution.
The Issue:
I am attempting to simulate a fadeIn animation on a canvas for some text.
var introText =
{
"Welcome To A New Day...": "75",
"Full Service Web Design": "50",
"by": "50",
"J. David Hock": "50"
};
The numbers represent font size.
Here is the full code:
$(document).ready(function ()
{
var canvas = $('#myCanvas')[0];
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
var ctx = canvas.getContext('2d');
var introText =
{
"Welcome To A New Day...": "75",
"Full Service Web Design": "50",
"by": "50",
"J. David Hock": "50"
};
function fadeText(timeStamp, t, x, y)
{
var opacity = timeStamp / 1000;
console.log('Timestamp: ' + timeStamp + ' Opacity: ' + opacity);
console.log('t, x, y |' + t +' | ' + x + ' | ' + y)
//ctx.clearRect(0, 0, canvas.width, canvas.height);
//ctx.fillStyle = 'rgba(178, 34, 34, ' + opacity + ')';
//ctx.fillText(t, x, y);
if (opacity < 1)
{
requestAnimationFrame(function (timestamp)
{
fadeText(timestamp, t, x, y)
});
}
}
function MessageObject(x, y, f, fs, t)
{
this.x = x;
this.y = y;
this.f = f;
this.fs = fs;
this.t = t;
}
var msgArray = [];
function CreateMessageArray(myArray, callback)
{
var i = 0;
var v = 75;
var x = 0;
var y = 0;
var f = '';
var fs = '';
var t = '';
for (t in myArray)
{
fs = myArray[t]; //font size
f = 'italic ' + fs + 'px Bradley Hand'; //font type
x = (canvas.width / 2) //x pos of text
msgArray.push(new MessageObject(x, y, f, fs, t))
y = Number(fs);
//alert('x, y, f, t | ' + x + ' | ' + y + ' | ' + f + ' | ' + t);
}
return callback(msgArray);
}
let ProcessMessageArray = function (myArray)
{
var xPrime = 0;
var yPrime = 0;
for (i = 0; i < myArray.length; i++)
{
var msgObject = myArray[i];
var x = msgObject.x;
var y = msgObject.y;
var f = msgObject.f;
var fs = msgObject.fs;
var t = msgObject.t;
ctx.textBaseline = 'top';
ctx.font = f;
var txtWidth = ctx.measureText(t).width
xPrime = x - (txtWidth / 2);
requestAnimationFrame(function(timestamp)
{
fadeText(timestamp, t, x, y)
});
//ctx.fillStyle = 'rgba(178, 34, 34, 1)';
//ctx.fillText(t, xPrime, yPrime);
if (i === 0)
{
yPrime = Number(yPrime) + (2 * Number(fs) - 35);
}
else
{
yPrime = Number(yPrime) + Number(fs);
}
}
}
CreateMessageArray(introText, ProcessMessageArray)
});
The way it is supposed to work is that the CreateMessageArray function creates an array of objects that contain the x-pos, y-pos, etc. for each of the lines of text in the introTextArray.
The ProcessMessageArray is then responsible for outputting each line of text in the introTextArray into it proper position on the screen.
In the ProcessMessageArray there is a call to a requestAnimationFrame function where I was hoping it would "fade in" each line of the text but what is actually occurring is that I am only getting the last line of text in the IntroTextArray.
I am sure it has to do with the fact that I am calling a requestAnimationFrame within a loop but I am not sure how to accomplish what I want to otherwise. Any advice would be appreciated.

Animating with requestAnimationFrame
RAF requestAnimationFrame
There are many ways that you can use RAF to animate. You can have many RAF functions that will all present the content to the display at the same time if they are all called before the next refresh. But this does incur extra overhead (especially if the rendering load per item is small) and extra head pain. If you have a slow render (for whatever reason, browsers can hang for a 1/60th second without warning) it can mess up the presentation order and you will only get part of the frame rendered.
The easiest way to manage any type of animation is to have a single animation loop that is called via RAF. From that function you call your animations. This is safe from presentation order problems, and can easily switch state. (eg after text fade you may want something else fading in)
Fix
Your code was not able to be saved, and sorry, was DOA, so I rewrote the whole thing. I have added some notes in the comments as to why and what for but if you have questions please do ask in the comments.
The example displays the fading text as two sets (to illustrate changing display state, a state is an abstract and reference to related elements or render (eg intro, middle, end))
The function has one main loop that handles the rendering calls and the timing.
Text is displayed by a function that updates and then displays. it will return true when text has faded in so main loop can set up the next render state.
// Removed jQuery and I dont see the need to use it
// The convention in JavaScript is NOT to capitalize the first character
// unless you function / object is created using new ObjectName
const ctx = canvas.getContext("2d"); // canvas is named in HTML via its id
canvas.width = innerWidth; // window is the global scope you do not need
canvas.height = innerHeight; //to prefix when accesing its properties
// helper
//const log = (...data) => console.log(data.join(","));
// use an array it is better suited to the data you are storing
const introText = [
["Testing one two.", 75],
["Testing..." , 50],
["One, one two.", 50],
["Is this thing on?", 50],
["",1], // to delay next state
["",1], // to delay next state
];
const introText1 = [
["Second stage, state 2", 20],
["The End" , 40],
[":)", 30],
["",10],
["Thanks for watching..",12],
["",1],
["",1],
["Dont forget to vote for any answers you find helpfull..",10],
["",1],
["",10],
["This intro was brought to you by",12],
["",10],
["requestAnimationFrame",12],
["HTML5",12],
["canvas",12],
["JavaScript (AKA) ECMAScript6",12],
["",10],
["Staring...",14],
["Stackoverflow.com",18],
];
const TEXT_FADE_TIME = 1000; // in ms
// create the array of display arrays
const displayLists = [
createDisplayList(8, introText), // converts simple text array to display list
createDisplayList(8, introText1),
];
var curretDisplayListIdx = 0;
requestAnimationFrame(mainLoop); // will start when all code has been parsed
var startTime;
function mainLoop(time){ // keep it simple use one animation frame per frame
if(startTime === undefined) { startTime = time }
const timeSinceStart = time - startTime;
ctx.clearRect(0,0, canvas.width, canvas.height);
if (textFadeIn(timeSinceStart, curretDisplayListIdx )) {
if (curretDisplayListIdx < displayLists.length - 1) {
curretDisplayListIdx += 1;
startTime = time;
}
}
requestAnimationFrame(mainLoop);
}
// creates a display list from text array. top is the start y pos
function createDisplayList(top, array) {
const result = [];
var y = top;
var fontSize;
for (const item of array) {
const fontSize = item[1];
result.push({
font : 'italic ' + fontSize + 'px Bradley Hand',
text : item[0],
x : canvas.width / 2,
y , fontSize,
startTime : null,
fadeTime : TEXT_FADE_TIME,
alpha : 0, // starting alpha
});
y += fontSize;
}
return result;
}
// displays a text display list from the displayLists array.
// time is time since starting to display the list
// displayListIdx is the index
// returns true when text has faded in
function textFadeIn(time, displayListIdx) {
// complete will be true when all have faded in
const complete = updateDisplayList(displayLists[displayListIdx], time);
renderDisplayList(displayLists[displayListIdx]);
return complete;
}
// separate logic from rendering
function updateDisplayList(array, time) {
var fadeNext = true; // used to indicate that the next item should fade in
// for each item
for (const text of array) {
if (fadeNext) { // has the previous items done its thing?
if (text.startTime === null) { text.startTime = time }
}
if(text.startTime !== null){ // if start time is set then fade it in
text.alpha = Math.min(1, (time - text.startTime) / text.fadeTime);
if (text.alpha < 1) { fadeNext = false } // if not complete flag fadeNext to stop next text fading in
}
}
// if last item is fully faded in return true
return array[array.length - 1].alpha === 1;
}
// seperate rendering from logic
function renderDisplayList(array) {
ctx.textBaseline = "top";
ctx.textAlign = "center";
ctx.fillStyle = "black";
for (const text of array) {
ctx.font = text.font;
ctx.globalAlpha = text.alpha;
ctx.fillText(text.text,text.x,text.y);
}
}
canvas {
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>

Related

Javascript Garbage Collector not clearing objects

I've spent a few days searching through SO, googling, and reading articles, and I cannot for the life of me figure out how to avoid memory leaking. I wrote a quick demo to see what was going on here: https://codepen.io/dotjersh/pen/WMVwWx.
var SelectMap = function(canvas,onComplete){
var size = [3,3];
var s = 39;//sidelength
var p = 1; //padding
var color = ['#3D5AFE','#F57F17']
var ctx = canvas.getContext("2d");
var cursor = null;
canvas.width = size[0] * (s + p);
canvas.height = size[1] * (s + p);
canvas.addEventListener('mousemove',hover);
canvas.addEventListener('click',click);
render();
function click(e){
onComplete(Math.floor(cursor.x/(s + p)),Math.floor(cursor.y/(s + p)));
destroy();
}
function hover(e){
cursor = {x:Math.abs(e.clientX - canvas.offsetLeft),y:Math.abs(e.clientY - canvas.offsetTop)}
render();
}
function render(){
ctx.clearRect(0,0,canvas.width,canvas.height)
for(var x = 0; x < size[0]; x++){
for(var y = 0; y < size[1]; y++){
ctx.fillStyle = color[0];
if(cursor){
var xPoint = ((x*s) + (x*p));
var yPoint = ((y*s) + (y*p));
if(Math.floor(cursor.x/(s + p)) == x && Math.floor(cursor.y/(s + p)) == y){
ctx.fillStyle = color[1];
}
}
ctx.fillRect((x*s) + (x*p),(y*s) + (y*p),s,s);
}
}
}
function destroy(){
canvas.removeEventListener('mousemove',hover);
canvas.removeEventListener('click',click);
ctx.clearRect(0,0,canvas.width,canvas.height);
}
return{
destroy: destroy,
}
}
function go(){
var bob = new SelectMap(document.getElementById('canvas'),function(x,y){
alert(x + "," + y);
bob = null;
});
}
<canvas id="canvas"></canvas>
The intended result is that once you open the page, the base amount of Memory is stored. You can run go(), and see the memory increase. Once you click something, the object should remove itself from the global scope. On chrome I run the garbage collector, but afterward there is no change in the amount of memory used. It should return to the original memory should it not?
Some of the things I've done:
- Made sure all the events are removed
- set object to null
- cleared out the canvas
I've been trying to understand this for days, any help would be appreciated.
Credit to #JonasW.
He mentioned that garbage collectors only will collect data if there is data to collect, and they won't get kilobytes of data. I modified my codepen to created 25MB of useless data and it ended up working. The codepen saved ended up created kilobytes of data each type go() was run and then removed. Which was the intention, to get rid of that 25MB every time it ran.
Thanks!

Get the color of one pixel at Photoshop scripts

I am trying to figure out how to get the color of one defined Pixel.
In my imagination its shoud look like:
color = get.color.Pixel(x,y);
Maybe someone can help me with this piece of code?
Photoshop's JavaScript API doesn't provide a mechanism like you imagine in your question.
You'll need to utilize the Document.colorSamplers.add([x, y]) method, then read each component color value via its properties:
The following gist shows how to obtain either rgb or cmyk values for a given x,y coordinate:
#target photoshop
// Define the x and y coordinates for the pixel to sample.
var x = 1;
var y = 1;
// Add a Color Sampler at a given x and y coordinate in the image.
var pointSample = app.activeDocument.colorSamplers.add([(x - 1),(y - 1)]);
// Obtain array of RGB values.
var rgb = [
pointSample.color.rgb.red,
pointSample.color.rgb.green,
pointSample.color.rgb.blue
];
// Obtain array of rounded CMYK values.
var cmyk = [
Math.round(pointSample.color.cmyk.cyan),
Math.round(pointSample.color.cmyk.magenta),
Math.round(pointSample.color.cmyk.yellow),
Math.round(pointSample.color.cmyk.black)
];
// Remove the Color Sampler.
pointSample.remove();
// Display the complete RGB values and each component color.
alert('RGB: ' + rgb)
alert('red: ' + rgb[0])
alert('green: ' + rgb[1])
alert('blue: ' + rgb[2])
// Display the complete CMYK values and each component color.
alert('CMYK: ' + cmyk)
alert('cyan: ' + cmyk[0])
alert('magenta: ' + cmyk[1])
alert('yellow: ' + cmyk[2])
alert('black: ' + cmyk[3])
Here's a simple script using a ColorSampler. It's set up to return RGB values.
function PixelSampler(doc) {
this.doc = doc
this.doc.colorSamplers.removeAll();
this.sampler = this.doc.colorSamplers.add([0, 0]);
}
// Return an array of R, G, B pixel values for a particular coordinate.
PixelSampler.prototype.get = function (x, y) {
this.sampler.move([x, y]);
const R = this.sampler.color.rgb.red;
const G = this.sampler.color.rgb.green;
const B = this.sampler.color.rgb.blue;
return [R, G, B];
}
////////////////////////////////////////////////////////
/// SOME TESTS /////////////////////////////////////////
////////////////////////////////////////////////////////
const p = new PixelSampler(app.activeDocument);
alert("Pixel 0 =\n\n" + p.get(0, 0));
$.hiresTimer;
var n = 1000; //p.width * p.height;
for (var i = 0; i < n; i++) p.get(i, 0);
sec = ($.hiresTimer / 1000 / 1000);
alert("Got " + (n / 1000) + " kilopixels in " + sec.toFixed(2) + " seconds.");
This gives me pixel values at about 100 pixels per second on my machine.
I found this and cleaned up the script a bit. Basically, the idea is to:
Save the current image as a raw bitmap.
Read it back in, but on the javascript side.
Do all access to pixels on the javascript side.
This gives me pixel values at about 72,000 pixels per second, not including the overhead of writing the raw data to disk and reading it back in. It has the added benefit that you can set pixel values, too.
// Adapted from https://community.adobe.com/t5/photoshop/get-index-of-each-pixel/td-p/10022899?page=1
// The purpose is to query (and change) pixel values quickly.
//
// The secret to speed is doing everything on the script side rather than ask Photoshop to do things.
// We use files on disk as an intermediary; on the script side, we read / write it as a binary file; on the
// Photoshop side, we save / open it as a raw bitmap.
//
// Only works on RGB 8bpp images, but this could be easily extended to support others.
function RawPixels(doc) {
this.doc = doc;
const currentActiveDoc = app.activeDocument;
// Obtain the width and height in pixels of the desired document.
const currentRulerUnits = app.preferences.rulerUnits;
app.preferences.rulerUnits = Units.PIXELS;
app.activeDocument = doc;
this.width = Number(doc.width.value);
this.height = Number(doc.height.value);
this.length = this.width * this.height;
this.pixelData = "";
// Return the ruler to its previous state.
app.preferences.rulerUnits = currentRulerUnits;
try {
// We're going to save this document as a raw bitmap to be able to read back in the pixel values
// themselves.
const file = new File(Folder.temp.fsName + "/" + Math.random().toString().substr(2) + ".raw");
// Set up the save action.
// See https://helpx.adobe.com/photoshop/using/file-formats.html#photoshop_raw_format for some info,
// and more technical at https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/
var rawFormat = new ActionDescriptor();
rawFormat.putString(stringIDToTypeID("fileCreator"), "8BIM");
rawFormat.putBoolean(stringIDToTypeID("channelsInterleaved"), true);
var saveAction = new ActionDescriptor();
saveAction.putObject(stringIDToTypeID("as"), stringIDToTypeID("rawFormat"), rawFormat);
saveAction.putPath(stringIDToTypeID("in"), file);
saveAction.putBoolean(stringIDToTypeID("copy"), false);
executeAction(stringIDToTypeID("save"), saveAction, DialogModes.NO);
// File is saved; now read it back in as raw bytes.
file.open("r");
file.encoding = "BINARY";
this.pixelData = file.read();
const err = file.error;
file.close();
file.remove();
file = null;
if (err) alert(err);
}
catch (e) { alert(e); }
// Return focus to whatever the user had.
app.activeDocument = currentActiveDoc;
}
// Calculate offset from x, y coordinates. Does not check for valid bounds.
getOffset = function(x, y) {
if (y == undefined) {
// allow linear indices too
y = Math.floor(x / this.width);
x = x - y * this.width;
}
return (y * this.width + x) * 3;
}
// Return an array of R, G, B pixel values for a particular coordinate.
RawPixels.prototype.get = function (x, y) {
const off = getOffset(x, y);
const R = this.pixelData.charCodeAt(off + 0);
const G = this.pixelData.charCodeAt(off + 1);
const B = this.pixelData.charCodeAt(off + 2);
return [R, G, B];
}
// Set the pixel at x, y to the values in RGB.
RawPixels.prototype.set = function (RGB, x, y) {
const off = getOffset(x, y);
// note: note checking that length of p = 3!
const R = String.fromCharCode(RGB[0]);
const G = String.fromCharCode(RGB[1]);
const B = String.fromCharCode(RGB[2]);
this.pixelData = this.pixelData.substr(0, off) + R + G + B + this.pixelData.substr(off + 3);
}
// If any changes were made to the pixels, we need to save them to disk and have Photoshop read that file back in.
// We do that by creating a new layer in the desired document.
RawPixels.prototype.create_layer = function () {
try {
const file = new File(Folder.temp.fsName + "/" + Math.random().toString().substr(2) + ".raw");
file.open("w");
file.encoding = "BINARY";
file.write(this.pixelData);
const err = file.error;
file.close();
if (err) { file.remove(); alert(err); return; }
var rawFormat = new ActionDescriptor();
rawFormat.putInteger(stringIDToTypeID("width"), this.width);
rawFormat.putInteger(stringIDToTypeID("height"), this.height);
rawFormat.putInteger(stringIDToTypeID("channels"), 3);
rawFormat.putBoolean(stringIDToTypeID("channelsInterleaved"), true);
rawFormat.putInteger(stringIDToTypeID("depth"), 8);
var openAction = new ActionDescriptor();
openAction.putPath(stringIDToTypeID("null"), file);
openAction.putObject(stringIDToTypeID("as"), stringIDToTypeID("rawFormat"), rawFormat);
executeAction(stringIDToTypeID("open"), openAction, DialogModes.NO);
file.remove();
// The new active document is the file we just opened. Duplicate its contents into
// a new layer in our desired document, then close this temporary file.
app.activeDocument.activeLayer.duplicate(this.doc.layers[0], ElementPlacement.PLACEBEFORE);
const tempDoc = app.activeDocument;
app.activeDocument = this.doc;
this.doc.layers[0].name = "Pixels";
app.activeDocument = tempDoc;
app.activeDocument.close(SaveOptions.DONOTSAVECHANGES);
app.activeDocument = this.doc;
}
catch (e) { alert(e); }
}
////////////////////////////////////////////////////////
/// SOME TESTS /////////////////////////////////////////
////////////////////////////////////////////////////////
$.hiresTimer;
const p = new RawPixels(app.activeDocument);
var sec = ($.hiresTimer / 1000 / 1000);
alert("Init RawPixels in " + sec.toFixed(2) + " seconds");
alert("Pixel 0 =\n\n" + p.get(0));
var a = new Array();
for (var i = 0; i < 100; i++) a.push(p.get(i));
alert("Pixel 0-99 = \n\n" + a.toSource());
p.set(0, [1, 200, 3]);
alert("New Pixel 0=\n\n" + p.get(0));
$.hiresTimer;
var n = p.width * p.height;
for (var i = 0; i < n; i++) p.get(i);
sec = ($.hiresTimer / 1000 / 1000);
alert("Got " + (n / 1000 / 1000) + " megapixels in " + sec.toFixed(2) + " seconds.");
$.hiresTimer;
n = 10;
for (var i = 0; i < n; i++) p.set([255, i * 20, i * 10], 1 + i * 2);
sec = ($.hiresTimer / 1000 / 1000);
//alert("Set " + n + " pixels in " + sec.toFixed(2) + " seconds");
p.create_layer();
alert("New layer created with new pixels");

How to control requestAnimationFrame for waves speed

I have to create a animation for waves . I need to control the speed of the waves depends on the availability of the data. Is it possible to speed up the waves. I'm using canvas for the waves.
Thanks in advance
Fiddle:https://jsfiddle.net/Chaitanya_Kumar/6ztr0Lfh/
function animate() {
if (x > data.length - 1) {
return;
}
if (continueAnimation) {
requestAnimationFrame(animate);
}
if (x++ < panAtX) {
var temp = data[x];
var final = constant-(temp);
ctx.fillRect(x, final, 1, 1);
ctx.lineTo(x, final);
ctx.stroke();
} else {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath(); // reset the path
for (var xx = 0; xx < panAtX; xx++) {
var y = data[x - panAtX + xx];
var final = constant - (y);
ctx.fillRect(xx, final, 1, 1);
ctx.lineTo(xx, final);
}
ctx.stroke();
}
}
Sub sampling data
Below is an example of data sampling. It uses linear interpolation to subsample a data source and display that data on a rolling graph display.
Regularly interval data.
The data from your question and fiddle suggest that you have a constant sample rate interval and that you want to vary the display rate of that data. This is what I have done in the demo below.
About the demo
The graph is a real-time display of the data and its speed from left to right is dependent on the rate at which you call the sample function.
displayBuffer.readFrom(dataSource, dataSpeed, samplesPerFrame)
displayBuffer is the object that holds the displayable data
dataSource is the source of data and has a read and seek function and a readPos You seek to a position dataSource.seek(0.01); move ahead 0.01 data samples and then read the data dataSource.read(); and the linear interpolated value is returned.
This allows you to speed up or slow down data streaming from the source data.
The data reader object
//------------------------------------------------------------------------------
// data reader reads from a data source
const dataReader = {
readPos : 0,
seek(amount){ // moves read pos forward or back
if(this.data.length === 0){
this.readPos = 0;
return 0;
}
this.readPos += amount;
this.readPos = this.readPos < 0 ? 0 :this.readPos >= this.data.length ? this.data.length - 1 : this.readPos;
return this.readPos;
},
// this function reads the data at read pos. It is a linear interpolation of the
// data and does nor repressent what the actual data may be at fractional read positions
read(){
var fraction = this.readPos % 1;
var whole = Math.floor(this.readPos);
var v1 = this.data[Math.min(this.data.length-1,whole)];
var v2 = this.data[Math.min(this.data.length-1,whole + 1)];
return (v2 - v1) * fraction + v1;
},
}
Timestamped data source.
The demo can be adapted by adding to the dataReader.
If your data sample rate is irregular than you will need to add a timestamp for each sample. You then add a timeSeek function that is similare to seek but uses the slope between time samples to calculate the read position for a given time. It will require sampling of each sample from the current sampled time to the next (in the seek direction) making CPU cycles needed to seek indeterminant.
The following is an example seekTime that finds the readPos (from above dataReader object) for time shifted forward by the timeShift argument. the object's readTime and readPos properties are updated and the next read() call will return the data at dataSource.readTime.
readTime : 0, // current seeked time
seekTime(timeShift){ // Example is forward seek only
if(this.timeStamps.length === 0){
this.readPos = 0;
return 0;
}
this.readTime += timeShift; // set new seeked time
var readPos = Math.floor(this.readPos);
// move read pos forward until at correct sample
while(this.timeStamps[readPos] > this.readTime &&
readPos++ < this.timeStamps.length);
// Warning you could be past end of buffer
// you will need to check and set seek time to the last
// timestamp value and exit. Code below the following line
// will crash if you dont vet here.
//if(readPos === this.timeStamps.length)
// now readPos points to the first timeStamp less than the needed
// time position. The next read position should be a time ahead of the
// needed time
var t1 = this.timeStamps[readPos]; // time befor seekTime
var t2 = this.timeStamps[readPos+1]; // time after seekTime
// warning divide by zero if data bad
var fraction = (this.readTime-t1)/(t2-t1)); // get the sub sample fractional location for required time.
this.readPos = readPos + fraction;
return this.readPos;
},
Warning I have omitted all safety checks. You should check for buffer end, bad time shift values. If time stamped data has bad samples you will get a divide by zero that will make the dataReader return only NaN from that point on and throw for any reads. So vet for safety.
Note For the above time stamped function to work you will need to ensure that for each data sample there is a corresponding timeStamp. If there is not a one to one matching time stamp of each sample the above code will not work.
Changes to the dataDisplay are simple. Just change the seek call in the function
dataDisplay.readFrom(dataSource,speed,samples) to dataSource.seekTime(speed / samples) the speed now represents time rather than samples. (or I just overwrite the seek() function with seekTime() if I have time stamps) this allows the dataDisplay object to handle both timeStamped and regular interval data as is.
Demo
The example samples random data and displays it at variable speed and sampling rates. Use left right to set display speed. The framerate is 60fps thereabouts but you can make the speed variable scaled to the time between frames.
var ctx = canvas.getContext("2d");
window.focus();
//==============================================================================
// the current data read speed
var dataSpeed = 1;
var samplesPerFrame = 1;
requestAnimationFrame(mainLoop); // start animation when code has been parsed and executed
//------------------------------------------------------------------------------
// data reader reads from a data source
const dataReader = {
readPos : 0,
seek(amount){ // moves read pos forward or back
if(this.data.length === 0){
this.readPos = 0;
return 0;
}
this.readPos += amount;
this.readPos = this.readPos < 0 ? 0 :this.readPos >= this.data.length ? this.data.length - 1 : this.readPos;
return this.readPos;
},
// this function reads the data at read pos. It is a linear interpolation of the
// data and does nor repressent what the actual data may be at fractional read positions
read(){
var fraction = this.readPos % 1;
var whole = Math.floor(this.readPos);
var v1 = this.data[Math.min(this.data.length-1,whole)];
var v2 = this.data[Math.min(this.data.length-1,whole + 1)];
return (v2 - v1) * fraction + v1;
},
}
//------------------------------------------------------------------------------
// Create a data source and add a dataReader to it
const dataSource = Object.assign({
data : [],
},dataReader
);
// fill the data source with random data
for(let i = 0; i < 100000; i++ ){
// because random data looks the same if sampled every 1000 or 1 unit I have added
// two waves to the data that will show up when sampling at high rates
var wave = Math.sin(i / 10000) * 0.5;
wave += Math.sin(i / 1000) * 0.5;
// high frequency data shift
var smallWave = Math.sin(i / 100) * (canvas.height / 5);
// get a gaussian distributed random value
dataSource.data[i] = Math.floor(smallWave + ((wave + Math.random()+Math.random()+Math.random()+Math.random()+Math.random()) / 5) * canvas.height);
}
//------------------------------------------------------------------------------
// Data displayer used to display a data source
const dataDisplay = {
writePos : 0,
width : 0,
color : "black",
lineWidth : 1,
// this function sets the display width which limits the data buffer
// when it is called all buffers are reset
setDisplayWidth(width){
this.data.length = 0;
this.width = width;
this.writePos = 0;
if(this.lastRead === undefined){
this.lastRead = {};
}
this.lastRead.mean = 0;
this.lastRead.max = 0;
this.lastRead.min = 0;
},
// this draws the buffered data scrolling from left to right
draw(){
var data = this.data; // to save my self from writing this a zillion times
const ch = canvas.height / 2;
if(data.length > 0){ // only if there is something to draw
ctx.beginPath();
ctx.lineWidth = this.lineWidth;
ctx.strokeStyle = this.color;
ctx.lineJoin = "round";
if(data.length < this.width){ // when buffer is first filling draw from start
ctx.moveTo(0, data[0])
for(var i = 1; i < data.length; i++){
ctx.lineTo(i, data[i])
}
}else{ // buffer is full and write position is chasing the tail end
ctx.moveTo(0, data[this.writePos])
for(var i = 1; i < data.length; i++){
ctx.lineTo(i, data[(this.writePos + i) % data.length]);
}
}
ctx.stroke();
}
},
// this reads data from a data source (that has dataReader functionality)
// Speed is in data units,
// samples is number of samples per buffer write.
// samples is only usefull if speed > 1 and lets you see the
// mean, min, and max of the data over the speed unit
// If speed < 1 and sample > 1 the data is just a linear interpolation
// so the lastRead statistics are meaningless (sort of)
readFrom(dataSource,speed,samples){ // samples must be a whole positive number
samples = Math.floor(samples);
var value = 0;
var dataRead;
var min;
var max;
for(var i = 0; i < samples; i ++){ // read samples
dataSource.seek(speed / samples); // seek to next sample
dataRead = dataSource.read(); // read the sample
if(i === 0){
min = dataRead;
max = dataRead;
}else{
min = Math.min(dataRead,min);
max = Math.min(dataRead,max);
}
value += dataRead;
}
// write the samples data and statistics.
this.lastRead.min = min;
this.lastRead.max = max;
this.lastRead.delta = value / samples - this.lastRead.mean;
this.lastRead.mean = value / samples;
this.data[this.writePos] = value / samples;
this.writePos += 1;
this.writePos %= this.width;
}
}
// display data buffer
var displayBuffer = Object.assign({ // this data is displayed at 1 pixel per frame
data : [], // but data is written into it at a variable speed
},
dataDisplay // add display functionality
);
//------------------------------------------------------------------------------
// for control
const keys = {
ArrowLeft : false,
ArrowRight : false,
ArrowUp : false,
ArrowDown : false,
}
function keyEvent(event){
if(keys[event.code] !== undefined){
event.preventDefault();
keys[event.code] = true;
}
}
addEventListener("keydown",keyEvent);
//------------------------------------------------------------------------------
function mainLoop(time){
ctx.clearRect(0,0,canvas.width,canvas.height);
if(canvas.width !== displayBuffer.width){
displayBuffer.setDisplayWidth(canvas.width);
}
displayBuffer.readFrom(dataSource,dataSpeed,samplesPerFrame);
displayBuffer.draw();
//-----------------------------------------------------------------------------
// rest is display UI and stuff like that
ctx.font = "16px verdana";
ctx.fillStyle = "black";
//var dataValue =displayBuffer.lastRead.mean.toFixed(2);
//var delta = displayBuffer.lastRead.delta.toFixed(4);
var readPos = dataSource.readPos.toFixed(4);
//if(displayBuffer.lastRead.delta > 0){ delta = "+" + delta }
// ctx.fillText("Data : " + dataValue + " ( " +delta +" )" ,4,18);
ctx.setTransform(0.9,0,0,0.89,4,18);
ctx.fillText("Speed : " + dataSpeed.toFixed(3) + ", Sample rate :" +samplesPerFrame + ", Read # "+readPos ,0,0);
ctx.setTransform(0.7,0,0,0.7,4,32);
if(samplesPerFrame === 1){
ctx.fillText("Keyboard speed -left, +right Sample rate +up",0,0);
}else{
ctx.fillText("Keyboard speed -left, +right Sample rate -down, +up",0,0);
}
ctx.setTransform(1,0,0,1,0,0);
if(keys.ArrowLeft){
keys.ArrowLeft = false;
if(dataSpeed > 1){
dataSpeed -= 1;
}else{
dataSpeed *= 1/1.2;
}
}
if(keys.ArrowRight){
keys.ArrowRight = false;
if(dataSpeed >= 1){
dataSpeed += 1;
}else{
dataSpeed *= 1.2;
if(dataSpeed > 1){ dataSpeed = 1 }
}
}
if(keys.ArrowUp){
keys.ArrowUp = false;
samplesPerFrame += 1;
}
if(keys.ArrowDown){
keys.ArrowDown = false;
samplesPerFrame -= 1;
samplesPerFrame = samplesPerFrame < 1 ? 1 : samplesPerFrame;
}
requestAnimationFrame(mainLoop);
}
canvas {
border : 2px black solid;
}
<canvas id=canvas width=512 height=200></canvas>
Reading and displaying data this way is quick and simple. It is easy it add grid markings and data processing to the data source and display data. The demo (regular interval data) can easily handle displaying large data sources while zooming in and out on data. Note that for timeStamped data the above seekTime function is not suitable for large datasets. You will need to index such data for more effective seek times.

How to straighten unneeded turns in a A* graph search result?

I have been working on a JavaScript implementation of the early 90's adventure games and specifically plotting a path from the place the hero is standing to the location the player has clicked on. My approach is to first determine if a strait line (without obstructions) can be drawn, if not then to search for a path of clear way-points using Brian Grinstead's excellent javascript-astar. The problem I'm facing however is the path (while optimal will veer into spaces that would seem to the user an unintended. Here is a classic example of what I'm talking about (the green path is the generated path, the red dots are each turns where the direction of the path changes):
Now I know that A* is only guaranteed to return a path that cannot be simpler (in terms of steps), but I'm struggling to implement a heuristic that weights turns. Here is a picture that show two other paths that would also qualify as just as simple (with an equal number of steps)
The Blue path would present the same number of steps and turns while the red path has the same number of steps and fewer turns. In my code I have a simplifyPath() function that removes steps where the direction changes, so if I could get all possible paths from astar then I could select the one with the least turns, but that's not how A* fundamentally works, so I'm looking for a way to incorporate simplicity into the heuristic.
Here is my current code:
var img,
field = document.getElementById('field'),
EngineBuilder = function(field, size) {
var context = field.getContext("2d"),
graphSettings = { size: size, mid: Math.ceil(size/2)},
engine = {
getPosition: function(event) {
var bounds = field.getBoundingClientRect(),
x = Math.floor(((event.clientX - bounds.left)/field.clientWidth)*field.width),
y = Math.floor(((event.clientY - bounds.top)/field.clientHeight)*field.height),
node = graph.grid[Math.floor(y/graphSettings.size)][Math.floor(x/graphSettings.size)];
return {
x: x,
y: y,
node: node
}
},
drawObstructions: function() {
context.clearRect (0, 0, 320, 200);
if(img) {
context.drawImage(img, 0, 0);
} else {
context.fillStyle = 'rgb(0, 0, 0)';
context.fillRect(200, 100, 50, 50);
context.fillRect(0, 100, 50, 50);
context.fillRect(100, 100, 50, 50);
context.fillRect(0, 50, 150, 50);
}
},
simplifyPath: function(start, complexPath, end) {
var previous = complexPath[1], simplePath = [start, {x:(previous.y*graphSettings.size)+graphSettings.mid, y:(previous.x*graphSettings.size)+graphSettings.mid}], i, classification, previousClassification;
for(i = 1; i < (complexPath.length - 1); i++) {
classification = (complexPath[i].x-previous.x).toString()+':'+(complexPath[i].y-previous.y).toString();
if(classification !== previousClassification) {
simplePath.push({x:(complexPath[i].y*graphSettings.size)+graphSettings.mid, y:(complexPath[i].x*graphSettings.size)+graphSettings.mid});
} else {
simplePath[simplePath.length-1]={x:(complexPath[i].y*graphSettings.size)+graphSettings.mid, y:(complexPath[i].x*graphSettings.size)+graphSettings.mid};
}
previous = complexPath[i];
previousClassification = classification;
}
simplePath.push(end);
return simplePath;
},
drawPath: function(start, end) {
var path, step, next;
if(this.isPathClear(start, end)) {
this.drawLine(start, end);
} else {
path = this.simplifyPath(start, astar.search(graph, start.node, end.node), end);
if(path.length > 1) {
step = path[0];
for(next = 1; next < path.length; next++) {
this.drawLine(step, path[next]);
step = path[next];
}
}
}
},
drawLine: function(start, end) {
var x = start.x,
y = start.y,
dx = Math.abs(end.x - start.x),
sx = start.x<end.x ? 1 : -1,
dy = -1 * Math.abs(end.y - start.y),
sy = start.y<end.y ? 1 : -1,
err = dx+dy,
e2, pixel;
for(;;) {
pixel = context.getImageData(x, y, 1, 1).data[3];
if(pixel === 255) {
context.fillStyle = 'rgb(255, 0, 0)';
} else {
context.fillStyle = 'rgb(0, 255, 0)';
}
context.fillRect(x, y, 1, 1);
if(x === end.x && y === end.y) {
break;
} else {
e2 = 2 * err;
if(e2 >= dy) {
err += dy;
x += sx;
}
if(e2 <= dx) {
err += dx;
y += sy;
}
}
}
},
isPathClear: function(start, end) {
var x = start.x,
y = start.y,
dx = Math.abs(end.x - start.x),
sx = start.x<end.x ? 1 : -1,
dy = -1 * Math.abs(end.y - start.y),
sy = start.y<end.y ? 1 : -1,
err = dx+dy,
e2, pixel;
for(;;) {
pixel = context.getImageData(x, y, 1, 1).data[3];
if(pixel === 255) {
return false;
}
if(x === end.x && y === end.y) {
return true;
} else {
e2 = 2 * err;
if(e2 >= dy) {
err += dy;
x += sx;
}
if(e2 <= dx) {
err += dx;
y += sy;
}
}
}
}
}, graph;
engine.drawObstructions();
graph = (function() {
var x, y, rows = [], cols, js = '[';
for(y = 0; y < 200; y += graphSettings.size) {
cols = [];
for(x = 0; x < 320; x += graphSettings.size) {
cols.push(context.getImageData(x+graphSettings.mid, y+graphSettings.mid, 1, 1).data[3] === 255 ? 0 : 1);
}
js += '['+cols+'],\n';
rows.push(cols);
}
js = js.substring(0, js.length - 2);
js += ']';
document.getElementById('Graph').value=js;
return new Graph(rows, { diagonal: true });
})();
return engine;
}, start, end, engine = EngineBuilder(field, 10);
field.addEventListener('click', function(event) {
var position = engine.getPosition(event);
if(!start) {
start = position;
} else {
end = position;
}
if(start && end) {
engine.drawObstructions();
engine.drawPath(start, end);
start = end;
}
}, false);
#field {
border: thin black solid;
width: 98%;
background: #FFFFC7;
}
#Graph {
width: 98%;
height: 300px;
overflow-y: scroll;
}
<script src="http://jason.sperske.com/adventure/astar.js"></script>
<code>Click on any 2 points on white spaces and a path will be drawn</code>
<canvas id='field' height
='200' width='320'></canvas>
<textarea id='Graph' wrap='off'></textarea>
After digging into Michail Michailidis' excellent answer I've added the following code to my simplifyPath() function) (demo):
simplifyPath: function(start, complexPath, end) {
var previous = complexPath[1],
simplePath = [start, {x:(previous.y*graphSettings.size)+graphSettings.mid, y:(previous.x*graphSettings.size)+graphSettings.mid}],
i,
finalPath = [simplePath[0]],
classification,
previousClassification;
for(i = 1; i < (complexPath.length - 1); i++) {
classification = (complexPath[i].x-previous.x).toString()+':'+(complexPath[i].y-previous.y).toString();
if(classification !== previousClassification) {
simplePath.push({x:(complexPath[i].y*graphSettings.size)+graphSettings.mid, y:(complexPath[i].x*graphSettings.size)+graphSettings.mid});
} else {
simplePath[simplePath.length-1]={x:(complexPath[i].y*graphSettings.size)+graphSettings.mid, y:(complexPath[i].x*graphSettings.size)+graphSettings.mid};
}
previous = complexPath[i];
previousClassification = classification;
}
simplePath.push(end);
previous = simplePath[0];
for(i = 2; i < simplePath.length; i++) {
if(!this.isPathClear(previous, simplePath[i])) {
finalPath.push(simplePath[i-1]);
previous = simplePath[i-1];
}
}
finalPath.push(end);
return finalPath;
}
Basically after it reduces redundant steps in the same direction, it tries to smooth out the path by looking ahead to see if it can eliminate any steps.
Very very intresting problem! Thanks for this question! So...some observations first:
Not allowing diagonal movement fixes this problem but since you are interested in diagonal movement I had to search more.
I had a look at path simplifying algorithms like:
Ramer Douglas Peuker
(http://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm)
and an implementation: https://gist.github.com/rhyolight/2846020.
I added an implementation to your code without success. This algorithm doesn't take into account obstacles so it was difficult to adapt it.
I wonder what would the behavior be (for diagonal movements) if you had used Dijkstra instead of A* or if you used an 'all shortest paths between a pair of nodes' algorithm and then you sorted them by increasing changes in direction.
After reading a bit about A* here http://buildnewgames.com/astar/ I thought that the implementation of A-star you are using is the problem or the heuristics. I tried all the heuristics on the a-star of your code including euclidean that I coded myself and tried also all the heuristics in the http://buildnewgames.com/astar code Unfortunately all of the diagonal allowing heuristics were having the same issue you are describing.
I started working with their code because it is a one-to-one grid and yours was giving me issues drawing. Your simplifyPath that I tried to remove was also causing additional problems. You have to keep in mind that since
you are doing a remapping this could be an issue based on that
On a square grid that allows 4 directions of movement, use Manhattan distance (L1).
On a square grid that allows 8 directions of movement, use Diagonal distance (L∞).
On a square grid that allows any direction of movement, you might or might not want Euclidean distance (L2). If A* is finding paths on the grid but you are allowing movement not on the grid, you may want to consider other representations of the map. (from http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html)
What is my pseudocode algorithm:
var path = A-star();
for each node in path {
check all following nodes till some lookahead limit
if you find two nodes in the same row but not column or in the same column but not row {
var nodesToBeStraightened = push all nodes to be "straightened"
break the loop;
}
skip loop index to the next node after zig-zag
}
if nodesToBeStraightened length is at least 3 AND
nodesToBeStraightened nodes don't form a line AND
the resulting Straight line after simplification doesn't hit an obstruction
var straightenedPath = straighten by getting the first and last elements of nodesToBeStraightened and using their coordinates accordingly
return straightenedPath;
Here is the visual explanation of what is being compared in the algorithm:
Visual Explanation:
How this code will be used with yours (I did most of the changes - I tried my best but there are so many problems like with how you do drawing and because of the rounding of the grid etc - you have to use a grid and keep the scale of the paths accurate - please see also assumptions below):
var img,
field = document.getElementById('field'),
EngineBuilder = function(field, size) {
var context = field.getContext("2d"),
graphSettings = { size: size, mid: Math.ceil(size/2)},
engine = {
//[...] missing code
removeZigZag: function(currentPath,lookahead){
//for each of the squares on the path - see lookahead more squares and check if it is in the path
for (var i=0; i<currentPath.length; i++){
var toBeStraightened = [];
for (var j=i; j<lookahead+i+1 && j<currentPath.length; j++){
var startIndexToStraighten = i;
var endIndexToStraighten = i+1;
//check if the one from lookahead has the same x xor the same y with one later node in the path
//and they are not on the same line
if(
(currentPath[i].x == currentPath[j].x && currentPath[i].y != currentPath[j].y) ||
(currentPath[i].x == currentPath[j].y && currentPath[i].x != currentPath[j].x)
) {
endIndexToStraighten = j;
//now that we found something between i and j push it to be straightened
for (var k = startIndexToStraighten; k<=endIndexToStraighten; k++){
toBeStraightened.push(currentPath[k]);
}
//skip the loop forward
i = endIndexToStraighten-1;
break;
}
}
if (toBeStraightened.length>=3
&& !this.formsALine(toBeStraightened)
&& !this.lineWillGoThroughObstructions(currentPath[startIndexToStraighten], currentPath[endIndexToStraighten],this.graph?????)
){
//straightening:
this.straightenLine(currentPath, startIndexToStraighten, endIndexToStraighten);
}
}
return currentPath;
},
straightenLine: function(currentPath,fromIndex,toIndex){
for (var l=fromIndex; l<=toIndex; l++){
if (currentPath[fromIndex].x == currentPath[toIndex].x){
currentPath[l].x = currentPath[fromIndex].x;
}
else if (currentPath[fromIndex].y == currentPath[toIndex].y){
currentPath[l].y = currentPath[fromIndex].y;
}
}
},
lineWillGoThroughObstructions: function(point1, point2, graph){
var minX = Math.min(point1.x,point2.x);
var maxX = Math.max(point1.x,point2.x);
var minY = Math.min(point1.y,point2.y);
var maxY = Math.max(point1.y,point2.y);
//same row
if (point1.y == point2.y){
for (var i=minX; i<=maxX && i<graph.length; i++){
if (graph[i][point1.y] == 1){ //obstacle
return true;
}
}
}
//same column
if (point1.x == point2.x){
for (var i=minY; i<=maxY && i<graph[0].length; i++){
if (graph[point1.x][i] == 1){ //obstacle
return true;
}
}
}
return false;
},
formsALine: function(pointsArray){
//only horizontal or vertical
if (!pointsArray || (pointsArray && pointsArray.length<1)){
return false;
}
var firstY = pointsArray[0].y;
var lastY = pointsArray[pointsArray.length-1].y;
var firstX = pointsArray[0].x;
var lastX = pointsArray[pointsArray.length-1].x;
//vertical line
if (firstY == lastY){
for (var i=0; i<pointsArray.length; i++){
if (pointsArray[i].y!=firstY){
return false;
}
}
return true;
}
//horizontal line
else if (firstX == lastX){
for (var i=0; i<pointsArray.length; i++){
if (pointsArray[i].x!=firstX){
return false;
}
}
return true;
}
return false;
}
//[...] missing code
}
//[...] missing code
}
Assumptions and Incompatibilities of the above code:
obstacle is 1 and not 0
the orginal code I have in the demo is using array instead of {x: number, y:number}
in case you use the other a-star implementation, the point1 location is on the column 1 and row 2.
converted to your {x: number, y:number} but haven't checked all the parts:
I couldn't access the graph object to get the obstacles - see ?????
You have to call my removeZigZag with a lookahead e.g 7 (steps away) in the place where you were
doing your path simplification
admittedly their code is not that good compared to the a-star implementation from Stanford but for our purposes it should be irelevant
possibly the code has bugs that I don't know of and could be improved. Also I did my checks only in this specific world configuration
I believe the code has complexity O(N x lookahead)~O(N) where N is the number of steps in the input A* shortest path.
Here is the code in my github repository (you can run the demo)
https://github.com/zifnab87/AstarWithDiagonalsFixedZigZag
based on this A* Javascript implementation downloaded from here: http://buildnewgames.com/astar/
Their clickHandling and world boundary code is broken as when you click on the right side of the map the path calculation is not working sometimes. I didn't have time to find their bug. As a result my code has the same issue
Probably it is because the map I put from your question is not square - but anyway my algorithm should be unaffected. You will see this weird behavior is happening if non of my remove ZigZag code runs. (Edit: It was actually because the map was not square - I updated the map to be square for now)
Feel free to play around by uncommenting this line to see the before-after:
result = removeZigZag(result,7);
I have attached 3 before after image sets so the results can be visualized:
(Keep in mind to match start and goal if you try them - direction DOES matter ;) )
Case 1: Before
Case 1: After
Case 2: Before
Case 2: After
Case 3: Before
Case 3: After
Case 4: Before
Case 4: After
Resources:
My code (A* diagonal movement zig zag fix demo): https://github.com/zifnab87/AstarWithDiagonalsFixedZigZag
Original Javascript A* implementation of my demo can be found above (first commit) or here: - http://buildnewgames.com/astar/
A* explanation: http://buildnewgames.com/astar/
A* explanation from Stanford: http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html
JavaScript A* implementation used by OP's question (Github):
Ramer Douglas Peuker Algorithm (Wikipedia): http://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
Javascript implementation of Douglas Peuker Algorithm: https://gist.github.com/rhyolight/2846020
A* Algorithm (Wikipedia): http://en.wikipedia.org/wiki/A*_search_algorithm
You can use a modified A* algorithm to account for changes in direction. While simplifying the result of the standard A* algorithm may yield good results, it may not be optimal. This modified A* algorithm will return a path of minimal length with the least number of turns.
In the modified A* algorithm, each position corresponds to eight different nodes, each with their own heading. For example, the position (1, 1) corresponds to the eight nodes
(1,1)-up, (1,1)-down, (1,1)-right, (1,1)-left,
(1,1)-up-left, (1,1)-up-right, (1,1)-down-left, and (1,1)-down-right
The heuristic distance from a node to the goal is the the heuristic distance from the corresponding point to the goal. In this case, you probably want to use the following function:
H(point) = max(abs(goal.xcor-point.xcor), abs(goal.ycor-point.ycor))
The nodes that correspond to a particular position are connected to the nodes of the neighboring positions with the proper heading. For example, the nodes corresponding to the position (1,1) are all connected to the following eight nodes
(1,2)-up, (1,0)-down, (2,1)-right, (0,1)-left,
(0,2)-up-left, (2,2)-up-right, (0,0)-down-left, and (2,0)-down-right
The distance between any two connected nodes depends on their heading. If they have the same head, then the distance is 1, otherwise, we have made a turn, so the distance is 1+epsilon. epsilon represents an arbitrarily small value/number.
We know need to have a special case for the both the start and goal. The start and goal are both represented as a single node. At the start, we have no heading, so the distance between the start node and any connected node is 1.
We can now run the standard A* algorithm on the modified graph. We can map the returned path to a path in the original grid, by ignoring the headings. The total length of the returned path will be of the form n+m*epsilon. n is the total length of the corresponding path in the original grid, and m is the number of turns. Because A* returns a path of minimal length, the path in the original grid is a path of minimal length that makes the least turns.
I have come up with somewhat of a fix that is a simple addition to your original code, but it doesn't work in all situations (see image below) because we are limited to what the A* returns us. You can see my jsfiddle here
I added the following code to your simplifyPath function right before the return. What it does is strips out extra steps by seeing if there is a clear path between non-adjacent steps (looking at larger gaps first). It could be optimized, but you should get the gist from what I've got.
do{
shortened = false;
loop:
for(i = 0; i < simplePath.length; i++) {
for(j = (simplePath.length - 1); j > (i + 1); j--) {
if(this.isPathClear(simplePath[i],simplePath[j])) {
simplePath.splice((i + 1),(j - i - 1));
shortened = true;
break loop;
}
}
}
} while(shortened == true);
You can see below that this removes the path that goes in on the left (as in the question) but also that not all the odd turns are removed. This solution only uses the points provided from the A*, not points in between on the path - for example, because the 2nd point does not have a straight unobstructed line to the 4th or 5th points, it cannot optimize point 3 out. It happens a lot less than the original code, but it still does give weird results sometimes.
In edition to nodes having references to their parent nodes. Also store which direction that node came from inside a variable. In my case there was only two possibilities horizontally or vertically. So I created two public static constants for each possibility. And a helper function named "toDirection" which takes two nodes and return which direction should be taken in order to go from one to another:
public class Node {
final static int HORIZONTALLY = 0;
final static int VERTICALLY = 1;
int col, row;
boolean isTravelable;
int fromDirection;
double hCost;
double gCost;
double fCost;
Node parent;
public Node(int col, int row, boolean isTravelable) {
this.col = col;
this.row = row;
this.isTravelable = isTravelable;
}
public static int toDirection(Node from, Node to) {
return (from.col != to.col) ? Node.HORIZONTALLY : Node.VERTICALLY;
}
}
Then you can change your weight calculation function to take turns into account. You can now give a small punishment for turns like:
public double calcGCost(Node current, Node neighbor) {
if(current.fromDirection == Node.toDirection(current, neighbor)) {
return 1;
} else{
return 1.2;
}
}
Full code: https://github.com/tezsezen/AStarAlgorithm
At the risk of potential down voting, I will try my best to suggest an answer. If you weren't using a third party plugin I would suggest a simple pop/push stack object be built however since you are using someone else's plugin it might be best to try and work along side it rather than against it.
That being said I might just do something simple like track my output results and try to logically determine the correct answer. I would make a simple entity type object literal for storage within an array of all possible path's? So the entire object's life span is only to hold position information. Then you could later parse that array of objects looking for the smallest turn count.
Also, since this third party plugin will do most of the work behind the scenes and doesn't seem very accessible to extract, you might need to feed it criteria on your own. For example if its adding more turns then you want, i.e. inside the door looking square, then maybe sending it the coordinates of the start and end arent enouugh. Perhaps its better to stop at each turn and send in the new coordinates to see if a straight line is now possible. If you did this then each turn would have a change to look and see if there is an obstruction stopping a straight line movement.
I feel like this answer is too simple so it must be incorrect but I will try nonetheless...
//Entity Type Object Literal
var pathsFound = function() {
//Path Stats
straightLine: false,
turnCount: 0,
xPos: -1, //Probably should not be instantiated -1 but for now it's fine
yPos: -1,
//Getters
isStraightLine: function() { return this.straightLine; },
getTurnCount: function() { return this.turnCount; },
getXPos: function() { return this.xPos; },
getYPos: function() { return this.yPos; },
//Setters
setStraightLine: function() { this.straightLine = true; },
setCrookedLine: function() { this.straightLine = false; },
setXPos: function(val) { this.xPos = val; },
setYPos: function(val) { this.yPos = val; },
//Class Functionality
incrementTurnCounter: function() { this.turnCount++; },
updateFullPosition: function(xVal, yVal) {
this.xPos = xVal;
this.yPos = yVal.
},
}
This way you could report all the data every step of the way and before you draw to the screen you could iterate through your array of these object literals and find the correct path by the lowest turnCount.
var img,
field = document.getElementById('field'),
EngineBuilder = function(field, size) {
var context = field.getContext("2d"),
graphSettings = { size: size, mid: Math.ceil(size/2)},
engine = {
getPosition: function(event) {
var bounds = field.getBoundingClientRect(),
x = Math.floor(((event.clientX - bounds.left)/field.clientWidth)*field.width),
y = Math.floor(((event.clientY - bounds.top)/field.clientHeight)*field.height),
node = graph.grid[Math.floor(y/graphSettings.size)][Math.floor(x/graphSettings.size)];
return {
x: x,
y: y,
node: node
}
},
drawObstructions: function() {
context.clearRect (0, 0, 320, 200);
if(img) {
context.drawImage(img, 0, 0);
} else {
context.fillStyle = 'rgb(0, 0, 0)';
context.fillRect(200, 100, 50, 50);
context.fillRect(0, 100, 50, 50);
context.fillRect(100, 100, 50, 50);
context.fillRect(0, 50, 150, 50);
}
},
simplifyPath: function(start, complexPath, end) {
var previous = complexPath[1], simplePath = [start, {x:(previous.y*graphSettings.size)+graphSettings.mid, y:(previous.x*graphSettings.size)+graphSettings.mid}], i, classification, previousClassification;
for(i = 1; i < (complexPath.length - 1); i++) {
classification = (complexPath[i].x-previous.x).toString()+':'+(complexPath[i].y-previous.y).toString();
if(classification !== previousClassification) {
simplePath.push({x:(complexPath[i].y*graphSettings.size)+graphSettings.mid, y:(complexPath[i].x*graphSettings.size)+graphSettings.mid});
} else {
simplePath[simplePath.length-1]={x:(complexPath[i].y*graphSettings.size)+graphSettings.mid, y:(complexPath[i].x*graphSettings.size)+graphSettings.mid};
}
previous = complexPath[i];
previousClassification = classification;
}
simplePath.push(end);
return simplePath;
},
drawPath: function(start, end) {
var path, step, next;
if(this.isPathClear(start, end)) {
this.drawLine(start, end);
} else {
path = this.simplifyPath(start, astar.search(graph, start.node, end.node), end);
if(path.length > 1) {
step = path[0];
for(next = 1; next < path.length; next++) {
this.drawLine(step, path[next]);
step = path[next];
}
}
}
},
drawLine: function(start, end) {
var x = start.x,
y = start.y,
dx = Math.abs(end.x - start.x),
sx = start.x<end.x ? 1 : -1,
dy = -1 * Math.abs(end.y - start.y),
sy = start.y<end.y ? 1 : -1,
err = dx+dy,
e2, pixel;
for(;;) {
pixel = context.getImageData(x, y, 1, 1).data[3];
if(pixel === 255) {
context.fillStyle = 'rgb(255, 0, 0)';
} else {
context.fillStyle = 'rgb(0, 255, 0)';
}
context.fillRect(x, y, 1, 1);
if(x === end.x && y === end.y) {
break;
} else {
e2 = 2 * err;
if(e2 >= dy) {
err += dy;
x += sx;
}
if(e2 <= dx) {
err += dx;
y += sy;
}
}
}
},
isPathClear: function(start, end) {
var x = start.x,
y = start.y,
dx = Math.abs(end.x - start.x),
sx = start.x<end.x ? 1 : -1,
dy = -1 * Math.abs(end.y - start.y),
sy = start.y<end.y ? 1 : -1,
err = dx+dy,
e2, pixel;
for(;;) {
pixel = context.getImageData(x, y, 1, 1).data[3];
if(pixel === 255) {
return false;
}
if(x === end.x && y === end.y) {
return true;
} else {
e2 = 2 * err;
if(e2 >= dy) {
err += dy;
x += sx;
}
if(e2 <= dx) {
err += dx;
y += sy;
}
}
}
}
}, graph;
engine.drawObstructions();
graph = (function() {
var x, y, rows = [], cols, js = '[';
for(y = 0; y < 200; y += graphSettings.size) {
cols = [];
for(x = 0; x < 320; x += graphSettings.size) {
cols.push(context.getImageData(x+graphSettings.mid, y+graphSettings.mid, 1, 1).data[3] === 255 ? 0 : 1);
}
js += '['+cols+'],\n';
rows.push(cols);
}
js = js.substring(0, js.length - 2);
js += ']';
document.getElementById('Graph').value=js;
return new Graph(rows, { diagonal: true });
})();
return engine;
}, start, end, engine = EngineBuilder(field, 10);
field.addEventListener('click', function(event) {
var position = engine.getPosition(event);
if(!start) {
start = position;
} else {
end = position;
}
if(start && end) {
engine.drawObstructions();
engine.drawPath(start, end);
start = end;
}
}, false);
#field {
border: thin black solid;
width: 98%;
background: #FFFFC7;
}
#Graph {
width: 98%;
height: 300px;
overflow-y: scroll;
}
<script src="http://jason.sperske.com/adventure/astar.js"></script>
<code>Click on any 2 points on white spaces and a path will be drawn</code>
<canvas id='field' height
='200' width='320'></canvas>
<textarea id='Graph' wrap='off'></textarea>

move div smoothly with javascript/jquery and an array of pos

I'm looking for a way to move a div from an array of position with javascript/jquery.
I have trying to do it with jquery.animate but he moved the div with a pause at each iteration of my array.
That could be something like move the div from 0,0 to 120px,230px passing by the 23px,35px;45px,50px etc...
That is for moving an game character on a Tile map
So as requested, some bit of code
First you have a global timer that call a function at short interval to see if it have any action to execute.
In this loop a routine look if some mobile tiles are waiting of any mouvement.
Mobiles are declared as Object class and have a sub function that do the deplacement like that
setPos:function(coord){
var pos = jQuery("#"+this.id).position();
var x = (coord[0] - 32 + this.screenOffX + this.xOffset) - pos.left;
var y =(coord[1] + this.yOffset) - pos.top;
//this.stopAnimation();
//this.startAnimation(this.walkingAnimation);
jQuery("#"+this.id).animate({
left: '+='+ x,
top: '+='+ y
}, 33, function() {
// Animation complete.
});
},
That is a bit messy cause i trying a lot of thing to do the smooth movement that i'm looking for.
so setPos is calling in another place like that
stepMobile:function(mobile){
var wp;/*TEST*/
mobile.changeState("idle");
var ind = mobile.getWayPointIndex();
while(ind < (mobile.getWayPoints()).length - 1){
if (ind < (mobile.getWayPoints()).length - 1) {
wp = (mobile.getWayPoints())[ind + 1];
if (getTime() > wp.time) {
mobile.setWayPointIndex(ind + 1);
ind = ind +1;
}
}
wp = (mobile.getWayPoints())[ind];
var x;
var y = 0;
var z;
x = this.tileWidth * (wp.getTile()).getCol();
z = this.tileHeight * (wp.getTile()).getRow();
var elapsed = getTime() - wp.getTime();
console.log(elapsed);
if (ind == (mobile.getWayPoints()).length - 1) {
console.log('checkForOnStopEvent()');
} else {
//x += 1 * mobile.getWalkSpeed() * mobile.getCosAngle();
//z += 1 * mobile.getWalkSpeed() * mobile.getSinAngle();
}
var coord = this.mapToScreen(x, y, -z);
mobile.setPos(coord);
ind = mobile.getWayPointIndex();
}
},
Again lot of junk code here cause i literally burned my brain but i didn't get any good result.
And you have that global function that run this function over all mobiles waiting for deplacement.

Categories

Resources