Maxpointcount in EllipseSeries - LightningChart JS - javascript

I can save memory by setting max point count in scatter chart by following
const pointz = chart.addPointSeries({ pointShape: PointShape.Circle })
.setName('Kuopio')
.setPointFillStyle(fillStyles[0])
.setPointSize(pointSize)
.setMaxPointCount(10000);
But how do I set it same for EllipseSeries?
I dont see any such method like setMaxPointCount for EllipseSeries - https://www.arction.com/lightningchart-js-api-documentation/v1.3.0/classes/ellipseseries.html#add

The EllipseSeries doesn't support the setMaxPointCount functionality. The series type is not meant to be used with a lot of data and as such it doesn't have some of the optimizations that exists for PointSeries, LineSeries and other more basic series types.
You can manually remove points from the EllipseSeries by calling EllipseFigure.dispose() on each ellipse you want to remove from the EllipseSeries. Calling dispose will free up all resources used for rendering the ellipse and remove all references to the ellipse internally. If you remove all references to the ellipse in out own code after calling dispose, all of the memory used by the ellipse will be released.
let ellipse = ellipseSeries.add({x:0,y:0,radiusX: 10,radiusY:10}) // ellipse is rendered here
ellipse.dispose() // ellipse is no longer rendered but some memory is still used.
ellipse = undefined // last reference to the ellipse was removed, all memory is freed
// Extract required parts from LightningChartJS.
const {
lightningChart,
SolidFill,
SolidLine,
ColorRGBA,
emptyFill,
emptyTick,
FontSettings,
AutoCursorModes,
Animator,
AnimationEasings,
UIDraggingModes,
UIOrigins,
ColorPalettes
} = lcjs
// Custom callback template.
const forEachIn = (object, clbk) => { const obj = {}; for (const a in object) obj[a] = clbk(object[a]); return obj }
// Define colors to configure chart and bubbles.
const colors = {
background: ColorRGBA(255, 255, 255),
graphBackground: ColorRGBA(220, 255, 255),
title: ColorRGBA(0, 100, 0),
subTitle: ColorRGBA(0, 100, 0),
bubbleBorder: ColorRGBA(0, 0, 0),
bubbleFillPalette: ColorPalettes.fullSpectrum(100)
}
// Define font settings.
const fonts = {
title: new FontSettings({
size: 40,
weight: 400
})
}
// Create and subtitle with the same font settings, except font-size.
fonts.subTitle = fonts.title.setSize(20)
// Create solid fill styles for defined colors.
const solidFillStyles = forEachIn(colors, (color) => new SolidFill({ color }))
// Create chart with customized settings
const chart = lightningChart().ChartXY({})
.setBackgroundFillStyle(solidFillStyles.background)
.setChartBackgroundFillStyle(solidFillStyles.graphBackground)
.setTitle('Custom Styled Chart')
.setTitleFont(fonts.title)
.setTitleFillStyle(solidFillStyles.title)
.setTitleMarginTop(6)
.setTitleMarginBottom(0)
.setPadding({ left: 5, right: 5, top: 30, bottom: 30 })
.setAutoCursorMode(AutoCursorModes.disabled)
.setMouseInteractionRectangleZoom(undefined)
.setMouseInteractionRectangleFit(undefined)
.setMouseInteractions(false)
// Get axes.
const axes = {
bottom: chart.getDefaultAxisX(),
left: chart.getDefaultAxisY(),
top: chart.addAxisX(true),
right: chart.addAxisY(true).setChartInteractions(false)
}
chart.addUIElement(undefined, { x: chart.uiScale.x, y: axes.right.scale })
.setPosition({ x: 50, y: 10 })
.setOrigin(UIOrigins.CenterBottom)
.setMargin({ bottom: 10 })
.setText('- With Bubbles -')
.setFont(fonts.subTitle)
.setTextFillStyle(solidFillStyles.subTitle)
.setDraggingMode(UIDraggingModes.notDraggable)
// Axis mutator.
const overrideAxis = (axis) => axis
.setTickStyle(emptyTick)
.setTitleMargin(0)
.setNibStyle(line => line.setFillStyle(emptyFill))
.setMouseInteractions(undefined)
// Override default configurations of axes.
for (const axisPos in axes)
overrideAxis(axes[axisPos]);
[axes.bottom, axes.left].forEach(axis => axis.setInterval(-100, 100).setScrollStrategy(undefined))
const bubblePx = {
x: axes.bottom.scale.getPixelSize(),
y: axes.left.scale.getPixelSize()
}
// Create instance of ellipse series to draw bubbles.
const ellipseSeries = chart.addEllipseSeries()
let bubbleCount = 0
// Handler of dragging bubbles.
const bubbleDragHandler = (figure, event, button, startLocation, delta) => {
const prevDimensions = figure.getDimensions()
figure.setDimensions(Object.assign(prevDimensions, {
x: prevDimensions.x + delta.x * figure.scale.x.getPixelSize(),
y: prevDimensions.y + delta.y * figure.scale.y.getPixelSize()
}))
}
// Create resizeBubble array and sizeArray to store the values separately
const resizeBubble = []
const sizeArray = []
// Create a single bubble to visualize in specific coordinates and specified size.
const addBubble = (pos, size) => {
const radius = size * 10
const borderThickness = 1 + size * 1.0
const color = colors.bubbleFillPalette(Math.round(Math.random() * 99))
const fillStyle = new SolidFill({ color })
const strokeStyle = new SolidLine({ fillStyle: solidFillStyles.bubbleBorder, thickness: borderThickness })
const figure = ellipseSeries.add({
x: pos.x,
y: pos.y,
radiusX: radius * bubblePx.x,
radiusY: radius * bubblePx.y
})
.setFillStyle(fillStyle)
.setStrokeStyle(strokeStyle)
// Make draggable by mouse.
figure.onMouseDrag(bubbleDragHandler)
bubbleCount++
return figure
}
// Create an event to handle the case when user resizes the window, the bubble will be automatically scaled
chart.onResize(() => {
for (let i = 0; i <= bubbleMaxCount - 1; i++) {
const newBubble = resizeBubble[i].getDimensions()
resizeBubble[i].setDimensions({
x: newBubble.x,
y: newBubble.y,
radiusX: axes.bottom.scale.getPixelSize() * sizeArray[i] * 10,
radiusY: axes.left.scale.getPixelSize() * sizeArray[i] * 10
})
}
})
// Create a single bubble to visualize in random coordinates and with random size.
const addRandomBubble = () => {
const pos = {
x: Math.random() * 200 - 100,
y: Math.random() * 200 - 100
}
const size = 1 + Math.random() * 7.0
sizeArray.push(size)
resizeBubble.push(addBubble(pos, size))
}
// Amount of bubbles to render.
const bubbleMaxCount = 100
// Animate bubbles creation.
Animator(() => undefined)(2.5 * 1000, AnimationEasings.ease)([[0, bubbleMaxCount]], ([nextBubbleCount]) => {
while (bubbleCount < nextBubbleCount)
addRandomBubble()
})
// dispose all ellipses that have been added before the timeout expires.
setTimeout(()=>{
for(let i =0; i < resizeBubble.length; i++){
resizeBubble[i].dispose()
}
}, 2000)
<script src="https://unpkg.com/#arction/lcjs#1.3.1/dist/lcjs.iife.js"></script>

