I reformatted some code I found to use cardinal splines and draw a curve given a set of points to work with my Canvas library, which works quite nicely, but then I wanted to also use said technique to move objects along a given set of points -- a path. SO had several questions pertaining to my problem, and I've tried to implement the last answer of this question, but I honestly have no idea what half the variables in his code mean. Here's my library, and the curve object by itself:
Art.prototype.modules.display.Base.extend({
constructor: function (options) {
// Declare variables for brevity.
var extend = Art.prototype.modules.utility.extend,
defaults = {
points: [],
tension: 0.5,
closed: false
};
// Extend the object with the defaults overwritten by the options.
extend(this, extend(defaults, options));
},
id: 'curve',
draw: function () {
// Declare variables for brevity.
var t = this,
graphics = Art.prototype.modules.display.curve.core.graphics,
controls = [],
n = t.points.length,
getControlPoints = function (a, b, c, d, e, f, tension) {
var x = {
x: Math.sqrt(Math.pow(c - a, 2) + Math.pow(d - b, 2)),
y: Math.sqrt(Math.pow(e - c, 2) + Math.pow(f - d, 2))
};
var y = {
x: tension * x.x / (x.x + x.y)
};
y.y = tension - y.x;
var z = {
x: c + y.x * (a - e),
y: d + y.x * (b - f)
};
var $z = {
x: c - y.y * (a - e),
y: d - y.y * (b - f)
};
return [z.x, z.y, $z.x, $z.y];
};
graphics.strokeStyle = t.stroke;
graphics.lineWidth = t.lineWidth;
if (t.closed) {
t.points.push(t.points[0], t.points[1], t.points[2], t.points[3]);
t.points.unshift(t.points[n - 1]);
t.points.unshift(t.points[n - 1]);
for (var p = 0; p < n; p += 2) {
controls = controls.concat(getControlPoints(t.points[p], t.points[p + 1], t.points[p + 2], t.points[p + 3], t.points[p + 4], t.points[p + 5], t.tension));
}
controls = controls.concat(controls[0], controls[1]);
for (var $p = 2; $p < n + 2; $p += 2) {
graphics.beginPath();
graphics.moveTo(t.points[$p], t.points[$p + 1]);
graphics.bezierCurveTo(controls[2 * $p - 2], controls[2 * $p - 1], controls[2 * $p], controls[2 * $p + 1], t.points[$p + 2], t.points[$p + 3]);
graphics.stroke();
graphics.closePath();
}
} else {
for (var p = 0; p < n - 4; p += 2) {
controls = controls.concat(getControlPoints(t.points[p], t.points[p + 1], t.points[p + 2], t.points[p + 3], t.points[p + 4], t.points[p + 5], t.tension));
}
for (var $p = 2; $p < t.points.length - 5; $p += 2) {
graphics.beginPath();
graphics.moveTo(t.points[$p], t.points[$p + 1]);
graphics.bezierCurveTo(controls[2 * $p - 2], controls[2 * $p - 1], controls[2 * $p], controls[2 * $p + 1], t.points[$p + 2], t.points[$p + 3]);
graphics.stroke();
graphics.closePath();
}
graphics.beginPath();
graphics.moveTo(t.points[0], t.points[1]);
graphics.quadraticCurveTo(controls[0], controls[1], t.points[2], t.points[3]);
graphics.stroke();
graphics.closePath();
graphics.beginPath();
graphics.moveTo(t.points[n - 2], t.points[n - 1]);
graphics.quadraticCurveTo(controls[2 * n - 10], controls[2 * n - 9], t.points[n - 4], t.points[n - 3]);
graphics.stroke();
graphics.closePath();
}
return this;
}
});
I don't necessarily want the code handed to me on a silver platter (although... that would be nice) -- rather, I want to learn the math involved, but preferably in psuedocode and in relatively simple terms. An explanation of the SO answer I linked to would be especially helpful, as it works nicely.
Using an alternative implementation (https://gitlab.com/epistemex/cardinal-spline-js disclaimer: I'm the author) will produce all points along the path that you need in a simple way.
You can now calculate the total length
Find the corresponding segment in the returned array
Normalize the remainder to find exact location on the path
Example
After obtaining the points as a spline point array, the main function will iterate over the array to find the segment the two points the desired position is between. Next it will interpolate between those to get a final (x,y) position (there is plenty of room for optimization here):
This allows us to move along the spline with an even speed -
function getXY(points, pos, length) {
var len = 0, // total length so far
lastLen, // last segment length
i, // iterator
l = points.length; // length of point array
// find segment
for(i = 2; i < l; i += 2) {
// calculate length of this segment
lastLen = dist(points[i], points[i+1], points[i-2], points[i-1]);
// add to total length for now
len += lastLen;
// are we inside a segment?
if (pos < len && lastLen) {
len -= lastLen; // to back to beginning
pos -= len; // adjust position so we can normalize
return {
// interpolate prev X + (next X - prev X) * normalized
x: points[i-2] + (points[i] - points[i-2]) * (pos / lastLen),
y: points[i-1] + (points[i+1] - points[i-1]) * (pos / lastLen)
}
}
}
}
Full example
var ctx = document.querySelector("canvas").getContext("2d"),
points = [
10, 10, // x,y pairs
100, 50,
500, 100,
600, 200,
400, 220,
200, 90
],
spline = getCurvePoints(points),
length = getLength(spline),
t = 0,
dx = 3; // number of pixels to move object
// move along path:
(function loop() {
// make t ping-pong, and clamp t to [0, (length-1)]
t += dx;
if (t < 0 || t >= length) dx = -dx;
t = Math.max(0, Math.min(length - 1, t));
// find segment in points which t is inside:
var pos = getXY(spline, t, length);
// redraw
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
render();
// show marker
ctx.fillRect(pos.x - 3, pos.y - 3, 6, 6);
requestAnimationFrame(loop)
})();
function render(points) {
ctx.beginPath();
ctx.moveTo(spline[0], spline[1]);
for(var i = 2; i < spline.length; i+=2)
ctx.lineTo(spline[i], spline[i+1]);
ctx.stroke()
}
function getXY(points, pos, length) {
var len = 0, lastLen, i, l = points.length;
// find segment
for(i = 2; i < l; i += 2) {
lastLen = dist(points[i], points[i+1], points[i-2], points[i-1]);
len += lastLen;
if (pos < len && lastLen) {
len -= lastLen;
pos -= len;
return {
x: points[i-2] + (points[i] - points[i-2]) * (pos / lastLen),
y: points[i-1] + (points[i+1] - points[i-1]) * (pos / lastLen)
}
}
}
return null
}
function getLength(points) {
for(var len = 0, i = 0, dx, dy; i < points.length - 2; i+=2) {
len += dist(points[i+2], points[i+3], points[i], points[i+1])
}
return len
}
function dist(x1, y1, x2, y2) {
var dx = x2 - x1,
dy = y2 - y1;
return Math.sqrt(dx*dx + dy*dy)
}
Related
I'm trying to construct a Voronoi diagram using p5.js. So far I have managed to create an image representation of it by coloring pixels that belong to the same region, using the algorithm from this video. Here how it looks like:
Here's the code (a bit messy and terribly inefficient)
const h = 512
const w = 512
const scl = 512;
const rez = h / scl;
const tiles = []
let points = []
function randomIntFromInterval(min, max) { // min and max included
return Math.floor(Math.random() * (max - min + 1) + min)
}
function setup() {
createCanvas(w, h);
background(220);
for (let i = 0; i < 12; i++) {
const p = randomIntFromInterval(0, h * h)
points.push(p)
}
for (let y = 0; y < scl; y++) {
for (let x = 0; x < scl; x++) {
tiles.push(0);
const xx = x * rez;
const yy = y * rez;
noFill();
stroke(0);
rect(xx, yy, rez, rez);
const indx = `${x + y * scl}`
if (+indx === points[0] || +indx === points[1] || +indx === points[2]) {
stroke(225, 0, 0)
fill(255, 0, 0)
}
text(indx, xx + rez / 2 - 5, yy + rez / 2 + 5)
}
}
compute(0, scl, scl)
for (const p of points) {
const x = p % scl;
const y = (p - x) / scl;
fill(0)
circle(x * rez, y * rez, 5)
}
}
function compute(x, len, grandLen) {
const corners = getCorners(x, len, grandLen)
const lookup = []
for (const corner of corners) {
const ds = []
for (const point of points) {
ds.push(distance(corner, point, scl));
}
lookup.push(ds)
}
const is = []
for (let i = 0; i < lookup.length; i++) {
const min = Math.min(...lookup[i])
const iMin = lookup[i].indexOf(min);
is.push(iMin);
}
if (is.every((val, i, arr) => val === arr[0])) {
const colorR = map(points[is[0]], 0, h*h, 0, 255)
const colorG = 255 - map(points[is[0]], 0, h*h, 0, 255)
paintRegion(corners[0], len, rez, color(colorR, colorG, 200))
} else {
const rects = divide(corners[0], len)
rects.forEach(r => {
compute(r, len / 2, grandLen)
})
}
}
function paintRegion(a, len, size, color) {
let ax;
let ay;
[ax, ay] = toCoords(a);
fill(color)
noStroke(0);
rect(ax * size, ay * size, size * len, size * len)
}
function toCoords(index) {
const x = index % scl;
const y = Math.floor((index - x) / scl);
return [x, y]
}
function distance(a, b, len) {
let ax;
let ay;
let bx;
let by;
[ax, ay] = toCoords(a);
[bx, by] = toCoords(b);
const p1 = Math.pow(bx - ax, 2);
const p2 = Math.pow(by - ay, 2);
return sqrt(p1 + p2);
}
// l1 is square, l2 is canvas
function getCorners(a, l1, l2) {
const corners = []
corners.push(a);
corners.push(a + l1 - 1);
corners.push(a + (l1 - 1) * l2)
corners.push(a + (l1 - 1) * l2 + (l1 - 1));
return corners
}
function divide(a, len) {
let ax;
let ay;
[ax, ay] = toCoords(a);
const d = len / 2;
const p1 = ax + ay * scl;
const p2 = ax + d + ay * scl;
const p3 = ax + (ay + d) * scl;
const p4 = ax + d + (ay + d) * scl;
return [p1, p2, p3, p4];
}
function draw() {
}
<script src="https://cdn.jsdelivr.net/npm/p5#1.5.0/lib/p5.js"></script>
My problem is, such representation isn't very useful for me. I need get coordinates of regions' edges (including edges of a canvas). I've searched for ways to find edges of a shape on a 2d plane, but I feel like this is a very backwards approach. Is there a way to calculate a Voronoi diagram edges? If not, what's the simplest way to find edges by going over the pixels array?
I know there is quite a few resources on how to do this using numpy or in matlab, but I actually need a javaScript solution, if possible.
UPD: As I was researching the topic further and looking into related question brought up by Cristian in the comments, I came up to a conclusion that Future's algorithms is the best option for getting regions' edges. Fortunately, Wikipedia has links to its implementations. I tried this great implementation by Raymond Hill and it worked out great.
I just read this tutorial and tried this example. So I downloaded a video from web for my own testing. All I have to do is tweak rgb values in if conditions
HERE is the sample code from example
computeFrame: function() {
this.ctx1.drawImage(this.video, 0, 0, this.width, this.height);
let frame = this.ctx1.getImageData(0, 0, this.width, this.height);
let l = frame.data.length / 4;
for (let i = 0; i < l; i++) {
let r = frame.data[i * 4 + 0];
let g = frame.data[i * 4 + 1];
let b = frame.data[i * 4 + 2];
if (g > 100 && r > 100 && b < 43)
frame.data[i * 4 + 3] = 0;
}
this.ctx2.putImageData(frame, 0, 0);
return;
}
In the tutorial example its filtering out yellow(not yellow I guess) color. The sample video I downloaded uses green background. So I tweaked rgb value in if condition to get desired results
After multiple tries, I managed to get this.
Now what I want to know is how can I accurately filter out green screen (or any other screen)perfectly without guessing. Or randomly tweaking values.
With just guessing it take hours to get it perfectly right. And this is just a sample with a real world application. It can take maybe more.
NOTE: The example is working in Firefox for now..
You probably just need a better algorithm. Here's one, it's not perfect, but you can tweak it a lot easier.
Basically you'll just need a colorpicker, and pick the lightest and darkest values from the video (putting the RGB values in the l_ and d_ variables respectively). You can adjust the tolerance a little bit if you need to, but getting the l_ and r_ values just right by picking different areas with the color picker will give you a better key.
let l_r = 131,
l_g = 190,
l_b = 137,
d_r = 74,
d_g = 148,
d_b = 100;
let tolerance = 0.05;
let processor = {
timerCallback: function() {
if (this.video.paused || this.video.ended) {
return;
}
this.computeFrame();
let self = this;
setTimeout(function () {
self.timerCallback();
}, 0);
},
doLoad: function() {
this.video = document.getElementById("video");
this.c1 = document.getElementById("c1");
this.ctx1 = this.c1.getContext("2d");
this.c2 = document.getElementById("c2");
this.ctx2 = this.c2.getContext("2d");
let self = this;
this.video.addEventListener("play", function() {
self.width = self.video.videoWidth;
self.height = self.video.videoHeight;
self.timerCallback();
}, false);
},
calculateDistance: function(c, min, max) {
if(c < min) return min - c;
if(c > max) return c - max;
return 0;
},
computeFrame: function() {
this.ctx1.drawImage(this.video, 0, 0, this.width, this.height);
let frame = this.ctx1.getImageData(0, 0, this.width, this.height);
let l = frame.data.length / 4;
for (let i = 0; i < l; i++) {
let _r = frame.data[i * 4 + 0];
let _g = frame.data[i * 4 + 1];
let _b = frame.data[i * 4 + 2];
let difference = this.calculateDistance(_r, d_r, l_r) +
this.calculateDistance(_g, d_g, l_g) +
this.calculateDistance(_b, d_b, l_b);
difference /= (255 * 3); // convert to percent
if (difference < tolerance)
frame.data[i * 4 + 3] = 0;
}
this.ctx2.putImageData(frame, 0, 0);
return;
}
};
// :/
If performance does not matter, then you could work in another color space e.g. HSV. You could use the left top pixel as reference.
You compare the hue value of the reference point with hue value other pixels, and exclude all pixels that exceed a certain threshold and dark and light areas using saturation and value.
This how ever does not completely get rid of color bleeding, there you might need to do some color correct/desaturation.
function rgb2hsv () {
var rr, gg, bb,
r = arguments[0] / 255,
g = arguments[1] / 255,
b = arguments[2] / 255,
h, s,
v = Math.max(r, g, b),
diff = v - Math.min(r, g, b),
diffc = function(c){
return (v - c) / 6 / diff + 1 / 2;
};
if (diff == 0) {
h = s = 0;
} else {
s = diff / v;
rr = diffc(r);
gg = diffc(g);
bb = diffc(b);
if (r === v) {
h = bb - gg;
}else if (g === v) {
h = (1 / 3) + rr - bb;
}else if (b === v) {
h = (2 / 3) + gg - rr;
}
if (h < 0) {
h += 1;
}else if (h > 1) {
h -= 1;
}
}
return {
h: Math.round(h * 360),
s: Math.round(s * 100),
v: Math.round(v * 100)
};
}
let processor = {
timerCallback: function() {
if (this.video.paused || this.video.ended) {
return;
}
this.computeFrame();
let self = this;
setTimeout(function () {
self.timerCallback();
}, 0);
},
doLoad: function() {
this.video = document.getElementById("video");
this.c1 = document.getElementById("c1");
this.ctx1 = this.c1.getContext("2d");
this.c2 = document.getElementById("c2");
this.ctx2 = this.c2.getContext("2d");
let self = this;
this.video.addEventListener("play", function() {
self.width = self.video.videoWidth / 2;
self.height = self.video.videoHeight / 2;
self.timerCallback();
}, false);
},
computeFrame: function() {
this.ctx1.drawImage(this.video, 0, 0, this.width, this.height);
let frame = this.ctx1.getImageData(0, 0, this.width, this.height);
let l = frame.data.length / 4;
let reference = rgb2hsv(frame.data[0], frame.data[1], frame.data[2]);
for (let i = 0; i < l; i++) {
let r = frame.data[i * 4 + 0];
let g = frame.data[i * 4 + 1];
let b = frame.data[i * 4 + 2];
let hsv = rgb2hsv(r, g, b);
let hueDifference = Math.abs(hsv.h - reference.h);
if( hueDifference < 20 && hsv.v > 50 && hsv.s > 50 ) {
frame.data[i * 4 + 3] = 0;
}
}
this.ctx2.putImageData(frame, 0, 0);
return;
}
};
You're in a javascript loop:
The loop spits out random numbers increasing or decreasing by 1. It starts at 10 and begins to loop:
10, 9, 8, 7, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 4, 5, 6, 7, 6
I want to log the peak numbers. So in the list above that would be 10, 9, 7
So I assume I would need to log the last 3 numbers as the script loops. 2 numbers ago(a), 1 number ago(b), current number(c) and check if c<b && a<b then log b if that turned out to be true.
I'm unsure of how to actually store those 3 numbers without them being overwritten. So say I did let current = [current number]; because it's a loop that would always be the current number, but how would I store the previous 2 numbers without having them constant be overwritten while remaining in the loop?
UPDATE:
I'm trying to grab the y value for when the ball bounces highest. So if it bounces 3 times I would have 3 y values (when the ball reached its peak 3 times).
The numbers are being logged in the console.
*** Run the code in full page view
'use strict';
// Todo
// - Make the ball spin
// - Make the ball squish
// - Add speed lines
// - Clear only the ball not the whole canvas
(function () {
const canvas = document.getElementsByClassName('canvas')[0],
c = canvas.getContext('2d');
// -----------------------------------
// Resize the canvas to be full screen
// -----------------------------------
window.addEventListener('resize', resizeCanvas, false);
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// ---------
// Variables
// ---------
var circleRadius = 40,
circleHeight = circleRadius * 2,
x = (canvas.width/2) - circleRadius, // inital x position of the ball
y = (canvas.height/2) - circleRadius, // inital y position of the ball
fallHeight = y,
vx = 0, // velocity
vy = 0, // velocity
groundHeight = circleHeight,
bouncePoints = [],
gravity = 0.8,
dampening = 0.5,
pullStrength = 0.04,
segments = 4,
bezieCircleFormula = (4/3)*Math.tan(Math.PI/(2*segments)), // http://stackoverflow.com/a/27863181/2040509
pointOffset = {
positive: bezieCircleFormula*circleRadius,
negative: circleRadius-(bezieCircleFormula*circleRadius)
},
// Each side has 3 points, bezier 1, circle point, bezier 2
// These are listed below in clockwise order.
// So top has: left bezier, circle point, right bezier
// Right has: top bezier, circle point, bottom bezier
circlePoints = {
top: [
[x+pointOffset.negative, y],
[x+circleRadius, y],
[x+pointOffset.positive+circleRadius, y]
],
right: [
[x+circleHeight, y+pointOffset.negative],
[x+circleHeight, y+circleRadius],
[x+circleHeight, y+pointOffset.positive+circleRadius]
],
bottom: [
[x+pointOffset.positive+circleRadius, y+circleHeight],
[x+circleRadius, y+circleHeight],
[x+pointOffset.negative, y+circleHeight]
],
left: [
[x, y+pointOffset.positive+circleRadius],
[x, y+circleRadius],
[x, y+pointOffset.negative]
]
};
// --------------------
// Ball squish function
// --------------------
// For `side` you can pass `top`, `right`, `bottom`, `left`
// For `amount` use an interger
function squish (side, squishAmount) {
for (let i = 0; i < circlePoints[side].length; i++) {
if (side === 'top') {
circlePoints[side][i][1] += squishAmount;
} else if (side === 'right') {
circlePoints[side][i][0] -= squishAmount;
} else if (side === 'bottom') {
circlePoints[side][i][1] -= squishAmount;
} else if (side === 'left') {
circlePoints[side][i][0] += squishAmount;
}
}
}
// ------------------
// Animation Function
// ------------------
function render () {
// Clear the canvas
c.clearRect(0, 0, canvas.width, canvas.height);
// -----------------
// Draw the elements
// -----------------
// Ground
c.beginPath();
c.fillStyle = '#9cccc8';
c.fillRect(0, canvas.height - groundHeight, canvas.width, groundHeight);
c.closePath();
// Shadow
let distanceFromGround = parseFloat(((y - canvas.height/2) + circleHeight) / (canvas.height/2 - groundHeight/2)).toFixed(4),
shadowWidth = circleRadius * (1-distanceFromGround+1),
shadowHeight = circleRadius/6 * (1-distanceFromGround+1),
shadowX = (x + circleRadius) - shadowWidth/2,
shadowY = canvas.height - groundHeight/2,
shadowOpacity = 0.15 * distanceFromGround; // The first value here represents the opacity that will be used when the ball is touching the ground
c.beginPath();
c.fillStyle = 'rgba(0,0,0, ' + shadowOpacity + ')';
c.moveTo(shadowX, shadowY);
c.bezierCurveTo(shadowX, shadowY - shadowHeight, shadowX + shadowWidth, shadowY - shadowHeight, shadowX + shadowWidth, shadowY);
c.bezierCurveTo(shadowX + shadowWidth, shadowY + shadowHeight, shadowX, shadowY + shadowHeight, shadowX, shadowY);
c.fill();
c.closePath();
// Bezier circle
c.beginPath();
c.fillStyle = '#cf2264';
c.moveTo(circlePoints.left[1][0], circlePoints.left[1][1]);
c.bezierCurveTo(circlePoints.left[2][0], circlePoints.left[2][1], circlePoints.top[0][0], circlePoints.top[0][1], circlePoints.top[1][0], circlePoints.top[1][1]);
c.bezierCurveTo(circlePoints.top[2][0], circlePoints.top[2][1], circlePoints.right[0][0], circlePoints.right[0][1], circlePoints.right[1][0], circlePoints.right[1][1]);
c.bezierCurveTo(circlePoints.right[2][0], circlePoints.right[2][1], circlePoints.bottom[0][0], circlePoints.bottom[0][1], circlePoints.bottom[1][0], circlePoints.bottom[1][1]);
c.bezierCurveTo(circlePoints.bottom[2][0], circlePoints.bottom[2][1], circlePoints.left[0][0], circlePoints.left[0][1], circlePoints.left[1][0], circlePoints.left[1][1]);
c.stroke();
c.closePath();
// -------------------------------
// Recalculate circle co-ordinates
// -------------------------------
circlePoints = {
top: [
[x+pointOffset.negative, y],
[x+circleRadius, y],
[x+pointOffset.positive+circleRadius, y]
],
right: [
[x+circleHeight, y+pointOffset.negative],
[x+circleHeight, y+circleRadius],
[x+circleHeight, y+pointOffset.positive+circleRadius]
],
bottom: [
[x+pointOffset.positive+circleRadius, y+circleHeight],
[x+circleRadius, y+circleHeight],
[x+pointOffset.negative, y+circleHeight]
],
left: [
[x, y+pointOffset.positive+circleRadius],
[x, y+circleRadius],
[x, y+pointOffset.negative]
]
};
// -----------------
// Animation Gravity
// -----------------
// Increment gravity
vy += gravity;
// Increment velocity
y += vy;
x += vx;
// ----------
// Boundaries
// ----------
// Bottom boundary
if (y + circleHeight > canvas.height - groundHeight/2) {
y = canvas.height - groundHeight/2 - circleHeight;
vy *= -1;
// Dampening
vy *= dampening;
vx *= dampening;
// If the Y velocity is less than the value below, stop the ball
if (vy > -2.4) {
dampening = 0;
}
fallHeight = fallHeight*dampening;
}
// Right boundary
if (x + circleHeight > canvas.width) {
x = canvas.width - circleHeight;
vx *= -1;
// Dampening
vy *= dampening;
vx *= dampening;
}
// Left boundary
if (x + circleHeight < 0 + circleHeight) {
x = 0;
vx *= -1;
// Dampening
vy *= dampening;
vx *= dampening;
}
// Top boundary
if (y < 0) {
y = 0;
vy *= -1;
// Dampening
vy *= dampening;
vx *= dampening;
}
console.log(y);
requestAnimationFrame(render);
}
// -----------
// Click event
// -----------
canvas.addEventListener('mousedown', function (e) {
let dx = e.pageX - x,
dy = e.pageY - y;
if (dampening === 0) {
dampening = 0.5;
}
vx += dx * pullStrength;
vy += dy * pullStrength;
});
render();
}
resizeCanvas();
})();
body{
margin: 0;
}
canvas {
background: #ddf6f5;
display: block;
}
<canvas class="canvas"></canvas>
for(var a=0,b=1,c=2; c < input.length;)
{
if(input[b] > input[a] && input[b] > input[c])
{
console.log(input[b]);
}
a++;b++;c++;
}
As I explained, push them into the array.
var nums = [10, 9, 8, 7, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 4, 5, 6, 7, 6];
var peaks = [];
nums.forEach(function(val, index, arr){
var isPrevLess = index==0 || arr[index-1]<val, //is the last number less
isNextLess = index==arr.length-1 || arr[index+1]<val; //is the next number less
if (isPrevLess && isNextLess) { //if both are less it is a peak
peaks.push(val);
}
});
console.log(peaks);
If I understand correctly, array arr of length l has a peak at position i if any of these is fulfilled:
0 = i < l-1 and arr[i] > arr[i+1]
0 < i < l-1 and arr[i-1] < arr[i] > l-1
0 < i = l-1 and arr[i] > arr[i-1]
0 = i = l-1
Then, you can use
var peaks = arr.filter(function(n, i, a) {
return (i===0 || n > a[i-1]) && (i===a.length-1 || n > arr[i+1]);
});
You really should post a function that does what you say, then your attempt at a solution. Since that's missing…
function foo(startNum, count) {
var prev = -Infinity;
var sense = 'up';
var curr = startNum;
for (var i=0; i<count; i++) {
// Randomly add or subtract one from current
curr += Math.random() < 0.5? -1 : 1;
// If it's a peak, do something. If it's not a peak, do something else
if (sense == 'up' && curr < prev) { // Hit peak
document.write('<br>peak: ' + prev + ' going ' + sense);
} else {
document.write('<br>Not peak: ' + prev + ' going ' + sense);
}
// Prepare for next loop
sense = prev > curr? 'down' : prev < curr? 'up' : sense;
prev = curr;
}
}
foo(10,20);
First off, sorry about the bad title, I couldn't think of a better way to describe what I was trying to do. I have an HTML canvas, which, for argument's sake, is x pixels wide and y pixels tall. I have been trying to write a code that takes the location in the array of the canvas' image data of two pixels that are on lines z and z + 1 and fill in all the pixels in the higher row between the two pixels a certain color. I'm afraid I may not have made much sense, so here's a diagram:
Sorry about the poor graphics, but assume each rectangle is a pixel. The program should take in the first value for each of the black pixels (each is stored as r,g,b,a, the program gets the location of the r in the array representing the canvas' image data), and stores the r value for the lower pixel as bottomPixel and the higher one as topPixel. In this case, bottomPixel = 124 and topPixel = 112 It should use this to fill all pixels between the two base pixels a certain color. For example, using the previous pixel locations, the red pixels in the following picture should be colored in, but the blue one should not.
Here is the code I have: (Assume that the canvas has an Id "Canvas" and is 6px wide by 10px tall)
var cnvs = document.getElementById("Canvas");
cnvs.height = 10; //This height and width is here as an example.
cnvs.width = 6;
var cont = cnvs.getContext("2d");
var environment = cont.getImageData(0,0,6,10);
var bottomPixel = 124;//Not neccesarily 124 or 112, just example values
var topPixel = 112;
if ( bottomPixel - topPixel > 6*4 ) //If bottomPixel is to the right of topPixel
{
for ( var i = 0 ; i < ((bottomPixel-6*4)-topPixel)/4 ; i++ )
{
var index = topPixel + i * 4;
environment.data[index] = 0;
environment.data[index + 1 ] = 255;
environment.data[index + 2 ] = 0;
environment.data[index + 3 ] = 255;
}
}
if ( bottomPixel - topPixel > 6*4 ) //If bottomPixel is to the left of topPixel
{
for ( var i = 0 ; i < (topPixel-(bottomPixel-6*4))/4; i++ )
{
var index = topPixel - i * 4;
environment.data[index] = 0;
environment.data[index + 1 ] = 255;
environment.data[index + 2 ] = 0;
environment.data[index + 3 ] = 255;
}
}
I'd like to know why my code isn't doing what I previously described. If anything here needs clarification, please leave a comment. Thanks!
This is a method that works on the point coordinates and uses a the setPixel function to modify imageData. I'm using blue for start and black for end. You'll need to adjust for your exact condition but you can use setPixel to allow for direct x and y edits on the imageData.
update
I've included an alternate line method and your line method. There is also an animation that will help you find errors.
function ptIndex(p, w) {
return ((p.x|0) + ((p.y|0) * w)) * 4;
}
function setPixel(p, w, d, rgba) {
var i = ptIndex(p, w);
d[i] = rgba.r;
d[i + 1] = rgba.g;
d[i + 2] = rgba.b;
d[i + 3] = rgba.a;
}
function yourLine(p1, p2, w, d, rgba) {
var cnvs = document.getElementById("Canvas");
var bottomPixel = ptIndex(p1, w);
var topPixel = ptIndex(p2, w)
if (bottomPixel - topPixel > w * 4) //If bottomPixel is to the right of topPixel
{
for (var i = 0; i < ((bottomPixel - w * 4) - topPixel) / 4; i++) {
var index = topPixel + i * 4;
d[index] = rgba.r;
d[index + 1] = rgba.g;
d[index + 2] = rgba.b;
d[index + 3] = rgba.a
}
}
if (bottomPixel - topPixel > w * 4) //If bottomPixel is to the left of topPixel
{
for (var i = 0; i < (topPixel - (bottomPixel - w * 4)) / 4; i++) {
var index = topPixel - i * 4;
d[index] = rgba.r;
d[index + 1] = rgba.g;
d[index + 2] = rgba.b;
d[index + 3] = rgba.a
}
}
}
function drawRandPoints() {
var cnvs = document.getElementById("Canvas");
var cont = cnvs.getContext("2d");
// ghost last draw
cont.fillStyle = "white";
cont.fillRect(0, 0, cnvs.width, cnvs.height);
// get image data
var environment = cont.getImageData(0, 0, cnvs.width, cnvs.height);
var d = environment.data, w = cnvs.width;
// create colors
var black = {
r: 0,
g: 0,
b: 0,
a: 255
};
var red = {
r: 255,
g: 0,
b: 0,
a: 255
};
var blue = {
r: 0,
g: 0,
b: 255,
a: 255
};
var frames = 0;
var p1 = {x: ((cnvs.width / 2|0)), y: 0, sx: 1, sy:0};
var p2 = {x: cnvs.width, y: ((cnvs.height / 2)|0), sx: -1, sy: 0};
function step(p) {
if (p.x > cnvs.width) {
p.x = cnvs.width;
p.sx = 0;
p.sy = 1;
}
if (p.y > cnvs.height) {
p.y = cnvs.height;
p.sy = 0;
p.sx = -1;
}
if (p.x < 0) {
p.x = 0;
p.sx = 0;
p.sy = -1;
}
if (p.y < 0) {
p.y = 0;
p.sy = 0;
p.sx = 1;
}
}
function ani() {
cont.fillStyle = "white";
cont.fillRect(0, 0, cnvs.width, cnvs.height);
environment = cont.getImageData(0, 0, cnvs.width, cnvs.height);
d = environment.data;
step(p1);
step(p2);
var p3 = {
x: cnvs.width - p1.x,
y: cnvs.height - p2.y
};
var p4 = {
x: cnvs.width - p2.x,
y: cnvs.height - p1.y
};
yourLine(p1, p2, w, d, {r:0,g:255,b:0,a:255});
myDrawLine(p1, p2, w, d, red);
drawLineNoAliasing(p3, p4, w, d, blue);
setPixel(p1, w, d, black);
setPixel(p2, w, d, black);
frames %= 12;
p1.x += p1.sx;
p1.y += p1.sy;
p2.x += p2.sx;
p2.y += p2.sy;
// Put the pixel data on the canvas.
cont.putImageData(environment, 0, 0);
requestAnimationFrame(ani);
}
ani();
}
function myDrawLine(p1, p2, w, d, rgba) {
// Get the max length between x or y
var lenX = Math.abs(p1.x - p2.x);
var lenY = Math.abs(p1.y - p2.y);
var len = Math.sqrt(Math.pow(lenX,2) + Math.pow(lenY,2));
// Calculate the step increment between points
var stepX = lenX / len;
var stepY = lenY / len;
// If the first x or y is greater then make step negetive.
if (p2.x < p1.x) stepX *= -1;
if (p2.y < p1.y) stepY *= -1;
// Start at the first point
var x = p1.x;
var y = p1.y;
for (var i = 0; i < len; i++) {
x += stepX;
y += stepY;
// Make a point from new x and y
var p = {
x: x,
y: y
};
// Draw pixel on data
setPixel(p, w, d, rgba);
// reached goal (removes extra pixel)
if (Math.abs(p.x - p2.x) <= 1 && Math.abs(p.y - p2.y) <= 1) {
break;
}
}
// Draw start and end pixels. (might draw over line start and end)
setPixel(p1, w, d, rgba);
setPixel(p2, w, d, rgba);
}
// alternate from http://stackoverflow.com/questions/4261090/html5-canvas-and-anti-aliasing answer
// some helper functions
// finds the distance between points
function DBP(x1, y1, x2, y2) {
return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
}
// finds the angle of (x,y) on a plane from the origin
function getAngle(x, y) {
return Math.atan(y / (x == 0 ? 0.01 : x)) + (x < 0 ? Math.PI : 0);
}
// the function
function drawLineNoAliasing(p1, p2, w, d, rgba) {
var dist = DBP(p1.x, p1.y, p2.x, p2.y); // length of line
var ang = getAngle(p2.x - p1.x, p2.y - p1.y); // angle of line
var cos = Math.cos(ang);
var sin = Math.sin(ang);
for (var i = 0; i < dist; i++) {
// for each point along the line
var pt = {
x: p1.x + cos * i,
y: p1.y + sin * i
};
setPixel(pt, w, d, rgba);
}
}
// end alt
drawRandPoints();
#Canvas {
border: 1px solid red image-rendering: optimizeSpeed;
/* Older versions of FF */
image-rendering: -moz-crisp-edges;
/* FF 6.0+ */
image-rendering: -webkit-optimize-contrast;
/* Safari */
image-rendering: -o-crisp-edges;
/* OS X & Windows Opera (12.02+) */
image-rendering: pixelated;
/* Awesome future-browsers */
image-rendering: optimize-contrast;
/* CSS3 Proposed */
-ms-interpolation-mode: nearest-neighbor;
/* IE */
}
<canvas id="Canvas" width="128" height="64" style="width:320px"></canvas>
I have line segments and points stored in a db. How would I query the db in order to retrieve the all the points that are within a certain distance of multiple line segments.
The purpose is that when the user clicks on a path (road), all the objects that are within a distance from the path should be highlighted.
Thank you.
Update:
Example...
I have a path that goes from (0,0) to (0, 10). The program should find and highlight all objects within x-distance of this path.
Suppose that the x-distance is "2"... then, the program should highlight all objects within the rectangle (0,2)(10,-2). Basically, this is the same as finding all objects with a proximity to the line (not just a single point).
It is easy when the line is horizontal... But I don't know how to solve for all cases, including then the line may be a slope.
Update: The points are stored in a large database, so I cannot check each and every one of them for the proximity. I'm trying to find a way to retrieve only the ones that are close enough without overlapping requests too much... Once they are retrieved, I can refine the search by using the method described in "distance between a point and a line segment". (I think!)
Thanks!
This will give you the distance from point p to line segment v,w. (based on this question: Shortest distance between a point and a line segment). You'll have to run through all your points and calculate the distance to all your line segments to find the ones close enough to the route.
If it's too slow, you'll have to make some kind of simplification that doesn't need square roots.
function distanceToLineSegment(p, v, w)
{
var len2 = dist2(v, w);
if (len2 == 0) return Math.sqrt(dist2(p, v));
var s = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / len2;
if (s < 0) return Math.sqrt(dist2(p, v));
if (s > 1) return Math.sqrt(dist2(p, w));
var i = {x: v.x + s * (w.x - v.x), y: v.y + s * (w.y - v.y)};
return Math.sqrt(dist2(p, i));
function dist2(p, q) {
return Math.pow(p.x - q.x, 2) + Math.pow(p.y - q.y, 2);
}
}
alert(distanceToLineSegment({x:2, y:3}, {x:-1, y:4}, {x:3, y:8}));
This is a somewhat optimized implementation that checks a list of points against a route.
The points to check are stored as an array far[] of points with x and y values and an id string. There is a second, initially empty array close[] into which the points are moved if they are found to be close to the route, so that points aren't checked twice. These two arrays are stored in an object points, so that they can be passed by reference between the functions, instead of constantly being copied. I've also removed the square root functions for efficiency.
Further optimization is probably possible by changing the distance calculation to a coarser approximation (maybe using rectangles) instead of a mathematically correct one.
function isCloseToRoute(points, route, distance) {
var distance2 = Math.pow(distance, 2);
for (var i = 0; i < route.length - 1; i++) {
isCloseToLineSegment(points, route[i], route[i + 1], distance2);
}
function isCloseToLineSegment(points, v, w, distance2) {
for (var i = points.far.length - 1; i >= 0; i--) {
if (distanceToLineSegment2(points.far[i], v, w) <= distance2) {
points.close.push(points.far.splice(i, 1)[0]);
}
}
}
function distanceToLineSegment2(p, v, w) {
var len2 = dist2(v, w);
if (len2 == 0) return dist2(p, v);
var q = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / len2;
if (q < 0) return dist2(p, v);
if (q > 1) return dist2(p, w);
var i = {x: v.x + q * (w.x - v.x), y: v.y + q * (w.y - v.y)};
return dist2(p, i);
function dist2(p, q) {
return Math.pow(p.x - q.x, 2) + Math.pow(p.y - q.y, 2);
}
}
}
var points = {close: [], far: [{x: 1, y: 0, id: "A"},
{x: 2, y: 1, id: "B"},
{x:-1, y: 8, id: "C"},
{x:-3, y: 4, id: "D"}]};
var route = [{x: 0, y: 0}, {x: 1, y: 2}, {x:-1, y: 4}, {x: 2, y: 8}];
isCloseToRoute(points, route, 2);
alert(points.close.length + " points found near route");
for (i in points.close) console.log(points.close[i].id);
If you divide your map into a grid, you can use isCloseToRoute() to check which grid cells are near the route. It will return a list of grid cells which have a key like "6,4"; if you give each point in your database a key that indicates in which grid cells it's located, you can look them up without having to do any math on the coordinates.
You make an input object just like when checking a list of points, fill the far[] array with the center points of the grid cells, and run isCloseToRoute() on it with a distance of (distance + gridSize*sqrt(2)/2).
In the example, the map is a 1000 x 1000 square, divided into 64 grid cells each sized 125 x 125.
function isCloseToRoute(points, route, distance) {
var distance2 = Math.pow(distance, 2);
for (var i = 0; i < route.length - 1; i++) {
isCloseToLineSegment(points, route[i], route[i + 1], distance2);
}
function isCloseToLineSegment(points, v, w, distance2) {
for (var i = points.far.length - 1; i >= 0; i--) {
if (distanceToLineSegment2(points.far[i], v, w) <= distance2) {
points.close.push(points.far.splice(i, 1)[0]);
}
}
}
function distanceToLineSegment2(p, v, w) {
var len2 = dist2(v, w);
if (len2 == 0) return dist2(p, v);
var q = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / len2;
if (q < 0) return dist2(p, v);
if (q > 1) return dist2(p, w);
var i = {x: v.x + q * (w.x - v.x), y: v.y + q * (w.y - v.y)};
return dist2(p, i);
function dist2(p, q) {
return Math.pow(p.x - q.x, 2) + Math.pow(p.y - q.y, 2);
}
}
}
var route = [{x: 210, y: 190}, {x: 820, y: 480}, {x:530, y: 470}, {x: 440, y: 760}];
var distance = 100;
var mapSize = 1000;
var gridSize = 125;
var gridCells = Math.floor(mapSize / gridSize);
var grid = {close: [], far: []};
for (x = 0; x < gridCells; x++) {
for (y = 0; y < gridCells; y++) {
grid.far[y * (gridCells) + x] = {x: (x + 0.5) * gridSize,
y: (y + 0.5) * gridSize,
key: x + "," + y};
}
}
isCloseToRoute(grid, route, distance + 0.707107 * gridSize);
alert(grid.close.length + " grid cells near route");
for (i in grid.close) console.log(grid.close[i].key);
I've optimized isCloseToRoute() a bit more. The example runs a test with 1000 random points checked against a 1000-segment random route.
function isCloseToRoute(points, route, distance) {
var distance2 = Math.pow(distance, 2);
for (var i = 0; i < route.length - 1; i++) {
isCloseToLineSegment(route[i], route[i + 1]);
}
function isCloseToLineSegment(v, w) {
var len2 = distanceToPoint2(v, w);
var lenX = w.x - v.x, lenY = w.y - v.y;
for (var i = points.far.length - 1; i >= 0; i--) {
if (distanceToLineSegment2(points.far[i]) <= distance2) {
points.near.push(points.far.splice(i, 1)[0]);
}
}
function distanceToLineSegment2(p) {
if (len2 == 0) return distanceToPoint2(p, v); // enable if zero-length segments are possible
var q = ((p.x - v.x) * lenX + (p.y - v.y) * lenY) / len2;
if (q < 0) return distanceToPoint2(p, v);
if (q > 1) return distanceToPoint2(p, w);
var r = {x: v.x + q * lenX, y: v.y + q * lenY};
return distanceToPoint2(p, r);
}
function distanceToPoint2(p, q) {
return Math.pow(p.x - q.x, 2) + Math.pow(p.y - q.y, 2);
}
}
}
// generate random test data
var points = {near: [], far: [{x: 500, y: 500}]};
var route = [{x: 200, y: 200}];
var distance = 100;
for (var i = 1; i < 1000; i++) {
points.far[i] = {x: Math.random() * 1000, y: Math.random() * 1000};
route[i] = {x: route[i - 1].x + 3 * Math.random() - 1, y: route[i - 1].y + 3 * Math.random() - 1};
}
var t = new Date().getTime();
isCloseToRoute(points, route, distance);
t = new Date().getTime() - t;
alert(points.near.length + " points found near route.\n(1000 points checked against 1000 segments in " + t + " ms)");
for (i in points.near) console.log(points.near[i].x + "," + points.near[i].y);