Lens equivalent of evolve or lens to multiple values - javascript

It's my understanding is lenses are functions that contain the means to get and set values.
I have this helper function:
const overEach = uncurryN(3,
fn => lenses =>
lenses.length > 0 ?
compose(...map(flip(over)(fn), lenses)) :
identity
);
in use
const annual = ["yearsPlayed", "age"];
const annualInc = overEach(
inc,
map(lensProp, annual),
);
console.log(
annualInc({
jersey: 148,
age: 10,
yearsPlayed: 2,
id: 3.14159
})
);
The output:
{
jersey: 41,
age: 11,
yearsPlayed: 3,
id: 3.14159
}
This is interesting because (like evolve), I can define how something of a certain shape is meant to change. This is better than evolve because it gives me a clean separation of concern about the shape of my data and what I'm doing to it. This is worse than evolve because it creates an intermediate value that I never use. The more lenses I have, the more intermediate values I create.
{
jersey: 148,
age: 10,
yearsPlayed: 3,
id: 3.14159
}
I'd be curious to know if there's a way to define a lens that points to more than one value. compose(lenseIndices([1,7,9]), lensProp('parents'), lensIndex(0)) Might point to the first parent of three different people.
It seems to me this really should be possible, but I don't know what to search and I'd rather not re-invent the wheel (Especially as I haven't been in the weeds with lenses yet), if it can and also has been done.

I'd be curious to know if there's a way to define a lens that points to more than one value.
The intuition we should have for a "lens" is that it "focuses" on a particular part of a data structure. So really, no. A lens is all about working with something specific. (But see the update below that demonstrates that this something specific does not have to be a single property.)
Ramda's issue #2457 discusses the uses of lenses in greater detail.
I don't think I agree with your interpretation of additional flexibilities your function provides compared to evolve. In fact, if I were to implement it, I would probably do so atop evolve, with something like this:
const {evolve, fromPairs, inc} = R
const overEach = (fn, names) =>
evolve (fromPairs (names .map (name => [name, fn])))
const annualInc = overEach (inc, ["yearsPlayed", "age"])
console .log (annualInc ({
jersey: 148,
age: 10,
yearsPlayed: 2,
id: 3.14159
}))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.js"></script>
And evolve lets you easily choose different functions for different properties, allows you to nest transformations, and is extremely declarative.
overEach simply allows us to apply the same transformation function to many different nodes. This is useful, of course, but seems likely less common than the normal cases of evolve.
Update
I want to clarify something I said above. While lenses focus on a particular part of a data structure, that does not mean that they can only affect one field or property of an object. We need to think of this more holistically. That part can be multiple fields, possibly with subfields. I think this is easiest to describe through an example.
Let's imagine you have your polished box function used to describe a box on the cartesian grid. It has read-only position, width, and height properties, and methods to move it, scale it, list the corners, find the areas And all of these are properly functional, returning new boxes rather than mutating the original. You're pretty happy with this code:
const box = (l, t, w, h) => ({
move: (dx, dy) => box (l += dx, t += dy, w, h),
scale: (dw, dh) => box (l, t, w *= dw, h *= dh),
area: () => w * h,
get position () { return {x: l, y: t} },
get width () { return w},
get height () { return h },
corners: () => [{x: l, y: t}, {x: l + w, y: t}, {x: l + w, y: t + h}, {x: l, y: t + h}],
toString: () => `Box (left: ${l}, top: ${t}, height: ${h}, width: ${w})`
})
But now you want to apply your tools to a new situation, where you have widgets that look like this:
const widget = {
topLeft: {x: 126, y: 202},
bottomRight: {x: 776, y: 682},
borderColor: 'red',
borderWidth: 3,
backgroundUrl: 'http://example.com/img.png',
// ...
}
While the topRight and bottomLeft points are a compatible way of describing the rectangle, you would have to rewrite a pile of code that already handle boxes to deal with these new widgets. Moreover, boxes seem the logical view of the situation. Heights and widths seem much more relevant than the bottom-right corners. Here we can use lenses to deal with the concerns. That is, we can think entirely in boxes, extracting a box from the widget, adjusting the values by adjusting the box. We just need to write a lens to do it:
const boxLens = lens (
({topLeft: {x: x1, y: y1}, bottomRight: {x: x2, y: y2}}) =>
box (x1, y1, x2 - x1, y2 - y1),
({position: {x, y}, width, height}, widget) => ({
...widget,
topLeft: {x, y},
bottomRight: {x: x + width, y: y + height}
})
)
Now we can deal with the position and extent of our widget as though it were described by a box:
view (boxLens, widget) .toString ()
//=> "Box (left: 126, top: 202, height: 480, width: 650)"
view (boxLens, widget) .corners ()
//=> [{x: 126, y: 202}, {x: 776, y: 202}, {x: 776, y: 682}, {x: 126, y: 682}]
set (boxLens, box (200, 150, 1600, 900), widget)
//=> {topLeft: {x: 200, y: 150}, bottomRight: {x: 1800, y: 1050}, borderColor: "red", ...}
over (boxLens, box => box .scale (.5, .5), widget)
//=> {topLeft: {x: 126, y: 202}, bottomRight: {x: 451, y: 442}, borderColor: "red", ...}
const moveWidget = (dx, dy) =>
over(boxLens, box => box .move (dx, dy))
moveWidget (10, 50) (widget)
//=> {topLeft: {x: 136, y: 252}, bottomRight: {x: 786, y: 732}, borderColor: "red", ...}
You can confirm this in the following snippet:
const {lens, view, set, over} = R
const box = (l, t, w, h) => ({
move: (dx, dy) => box (l += dx, t += dy, w, h),
scale: (dw, dh) => box (l, t, w *= dw, h *= dh),
area: () => w * h,
get position () { return {x: l, y: t} },
get width () { return w},
get height () { return h },
corners: () => [{x: l, y: t}, {x: l + w, y: t}, {x: l + w, y: t + h}, {x: l, y: t + h}],
toString: () => `Box (left: ${l}, top: ${t}, height: ${h}, width: ${w})`
})
const boxLens = lens(
({topLeft: {x: x1, y: y1}, bottomRight: {x: x2, y: y2}}) => box (x1, y1, x2 - x1, y2 - y1),
({position: {x, y}, width, height}, widget) => ({
...widget,
topLeft: {x, y},
bottomRight: {x: x + width, y: y + height}
})
)
const widget = {
topLeft: {x: 126, y: 202},
bottomRight: {x: 776, y: 682},
borderColor: 'red',
borderWidth: 3,
backgroundUrl: 'http://example.com/img.png',
// ...
}
console .log (
view (boxLens, widget) .toString ()
)
console .log (
view (boxLens, widget) .corners ()
)
console .log (
set (boxLens, box (200, 150, 1600, 900), widget)
)
console .log (
over (boxLens, box => box .scale (.5, .5), widget)
)
const moveWidget = (dx, dy) =>
over(boxLens, box => box .move (dx, dy))
console .log (
moveWidget (10, 50) (widget)
)
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js"></script>
The Point
This shows that we can use lenses to deal with more than one field at a time, as Mrk Sef's self-answer also explains. But we have to deal with them in some way isomorphic to the original. This is actually a very powerful use of lenses. But this does not imply that we can simply use them to work on arbitrary properties.

What I've Learned So Far
This is probably not a geat idea. The problem is that lenses need to have certain properties to work. One of those properties is this:
view(lens, set(lens, a, store)) ≡ a — If you set a value into the store, and immediately view the value through the lens, you get the value that was set.
If you want a lens to point at multiple values without further restrictions, then that information must be encoded (somehow) in the data structure being altered. If a key is set to an array, that array encodes it's own size. But if setting an array actually corresponds to setting something else, then some subset of that something else must be isomorphic to arrays (grow, shrink, re-order, the whole shebang). So that you can always convert back and forth.
If you're happy with further restrictions you can do a bit more, but the results are lackluster.
Here's a fully functioning (as far as I can see) lens implemention that points to multple properties but restricts the properties you're allowed to set.
const subsetOf = pipe(without, length, equals(0));
const subset = flip(subsetOf);
const lensProps = propNames => lens(
pick(propNames),
(update, data) =>
subset(keys(update), propNames) ?
({ ...data, ...update }) :
call(() => {throw new Error("OH NO! LENS LAW BROKEN!");})
);
In use:
const annualLens = lensProps(["yearsPlayed", "age"]);
const timmy = {
jersey: 148,
age: 10,
yearsPlayed: 2,
id: 3.14159
};
console.log(
"View Timmy's Annual Props: ",
view(annualLens, timmy)
);
console.log(
"Set Timmy's Annual Props: ",
set(annualLens, {yearsPlayed: 100, age: 108}, timmy)
);
console.log(
"Update Timmy's Annual Props: ",
over(annualLens, map(inc), timmy)
);
// Break the LAW
set(annualLens, {newKey: "HelloWorld"}, timmy);
The output:
View Timmy's Annual Props: { age: 10, yearsPlayed: 2 }
Set Timmy's Annual Props: { jersey: 148, age: 108, yearsPlayed: 100, id: 3.14159 }
Update Timmy's Annual Props: { jersey: 148, age: 11, yearsPlayed: 3, id: 3.14159 }
Error: OH NO! LENS LAW BROKEN!
You could imagine writting a version of this that takes pathes instead of names, but that doesn't actually help since then in order to use set, you'd need to know the paths that the lens is expecting to set.
It gets worse though. You can compose these sorts of lenses, but there really isn't any point to it:
compose(lensIndex(0), lensProps(["a","b"]), lensProp("b"))
is the same as
compose(lensIndex(0), lensProp("b"))
So while it doesn't break anything, it quickly becomes profoundly uninteresting. Its only use, really, is as the 'outermost' lens in a composition. Even then it likely must be constrained to be useful.
As an upside, as the outmost lens, it can actually change multiple values without intermediate objects. This isn't great though, as you can use evolve as a function you pass to over and you bake in extra functionality without really losing anything.

Related

Sort Coordinates by Distance to Origin Point - JavaScript

I have random rectangles on canvas stored in an array like so:
var rectangles = [
{x: 10, y: 10},
{x: 40, y: 50},
{x: 1, y: 70},
{x: 80, y: 5},
{x: 30, y: 60}
];
I now want to label these rectangles based on their proximity to the origin point (0, 0).
My first thought was to loop through the x and y axis in different patterns, one example would be:
// 100 is the width and height of the canvas
for(var x = 0; x < 100; x++){
for(var y = 0; y < 100; y++){
// "intersects" loops through the array and returns the matching index or -1 if no match
if(intersects(rectangles, x, y) > -1){
console.log('Rectangle' + (intersects(rectangles, x, y) + 1));
}
}
}
The issue i am having, is that no matter the pattern of the loop the result is not as expected.
My second thought was to draw rectangles to the origin point (seen on the last image) and sort the by the size of the rectangle. However, this (and calculating the line distance for that matter) also did not produce the expected result. This can be seen with the green rectangle, that is very close to X0, but should be last.
For example this should return the same result:
Does anyone know how I can achieve the correct labeling result? Thanks!
Here's how to compare distances of coordinates against the origin and sort them (closest to furthest).
var rectangles = [
{x: 10, y: 10},
{x: 40, y: 50},
{x: 1, y: 70},
{x: 80, y: 5},
{x: 30, y: 60}
];
const sumOfSquares = (x, y) => {
return Math.pow(x, 2) + Math.pow(y, 2);
};
rectangles.sort((a, b) => {
const sumA = sumOfSquares(a.x, a.y);
const sumB = sumOfSquares(b.x, b.y);
return sumA - sumB;
});
console.log(rectangles);

How to create a random ground in matter.js

I am creating the ground of a game using a Perlin noise function. This gives me an array of vertices. I then add a vertex at the front that is {x:0 y: WORLD_HEIGHT} and another at the end of the array that is {x: WORLD_WIDTH y: WORLD_HEIGHT}. I am hoping that will give me a flat base with a random top.
How then do I add this into the matter.js world?
I am trying to create the ground using;
var terrain = Bodies.fromVertices(???, ???, vertexSets, {
isStatic: true
}, true);
but I don't know what to use for the ??? co-ordinates. I think they are supposed to represent the center of the object. However, I don't know what that is because it is noise. What I would like to do is specify the x & y of the first perlin noise vertex.
I am not even sure that given these vertices matter.js is creating a single body or multiple.
Is this the right way to approach it or there another way to do this? I am really struggling with the docs and the examples.
I use Matter.Body.setPosition(body, position) to override the center of mass and put the ground where I want it based on its bounds property.
const engine = Matter.Engine.create();
const render = Matter.Render.create({
element: document.body,
engine: engine,
});
const w = 300;
const h = 300;
const vertices = [
...[...Array(16)].map((_, i) => ({
x: i * 20,
y: ~~(Math.random() * 40),
})),
{x: w, y: 100},
{x: 0, y: 100},
];
const ground = Matter.Bodies.fromVertices(
w - 10, h - 10, // offset by 10 pixels for illustration
vertices,
{isStatic: true},
/* flagInternal =*/ true,
);
Matter.Body.setPosition(ground, {
x: w - ground.bounds.min.x,
y: h - ground.bounds.max.y + 110,
});
const {min: {x}, max: {y}} = ground.bounds;
console.log(x, y); // 10 120
Matter.Composite.add(engine.world, [ground]);
Matter.Render.run(render);
Matter.Runner.run(engine);
<script src="https://cdn.jsdelivr.net/npm/poly-decomp#0.3.0/build/decomp.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.18.0/matter.min.js"></script>
Without setPosition, you can see things jump around if you run this snippet a few times (just to reproduce OP's error with a concrete example):
const engine = Matter.Engine.create();
const render = Matter.Render.create({
element: document.body,
engine: engine,
});
const vertices = [
...[...Array(16)].map((_, i) => ({
x: i * 20,
y: ~~(Math.random() * 40),
})),
{x: 300, y: 100},
{x: 0, y: 100},
];
const ground = Matter.Bodies.fromVertices(
200, 100, vertices,
{isStatic: true},
/* flagInternal =*/ true,
);
Matter.Composite.add(engine.world, [ground]);
Matter.Render.run(render);
Matter.Runner.run(engine);
<script src="https://cdn.jsdelivr.net/npm/poly-decomp#0.3.0/build/decomp.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.18.0/matter.min.js"></script>
I'm not using Perlin noise and there are some internal vertices that aren't properly detected in the above examples, but the result should be the same either way.
should be integers, all width and height of the noise texture. values at those x, y integer places can be floats... no problem.
and same width and height should go to terrain and values at that places will be the height of the terrain.

Is there a way to create a HTML element using an array of coordinates?

Say I have an array of 4 x/y co-ordinates
[{x: 10, y: 5}, {x:10, y:15}, {x:20, y:10}, {x:20, y:20}]
Is there a way to construct a HTML element, so that each of the four corners math the co-ordinates in the array?
I know this is possible using canvas, but I'm stuggling to work out how to go about doing this with HTML elements.
The array will always contain 4 sets of coordinates.
The final shape may be rotated or skewed, but will always be a "valid" shape which can be acheived using CSS transformations.
Assuming you got it in form of [topLeft, bottomLeft, topRight, BottomRight] of the original rectangle, you can try recreate it like this:
const obj1 = [{x: 10, y: 5}, {x:10, y:15}, {x:20, y:10}, {x:20, y:20}];
const obj2 = [{x: 40, y: 80}, {x: 10, y: 160}, {x: 120, y: 80}, {x: 90, y: 160}];
const obj3 = [{x: 200, y: 30}, {x: 150, y: 80}, {x: 250, y: 80}, {x: 200, y: 130}];
function render(obj) {
const skewX = obj[1].x - obj[0].x;
const skewY = obj[2].y - obj[0].y;
let translateX = Math.min(...obj.map(t => t.x));
let translateY = Math.min(...obj.map(t => t.y));
if(skewX<0) translateX -= skewX;
if(skewY<0) translateY -= skewY;
const scaleX = Math.abs(obj[0].x - obj[2].x);
const scaleY = Math.abs(obj[0].y - obj[1].y);
const el = document.createElement('div');
el.style.width = '1px';
el.style.height = '1px';
el.style.backgroundColor = 'blue';
el.style.transformOrigin = 'top left';
el.style.transform = `matrix(${scaleX}, ${skewY}, ${skewX}, ${scaleY}, ${translateX}, ${translateY})`;
document.body.appendChild(el);
}
render(obj1);
render(obj2);
render(obj3);
However, I will recommend you to not store the shapes as its vertices but as it's transformation matrix. (if it's possible, of course)
If you're simply trying to draw shapes without the use of canvas, you could maybe draw SVG shapes by translating the coordinates in your object.
If you want to deform a div, best I can think off top of my head is to make use of CSS transform:matrix, but you'd need to figure out how to translate your x/y for each corner coordinates to scale/skew/translate parameters.
If you're not deforming a div, and simply creating a regular rectangular one, then you should be able to translate your x/y coordinates into top; left; width; height; CSS properties.
Well, no. HTML is a tree-like structured DOM. Although, you can have a DOM with position: absolute (absolute to html) and top: y; left: x, but it does not have any advantage doing it this way, from my perspective.

How to add gravity to multiple bodies 'automatically'?

I know how to add gravity to any given object/element. Just add acceleration Y downwards. But "what if want my hero to fly?" or "what if I want to turn gravity off for one particular object? I'll have to set gravity = 0 which will turn off for everyone obviously. I also thought giving every shape their own 'gravity' variable, but I figured that would be too much and it's probably not how it's done...
How would I go from creating shapes
(Using EaseJS)
function spawnShape(x, y, w, h) {
var shape = new createjs.Shape();
shape.graphics.beginFill("black").drawRect(x, y, w, h);
stage.addChild(shape);
}
spawnShape(20, 250, 600, 30);
spawnShape(200, 150, 5, 5);
stage.update();
to adding gravity "automatically"? (every shape inheriting downwards acceleration) I know there's 2D physics engines made but I want to do/understand this myself, and I did try to use PhysicsJS but failed to do so.. I'll probably be using an engine but for now I want to know how to do this :P
You can create an object:
function Shape(x, y, w, h, gravity){
this.x = x;
this.y = y;
this.w = w;
this.h = h;
this.gravity = gravity;
this.shape = new createjs.Shape();
stage.addChild(shape);
this.draw = function(){
shape.graphics.beginFill("black").drawRect(x, y, w, h);
}
}
Thus, you can call it as:
> x = new Shape(200, 200, 10, 10, 0.5)
Shape {x: 200, y: 200, w: 10, h: 10, gravity: 0.5}
> y = new Shape(400, 100, 50, 100, 0.75)
Shape {x: 400, y: 100, w: 50, h: 100, gravity: 0.75}
> x.gravity = 0
0
> y.gravity
0.75
I haven't worked with EaseJS so the specifics may be inaccurate but the overarching logic will be as demonstrated above.
I think you understand how to add gravity or not add gravity to an object. As you say, it is just adding acceleration Y to the object.
It sounds like you just need to think out your design a little. Let's say you have a module 'gravity.js' that is responsible for applying gravity to an object.
/* gravity.js */
const DEFAULT_GRAVITY_ACCELERATION = 1.0;
function applyGravity(shape) {
const gravity = shape.gravityAcceleration !== undefined ?
shape.gravityAcceleration : DEFAULT_GRAVITY_ACCELERATION;
//Do whatever you normally do to update Y acceleration. Code below
//is just an example.
shape.addYAcceleration(gravity);
}
If you create a shape someplace and want it to be free of gravity, just set the .gravityAcceleration member of that object. BTW, there is nothing special about that "gravityAcceleration" name--it could be whatever you want.
//Assuming spawnShape returns an object.
var superman = spawnShape(20, 250, 600, 30);
superman.gravityAcceleration = 0; //Override the default gravity.
You only need to set the .gravityAcceleration member for shape objects that will defy gravity.

How to calculate weighted center point of 4 points?

If I have 4 points
var x1;
var y1;
var x2;
var y2;
var x3;
var y3;
var x4;
var y4;
that make up a box. So
(x1,y1) is top left
(x2,y2) is top right
(x3,y3) is bottom left
(x4,y4) is bottom right
And then each point has a weight ranging from 0-522. How can I calculate a coordinate (tx,ty) that lies inside the box, where the point is closer to the the place that has the least weight (but taking all weights into account). So for example. if (x3,y3) has weight 0, and the others have weight 522, the (tx,ty) should be (x3,y3). If then (x2,y2) had weight like 400, then (tx,ty) should be move a little closer towards (x2,y2) from (x3,y3).
Does anyone know if there is a formula for this?
Thanks
Creating a minimum, complete, verifiable exmample
You have a little bit of a tricky problem here, but it's really quite fun. There might be better ways to solve it, but I found it most reliable to use Point and Vector data abstractions to model the problem better
I'll start with a really simple data set – the data below can be read (eg) Point D is at cartesian coordinates (1,1) with a weight of 100.
|
|
| B(0,1) #10 D(1,1) #100
|
|
| ? solve weighted average
|
|
| A(0,0) #20 C(1,0) #40
+----------------------------------
Here's how we'll do it
find the unweighted midpoint, m
convert each Point to a Vector of Vector(degrees, magnitude) using m as the origin
add all the Vectors together, vectorSum
divide vectorSum's magnitude by the total magnitude
convert the vector to a point, p
offset p by unweighted midpoint m
Possible JavaScript implementation
I'll go thru the pieces one at a time then there will be a complete runnable example at the bottom.
The Math.atan2, Math.cos, and Math.sin functions we'll be using return answers in radians. That's kind of a bother, so there's a couple helpers in place to work in degrees.
// math
const pythag = (a,b) => Math.sqrt(a * a + b * b)
const rad2deg = rad => rad * 180 / Math.PI
const deg2rad = deg => deg * Math.PI / 180
const atan2 = (y,x) => rad2deg(Math.atan2(y,x))
const cos = x => Math.cos(deg2rad(x))
const sin = x => Math.sin(deg2rad(x))
Now we'll need a way to represent our Point and Point-related functions
// Point
const Point = (x,y) => ({
x,
y,
add: ({x: x2, y: y2}) =>
Point(x + x2, y + y2),
sub: ({x: x2, y: y2}) =>
Point(x - x2, y - y2),
bind: f =>
f(x,y),
inspect: () =>
`Point(${x}, ${y})`
})
Point.origin = Point(0,0)
Point.fromVector = ({a,m}) => Point(m * cos(a), m * sin(a))
And of course the same goes for Vector – strangely enough adding Vectors together is actually easier when you convert them back to their x and y cartesian coordinates. other than that, this code is pretty straightforward
// Vector
const Vector = (a,m) => ({
a,
m,
scale: x =>
Vector(a, m*x),
add: v =>
Vector.fromPoint(Point.fromVector(Vector(a,m)).add(Point.fromVector(v))),
inspect: () =>
`Vector(${a}, ${m})`
})
Vector.zero = Vector(0,0)
Vector.fromPoint = ({x,y}) => Vector(atan2(y,x), pythag(x,y))
Lastly we'll need to represent our data above in JavaScript and create a function which calculates the weighted point. With Point and Vector by our side, this will be a piece of cake
// data
const data = [
[Point(0,0), 20],
[Point(0,1), 10],
[Point(1,1), 100],
[Point(1,0), 40],
]
// calc weighted point
const calcWeightedMidpoint = points => {
let midpoint = calcMidpoint(points)
let totalWeight = points.reduce((acc, [_, weight]) => acc + weight, 0)
let vectorSum = points.reduce((acc, [point, weight]) =>
acc.add(Vector.fromPoint(point.sub(midpoint)).scale(weight/totalWeight)), Vector.zero)
return Point.fromVector(vectorSum).add(midpoint)
}
console.log(calcWeightedMidpoint(data))
// Point(0.9575396819442366, 0.7079725827019256)
Runnable script
// math
const pythag = (a,b) => Math.sqrt(a * a + b * b)
const rad2deg = rad => rad * 180 / Math.PI
const deg2rad = deg => deg * Math.PI / 180
const atan2 = (y,x) => rad2deg(Math.atan2(y,x))
const cos = x => Math.cos(deg2rad(x))
const sin = x => Math.sin(deg2rad(x))
// Point
const Point = (x,y) => ({
x,
y,
add: ({x: x2, y: y2}) =>
Point(x + x2, y + y2),
sub: ({x: x2, y: y2}) =>
Point(x - x2, y - y2),
bind: f =>
f(x,y),
inspect: () =>
`Point(${x}, ${y})`
})
Point.origin = Point(0,0)
Point.fromVector = ({a,m}) => Point(m * cos(a), m * sin(a))
// Vector
const Vector = (a,m) => ({
a,
m,
scale: x =>
Vector(a, m*x),
add: v =>
Vector.fromPoint(Point.fromVector(Vector(a,m)).add(Point.fromVector(v))),
inspect: () =>
`Vector(${a}, ${m})`
})
Vector.zero = Vector(0,0)
Vector.unitFromPoint = ({x,y}) => Vector(atan2(y,x), 1)
Vector.fromPoint = ({x,y}) => Vector(atan2(y,x), pythag(x,y))
// data
const data = [
[Point(0,0), 20],
[Point(0,1), 10],
[Point(1,1), 100],
[Point(1,0), 40],
]
// calc unweighted midpoint
const calcMidpoint = points => {
let count = points.length;
let midpoint = points.reduce((acc, [point, _]) => acc.add(point), Point.origin)
return midpoint.bind((x,y) => Point(x/count, y/count))
}
// calc weighted point
const calcWeightedMidpoint = points => {
let midpoint = calcMidpoint(points)
let totalWeight = points.reduce((acc, [_, weight]) => acc + weight, 0)
let vectorSum = points.reduce((acc, [point, weight]) =>
acc.add(Vector.fromPoint(point.sub(midpoint)).scale(weight/totalWeight)), Vector.zero)
return Point.fromVector(vectorSum).add(midpoint)
}
console.log(calcWeightedMidpoint(data))
// Point(0.9575396819442366, 0.7079725827019256)
Going back to our original visualization, everything looks right!
|
|
| B(0,1) #10 D(1,1) #100
|
|
| * <-- about right here
|
|
|
| A(0,0) #20 C(1,0) #40
+----------------------------------
Checking our work
Using a set of points with equal weighting, we know what the weighted midpoint should be. Let's verify that our two primary functions calcMidpoint and calcWeightedMidpoint are working correctly
const data = [
[Point(0,0), 5],
[Point(0,1), 5],
[Point(1,1), 5],
[Point(1,0), 5],
]
calcMidpoint(data)
// => Point(0.5, 0.5)
calcWeightedMidpoint(data)
// => Point(0.5, 0.5)
Great! Now we'll test to see how some other weights work too. First let's just try all the points but one with a zero weight
const data = [
[Point(0,0), 0],
[Point(0,1), 0],
[Point(1,1), 0],
[Point(1,0), 1],
]
calcWeightedMidpoint(data)
// => Point(1, 0)
Notice if we change that weight to some ridiculous number, it won't matter. Scaling of the vector is based on the point's percentage of weight. If it gets 100% of the weight, it (the point) will not pull the weighted midpoint past (the point) itself
const data = [
[Point(0,0), 0],
[Point(0,1), 0],
[Point(1,1), 0],
[Point(1,0), 1000],
]
calcWeightedMidpoint(data)
// => Point(1, 0)
Lastly, we'll verify one more set to ensure weighting is working correctly – this time we'll have two pairs of points that are equally weighted. The output is exactly what we're expecting
const data = [
[Point(0,0), 0],
[Point(0,1), 0],
[Point(1,1), 500],
[Point(1,0), 500],
]
calcWeightedMidpoint(data)
// => Point(1, 0.5)
Millions of points
Here we will create a huge point cloud of random coordinates with random weights. If points are random and things are working correctly with our function, the answer should be pretty close to Point(0,0)
const RandomWeightedPoint = () => [
Point(Math.random() * 1000 - 500, Math.random() * 1000 - 500),
Math.random() * 1000
]
let data = []
for (let i = 0; i < 1e6; i++)
data[i] = RandomWeightedPoint()
calcWeightedMidpoint(data)
// => Point(0.008690554978970092, -0.08307212085822799)
A++
Assume w1, w2, w3, w4 are the weights.
You can start with this (pseudocode):
M = 522
a = 1
b = 1 / ( (1 - w1/M)^a + (1 - w2/M)^a + (1 - w3/M)^a + (1 - w4/M)^a )
tx = b * (x1*(1-w1/M)^a + x2*(1-w2/M)^a + x3*(1-w3/M)^a + x4*(1-w4/M)^a)
ty = b * (y1*(1-w1/M)^a + y2*(1-w2/M)^a + y3*(1-w3/M)^a + y4*(1-w4/M)^a)
This should approximate the behavior you want to accomplish. For the simplest case set a=1 and your formula will be simpler. You can adjust behavior by changing a.
Make sure you use Math.pow instead of ^ if you use Javascript.
A very simple approach is this:
Convert each point's weight to 522 minus the actual weight.
Multiply each x/y co-ordinate by its adjusted weight.
Sum all multiplied x/y co-ordinates together, and --
Divide by the total adjusted weight of all points to get your adjusted average position.
That should produce a point with a position that is biased proportionally towards the "lightest" points, as described. Assuming that weights are prefixed w, a quick snippet (followed by JSFiddle example) is:
var tx = ((522-w1)*x1 + (522-w2)*x2 + (522-w3)*x3 + (522-w4)*x4) / (2088-(w1+w2+w3+w4));
var ty = ((522-w1)*y1 + (522-w2)*y2 + (522-w3)*y3 + (522-w4)*y4) / (2088-(w1+w2+w3+w4));
JSFiddle example of this
Even though this has already been answered, I feel the one, short code snippet that shows the simplicity of calculating a weighted-average is missing:
function weightedAverage(v1, w1, v2, w2) {
if (w1 === 0) return v2;
if (w2 === 0) return v1;
return ((v1 * w1) + (v2 * w2)) / (w1 + w2);
}
Now, to make this specific to your problem, you have to apply this to your points via a reducer. The reducer makes it a moving average: the value it returns represents the weights of the points it merged.
// point: { x: xCoordinate, y: yCoordinate, w: weight }
function avgPoint(p1, p2) {
return {
x: weightedAverage(p1.x, p1.w, p2.x, p2.w),
x: weightedAverage(p1.x, p1.w, p2.x, p2.w),
w: p1.w + pw.2,
}
}
Now, you can reduce any list of points to get an average coordinate and the weight it represents:
[ /* points */ ].reduce(avgPoint, { x: 0, y: 0, w: 0 })
I hope user naomik doesn't mind, but I used some of their test cases in this runnable example:
function weightedAverage(v1, w1, v2, w2) {
if (w1 === 0) return v2;
if (w2 === 0) return v1;
return ((v1 * w1) + (v2 * w2)) / (w1 + w2);
}
function avgPoint(p1, p2) {
return {
x: weightedAverage(p1.x, p1.w, p2.x, p2.w),
y: weightedAverage(p1.y, p1.w, p2.y, p2.w),
w: p1.w + p2.w,
}
}
function getAvgPoint(arr) {
return arr.reduce(avgPoint, {
x: 0,
y: 0,
w: 0
});
}
const testCases = [
{
data: [
{ x: 0, y: 0, w: 1 },
{ x: 0, y: 1, w: 1 },
{ x: 1, y: 1, w: 1 },
{ x: 1, y: 0, w: 1 },
],
result: { x: 0.5, y: 0.5 }
},
{
data: [
{ x: 0, y: 0, w: 0 },
{ x: 0, y: 1, w: 0 },
{ x: 1, y: 1, w: 500 },
{ x: 1, y: 0, w: 500 },
],
result: { x: 1, y: 0.5 }
}
];
testCases.forEach(c => {
var expected = c.result;
var outcome = getAvgPoint(c.data);
console.log("Expected:", expected.x, ",", expected.y);
console.log("Returned:", outcome.x, ",", outcome.y);
console.log("----");
});
const rndTest = (function() {
const randomWeightedPoint = function() {
return {
x: Math.random() * 1000 - 500,
y: Math.random() * 1000 - 500,
w: Math.random() * 1000
};
};
let data = []
for (let i = 0; i < 1e6; i++)
data[i] = randomWeightedPoint()
return getAvgPoint(data);
}());
console.log("Expected: ~0 , ~0, 500000000")
console.log("Returned:", rndTest.x, ",", rndTest.y, ",", rndTest.w);
.as-console-wrapper {
min-height: 100%;
}

Categories

Resources