Related

simple z-buffer implementation example in JS? [beginner]

I'm looking for a very basic implementation of the Z-buffer, ideally in JS. I am trying to take a look at a very simple code, for example two polygons overlapping, one hiding the other.
I can't find such basic example, while I can find a couple of "well-above my current level and understanding" samples.
Is there a resource you could recommend to get started?
Thank you for your help and recommendations!
A Z-Buffer(also known as depth buffer) is nothing more than a 2D pixel array(think image). Instead of RGB it only stores a single value in each pixel, the distance from the current viewpoint:
// one value for each pixel in our screen
const depthBuffer = new Array(screenWidth * screenHeight);
It augments the color buffer that contains the actual image you present to the user:
// create buffer for color output
const numChannels = 3; // R G B
const colorBuffer = new Array(screenWidth * screenHeight * numChannels);
For every pixel of a shape you draw you check the Z-Buffer to see if there's anything closer to the camera that occludes the current pixel, if so you don't draw it. This way you can draw things in any order and they're still properly occluded on a per pixel level.
Z-Buffering may not only be used in 3D but also in 2D to achieve draw order-independence. Lets say we want to draw a few boxes, this will be our box class:
class Box {
/** #member {Object} position of the box storing x,y,z coordinates */
position;
/** #member {Object} size of the box storing width and height */
size;
/** #member {Object} color of the box given in RGB */
color;
constructor (props) {
this.position = props.position;
this.size = props.size;
this.color = props.color;
}
/**
* Check if given point is in box
* #param {Number} px coordinate of the point
* #param {Number} py coordinate of the point
* #return {Boolean} point in box
*/
pointInBox (px,py) {
return this.position.x < px && this.position.x + this.size.width > px
&& this.position.y < py && this.position.y + this.size.height > py;
}
}
With this class we can now create a few boxes and draw them:
const boxes = [
new Box({
position: { x: 50, y: 50, z: 10 },
size: { width: 50, height: 20 },
color: { r: 255, g: 0, b:0 }
}),
// green box
new Box({
position: { x: 80, y: 30, z: 5 },
size: { width: 10, height: 50 },
color: { r: 0, g: 255, b:0 }
}),
// blue
new Box({
position: { x: 60, y: 55, z: 8 },
size: { width: 50, height: 10 },
color: { r: 0, g: 0, b: 255 }
})
];
With our shapes specified we can now draw them:
for(const box of boxes) {
for(let x = 0; x < screenWidth; x++) {
for(let y = 0; y < screenHeight; y++) {
// check if our pixel is within the box
if (box.pointInBox(x,y)) {
// check if this pixel of our box is covered by something else
// compare depth value in depthbuffer against box position
// this is commonly referred to as "depth-test"
if (depthBuffer[x + y * screenWidth] < box.position.z) {
// something is already closer to the viewpoint than our current primitive, don't draw this pixel:
continue;
}
// we passed the depth test, put our current depth value in the z-buffer
depthBuffer[x + y * screenWidth] = box.position.z;
// put the color in the color buffer, channel by channel
colorBuffer[(x + y * screenWidth)*numChannels + 0] = box.color.r;
colorBuffer[(x + y * screenWidth)*numChannels + 1] = box.color.g;
colorBuffer[(x + y * screenWidth)*numChannels + 2] = box.color.b;
}
}
}
}
Note that this code is exemplary so it's overly verbose and inefficient for the sake of laying out the concept.
const ctx = document.getElementById("output").getContext('2d');
const screenWidth = 200;
const screenHeight = 200;
// one value for each pixel in our screen
const depthBuffer = new Array(screenWidth * screenHeight);
// create buffer for color output
const numChannels = 3; // R G B
const colorBuffer = new Array(screenWidth * screenHeight * numChannels);
/**
* Represents a 2D box
* #class
*/
class Box {
/** #member {Object} position of the box storing x,y,z coordinates */
position;
/** #member {Object} size of the box storing width and height */
size;
/** #member {Object} color of the box given in RGB */
color;
constructor (props) {
this.position = props.position;
this.size = props.size;
this.color = props.color;
}
/**
* Check if given point is in box
* #param {Number} px coordinate of the point
* #param {Number} py coordinate of the point
* #return {Boolean} point in box
*/
pointInBox (px,py) {
return this.position.x < px && this.position.x + this.size.width > px
&& this.position.y < py && this.position.y + this.size.height > py;
}
}
const boxes = [
// red box
new Box({
position: { x: 50, y: 50, z: 10 },
size: { width: 150, height: 50 },
color: { r: 255, g: 0, b:0 }
}),
// green box
new Box({
position: { x: 80, y: 30, z: 5 },
size: { width: 10, height: 150 },
color: { r: 0, g: 255, b:0 }
}),
// blue
new Box({
position: { x: 70, y: 70, z: 8 },
size: { width: 50, height: 40 },
color: { r: 0, g: 0, b: 255 }
})
];
const varyZ = document.getElementById('varyz');
varyZ.onchange = draw;
function draw () {
// clear depth buffer of previous frame
depthBuffer.fill(10);
for(const box of boxes) {
for(let x = 0; x < screenWidth; x++) {
for(let y = 0; y < screenHeight; y++) {
// check if our pixel is within the box
if (box.pointInBox(x,y)) {
// check if this pixel of our box is covered by something else
// compare depth value in depthbuffer against box position
if (depthBuffer[x + y * screenWidth] < box.position.z) {
// something is already closer to the viewpoint that our current primitive, don't draw this pixel:
if (!varyZ.checked) continue;
if (depthBuffer[x + y * screenWidth] < box.position.z + Math.sin((x+y))*Math.cos(x)*5) continue;
}
// we passed the depth test, put our current depth value in the z-buffer
depthBuffer[x + y * screenWidth] = box.position.z;
// put the color in the color buffer, channel by channel
colorBuffer[(x + y * screenWidth)*numChannels + 0] = box.color.r;
colorBuffer[(x + y * screenWidth)*numChannels + 1] = box.color.g;
colorBuffer[(x + y * screenWidth)*numChannels + 2] = box.color.b;
}
}
}
}
// convert to rgba for presentation
const oBuffer = new Uint8ClampedArray(screenWidth*screenHeight*4);
for (let i=0,o=0; i < colorBuffer.length; i+=3,o+=4) {
oBuffer[o]=colorBuffer[i];
oBuffer[o+1]=colorBuffer[i+1];
oBuffer[o+2]=colorBuffer[i+2];
oBuffer[o+3]=255;
}
ctx.putImageData(new ImageData(oBuffer, screenWidth, screenHeight),0,0);
}
document.getElementById('redz').oninput = e=>{boxes[0].position.z=parseInt(e.target.value,10);draw()};
document.getElementById('greenz').oninput = e=>{boxes[1].position.z=parseInt(e.target.value,10);draw()};
document.getElementById('bluez').oninput = e=>{boxes[2].position.z=parseInt(e.target.value,10);draw()};
draw();
canvas {
border:1px solid black;
float:left;
margin-right: 2rem;
}
label {display:block;}
label span {
display:inline-block;
width: 100px;
}
<canvas width="200" height="200" id="output"></canvas>
<label><span>Red Z</span>
<input type="range" min="0" max="10" value="10" id="redz"/>
</label>
<label><span>Green Z</span>
<input type="range" min="0" max="10" value="5" id="greenz"/>
</label>
<label><span>Blue Z</span>
<input type="range" min="0" max="10" value="8" id="bluez"/>
</label>
<label><span>Vary Z Per Pixel</span>
<input type="checkbox" id="varyz"/>
</label>

Scanline fill - how to ensure entire polygon is covered?

I am trying to efficiently "partition" GeoJSON objects into aligned square tiles of any size, such that:
The entire polygon or multi-polygon is covered (there is no area in the polygon that doesn't have a tile covering it).
There is no tile that doesn't cover any area of the polygon.
I have tried using libraries like Turfjs's square grid (https://turfjs.org/docs/#squareGrid), but the mask operation is unreasonably slow - it takes minutes for large areas with a low square size. It also doesn't cover the entire area of the polygon - only the interior.
I am trying to use the scanline fill algorithm to do this, since my research has shown it is incredibly fast and catered to this problem, but it doesn't always cover the entire area - it only covers the interior, and will sometimes leave corners out:
Or leave entire horizontal areas out:
My scanline works like a normal scanline fill, but works based on floats instead of integers. It floors the initial x position such that it will be on a grid line (Math.floor((x - polygonLeftBoundary) / cellWidth) * cellWidth)
Here is my implementation (using TypeScript):
public partitionIntoGrid(polygon, cellSizeKm) {
const bounds = this.getBoundingBox(polygon);
const left = bounds[0];
const cellDistance = this.distanceToDegrees(cellSizeKm);
const grid = [];
if (polygon.geometry.type === 'Polygon') {
polygon = [polygon.geometry.coordinates];
} else {
polygon = polygon.geometry.coordinates;
}
polygon.forEach(poly => {
poly = poly[0];
const edges = poly.reduce((acc, vertex, vertexIndex) => {
acc.push([vertex, poly[vertexIndex === poly.length - 1 ? 0 : vertexIndex + 1]]);
return acc;
}, []);
let edgeTable = edges
.map(edge => {
const x = Math.floor((edge[0][1] < edge[1][1] ? edge[0][0] : edge[1][0]) / cellDistance) * cellDistance;
return {
yMin: Math.min(edge[0][1], edge[1][1]), // minumum y coordinate
yMax: Math.max(edge[0][1], edge[1][1]), // maximum y coordinate
originalX: x,
x, // lower coordinate's x
w: (edge[0][0] - edge[1][0]) / (edge[0][1] - edge[1][1]), // change of edge per y
};
})
.filter(edge => !isNaN(edge.w))
.sort((a, b) => a.yMax - b.yMax);
let activeEdgeTable = [];
let y = edgeTable[0].yMin;
const originalY = y;
while (edgeTable.length || activeEdgeTable.length) {
// move edges from edge table under y into the active edge table
edgeTable = edgeTable.filter(edge => {
if (edge.yMin <= y) {
activeEdgeTable.push(edge);
return false;
}
return true;
});
// remove edges from the active edge table whose yMax is smaller than y
activeEdgeTable = activeEdgeTable.filter(edge => edge.yMax >= y);
// sort active edge table by x
activeEdgeTable = activeEdgeTable.sort((a, b) => a.x - b.x);
// fill pixels between even and odd adjacent pairs of intersections in active edge table
const pairs = Array.from({ length: activeEdgeTable.length / 2 }, (v, k) => [
activeEdgeTable[k * 2], activeEdgeTable[k * 2 + 1],
]);
pairs.forEach(pair => {
for (let x = pair[0].x; x <= pair[1].x; x += cellDistance) {
grid.push(bboxPolygon([
x, // minX
y, // minY
x + cellDistance, // maxX
y + cellDistance, // maxY
]));
}
});
// increment y
y += cellDistance;
// update x for all edges in active edge table
activeEdgeTable.forEach(edge => edge.x += edge.w * cellDistance);
// activeEdgeTable.forEach(edge => edge.x = edge.originalX + Math.floor((edge.w * (y - originalY) / cellDistance)) * cellDistance);
}
});
return grid;
}
I have been attempting to make the addition of the gradient work, but not getting there yet, hence the line is commented:
activeEdgeTable.forEach(edge => edge.x = edge.originalX + Math.floor((edge.w * (y - originalY) / cellDistance)) * cellDistance);

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%;
}

Attract one set of bodies to another set of bodies, but not to other bodies in the same set

I'm looking to implement a planet-like system, whereby you have large, mostly-fixed objects which have lots of smaller objects around them. The idea is that the smaller objects could orbit around the larger objects, but also change which of the larger objects they're orbiting around. I'm attempting to use the newtonian behaviour type on the smaller objects, but it's making those smaller objects try to orbit around everything, rather than just the large objects. Is there a way to define a behaviour on a set of objects (I have them in an array) and have them only be attracted to objects in another set of objects?
To give you a visual idea, this is how it currently looks
(https://i.imgur.com/DqizSFO.png):
I want the orange objects to orbit the blue objects, but I don't want the orange or blue objects to be attracted to other orange or blue objects respectively. Is this possible with PhysicsJS?
Here is my current progress.
To view the script in action, view it on CodePen):
var magnets = []
var particles = []
function rand (from, to) {
return Math.floor(Math.random() * to) + from
}
function generateMagnet (world, renderer) {
var magnet = Physics.body('circle', {
x: rand(0, renderer.width),
y: rand(0, renderer.height),
radius: 50,
mass: 2,
vx: 0.001,
vy: 0.001,
treatment: 'static',
styles: {
fillStyle: '#6c71c4'
}
})
magnets.push(magnet)
world.add(magnet)
}
function generateParticle (world, renderer) {
var particle = Physics.body('circle', {
x: rand(0, renderer.width),
y: rand(0, renderer.height),
vx: rand(-100, 100) / 1000,
vy: rand(-100, 100) / 1000,
mass: 1,
radius: 5,
styles: {
fillStyle: '#cb4b16'
}
})
particles.push(particle)
world.add(particle)
}
Physics(function (world) {
// bounds of the window
var el = document.getElementById('matterContainer')
var viewportBounds = Physics.aabb(0, 0, el.scrollWidth, el.scrollHeight)
var edgeBounce
var renderer
// create a renderer
renderer = Physics.renderer('canvas', {
'el': el
})
el.childNodes[0].style.left = 0
// add the renderer
world.add(renderer)
// render on each step
world.on('step', function () {
world.render()
})
// varrain objects to these bounds
edgeBounce = Physics.behavior('edge-collision-detection', {
aabb: viewportBounds,
restitution: 0.99,
cof: 0.8
})
// resize events
window.addEventListener('resize', function () {
// as of 0.7.0 the renderer will auto resize... so we just take the values from the renderer
viewportBounds = Physics.aabb(0, 0, renderer.width, renderer.height)
// update the boundaries
edgeBounce.setAABB(viewportBounds)
}, true)
for (var i = 0; i < 5; i++) generateMagnet(world, renderer)
for (var i = 0; i < 100; i++) generateParticle(world, renderer)
// add things to the world
var newton = Physics.behavior('newtonian', { strength: 0.05 })
// newton.applyTo(particles)
var attractor = Physics.behavior('attractor', {
order: 0,
strength: 0.1
}).applyTo(magnets);
world.add([
Physics.behavior('body-impulse-response'),
newton,
edgeBounce
])
// subscribe to ticker to advance the simulation
Physics.util.ticker.on(function (time) {
world.step(time)
})
})
<script src="http://wellcaffeinated.net/PhysicsJS/assets/scripts/vendor/physicsjs-current/physicsjs-full.min.js"></script>
<section id="matterContainer" class="sct-HomepageHero pt-0 pb-0"></section>
<style>
.sct-HomepageHero.pt-0.pb-0 {
min-height: 600px;
padding-top: 0;
padding-bottom: 0;
}
</style>

JQuery Spot of light with Sparkles, How to achieve it, is there something ready similar?

I have t his effect on this website
http://www.immersive-garden.com/
there's this point of light sparking, on hover you get the background, I want something similar without using flash
this is the script I'm using right now
/*
Particle Emitter JavaScript Library
Version 0.3
by Erik Friend
Creates a circular particle emitter of specified radius centered and offset at specified screen location. Particles appear outside of emitter and travel outward at specified velocity while fading until disappearing in specified decay time. Particle size is specified in pixels. Particles reduce in size toward 1px as they decay. A custom image(s) may be used to represent particles. Multiple images will be cycled randomly to create a mix of particle types.
example:
var emitter = new particle_emitter({
image: ['resources/particle.white.gif', 'resources/particle.black.gif'],
center: ['50%', '50%'], offset: [0, 0], radius: 0,
size: 6, velocity: 40, decay: 1000, rate: 10
}).start();
*/
particle_emitter = function (opts) {
// DEFAULT VALUES
var defaults = {
center: ['50%', '50%'], // center of emitter (x / y coordinates)
offset: [0, 0], // offset emitter relative to center
radius: 0, // radius of emitter circle
image: 'particle.gif', // image or array of images to use as particles
size: 1, // particle diameter in pixels
velocity: 10, // particle speed in pixels per second
decay: 500, // evaporation rate in milliseconds
rate: 10 // emission rate in particles per second
};
// PASSED PARAMETER VALUES
var _options = $.extend({}, defaults, opts);
// CONSTRUCTOR
var _timer, _margin, _distance, _interval, _is_chrome = false;
(function () {
// Detect Google Chrome to avoid alpha transparency clipping bug when adjusting opacity
if (navigator.userAgent.indexOf('Chrome') >= 0) _is_chrome = true;
// Convert particle size into emitter surface margin (particles appear outside of emitter)
_margin = _options.size / 2;
// Convert emission velocity into distance traveled
_distance = _options.velocity * (_options.decay / 1000);
// Convert emission rate into callback interval
_interval = 1000 / _options.rate;
})();
// PRIVATE METHODS
var _sparkle = function () {
// Pick a random angle and convert to radians
var rads = (Math.random() * 360) * (Math.PI / 180);
// Starting coordinates
var sx = parseInt((Math.cos(rads) * (_options.radius + _margin)) + _options.offset[0] - _margin);
var sy = parseInt((Math.sin(rads) * (_options.radius + _margin)) + _options.offset[1] - _margin);
// Ending Coordinates
var ex = parseInt((Math.cos(rads) * (_options.radius + _distance + _margin + 0.5)) + _options.offset[0] - 0.5);
var ey = parseInt((Math.sin(rads) * (_options.radius + _distance + _margin + 0.5)) + _options.offset[1] - 0.5);
// Pick from available particle images
var image;
if (typeof(_options.image) == 'object') image = _options.image[Math.floor(Math.random() * _options.image.length)];
else image = _options.image;
// Attach sparkle to page, then animate movement and evaporation
var s = $('<img>')
.attr('src', image)
.css({
zIndex: 10,
position: 'absolute',
width: _options.size + 'px',
height: _options.size + 'px',
left: _options.center[0],
top: _options.center[1],
marginLeft: sx + 'px',
marginTop: sy + 'px'
})
.appendTo('body')
.animate({
width: '1px',
height: '1px',
marginLeft: ex + 'px',
marginTop: ey + 'px',
opacity: _is_chrome ? 1 : 0
}, _options.decay, 'linear', function () { $(this).remove(); });
// Spawn another sparkle
_timer = setTimeout(function () { _sparkle(); }, _interval);
};
// PUBLIC INTERFACE
// This is what gets returned by "new particle_emitter();"
// Everything above this point behaves as private thanks to closure
return {
start:function () {
clearTimeout(_timer);
_timer = setTimeout(function () { _sparkle(); }, 0);
return(this);
},
stop:function () {
clearTimeout(_timer);
return(this);
},
centerTo:function (x, y) {
_options.center[0] = x;
_options.center[1] = y;
},
offsetTo:function (x, y) {
if ((typeof(x) == 'number') && (typeof(y) == 'number')) {
_options.center[0] = x;
_options.center[1] = y;
}
}
}
};
you probably need something like this: http://www.realcombiz.com/2012/09/customize-blackquote-with-light-bulb.html

Categories

Resources