In LightningChartJS, is there a way to do a drag zoom where the zoom for X and Y is always equal? - lightningchart

My problem is when I drag rectangle zoom for example (gyazo gif), the X axis is wider than the Y. In effect, the X axis is zoomed in more details than the Y and the visual of the graph looks different from the original graph.
https://gyazo.com/749db917465a8037b7c5f21792f572ce
I am looking for a way where if i zoom rectangle drag, the function is similar to zooming via mousewheel where x and y zoom feels equal.

here you will find an example how you can perform custom zoom rect instead of default and keep the ratio of the axes.
// Extract required parts from LightningChartJS.
const {
ColorRGBA,
ColorHEX,
emptyFill,
SolidFill,
SolidLine,
translatePoint,
_point,
lightningChart
} = lcjs;
// Import data-generator from 'xydata'-library.
const {
createProgressiveTraceGenerator
} = xydata
const chart = lightningChart()
.ChartXY()
// Disable default chart interactions with left mouse button.
// .setMouseInteractionRectangleFit(false)
.setMouseInteractionRectangleZoom(false)
.setTitleFillStyle(emptyFill)
// generate data and creating the series
const series = chart.addLineSeries().setStrokeStyle(
new SolidLine({
fillStyle: new SolidFill({ color: ColorHEX('#fff') }),
thickness: 2,
}),
)
// generate data and create series
createProgressiveTraceGenerator()
.setNumberOfPoints(200)
.generate()
.toPromise()
.then((data) => {
return series.add(data)
})
// create zooming rect and dispose it
const rect = chart
.addRectangleSeries()
.add({
x: 0,
y: 0,
width: 0,
height: 0,
})
.setFillStyle(new SolidFill({ color: ColorRGBA(255, 255, 255, 30) }))
.setStrokeStyle(
new SolidLine({
thickness: 2,
fillStyle: new SolidFill({ color: ColorRGBA(255, 255, 255, 255) }),
}),
)
.dispose()
// om Mouse drag restor rectange and set position and coordinates
chart.onSeriesBackgroundMouseDrag((obj, event, button, startLocation, delta) => {
if (button !== 0) return
const startLocationOnScale = translatePoint(
chart.engine.clientLocation2Engine(startLocation.x, startLocation.y),
chart.engine.scale,
series.scale,
)
const curLocationOnScale = translatePoint(chart.engine.clientLocation2Engine(event.x, event.y), chart.engine.scale, series.scale)
const x = Math.abs(series.getBoundaries().min.x) + Math.abs(series.getBoundaries().max.x)
const y = Math.abs(series.getBoundaries().min.y) + Math.abs(series.getBoundaries().max.y)
const ratio = x / y
const width = Math.abs(curLocationOnScale.x - startLocationOnScale.x)
const height = Math.abs(curLocationOnScale.y - startLocationOnScale.y)
const heightDirection = curLocationOnScale.y - startLocationOnScale.y // check direction of rect
// check for mouse direction to prevet fit and zoom conflict
if (curLocationOnScale.x > startLocationOnScale.x) {
rect.setDimensions({
x: startLocationOnScale.x,
y: startLocationOnScale.y,
width: width > height * ratio ? width : height * ratio,
height: width > height * ratio ? (heightDirection > 0 ? width : -width) / ratio : heightDirection,
}).restore()
} else {
// prevent phantom rectangle if you change zoom to fit during the dragging
rect.setDimensions({
x: 0,
y: 0,
width: 0,
height: 0,
}).dispose()
}
})
// on mouse drag stop dispose rect and zoom relative to its dimensions
chart.onSeriesBackgroundMouseDragStop((_, event, button, startLocation) => {
if (button !== 0) return
const rectZooom = rect.getDimensionsPositionAndSize()
if (rectZooom.width !== 0) {
chart.getDefaultAxisX().setInterval(rectZooom.x, (rectZooom.x + rectZooom.width), true, true)
if(rectZooom.height > 0){
chart.getDefaultAxisY().setInterval(rectZooom.y, (rectZooom.y + Math.abs(rectZooom.height)), true, true)
}else{
chart.getDefaultAxisY().setInterval((rectZooom.y - Math.abs(rectZooom.height)),rectZooom.y, true, true)
}
}
rect.setDimensions({
x: 0,
y: 0,
width: 0,
height: 0,
}).dispose()
})
<script src="https://unpkg.com/#arction/xydata#1.4.0/dist/xydata.iife.js"></script>
<script src="https://unpkg.com/#arction/lcjs#3.0.0/dist/lcjs.iife.js"></script>

Related

How to exclude code from running inside requestAnimationFrame

Hi all, in a nextjs app Im using requestAnimationFrame and matter.js to animate shapes created using paper.js. The shapes are created with a javascript Class inside a forEach loop. Each shape (RegularPolygon) has a random number of sides. When I run the code, Matter.js takes care of the physics but the logic to create the random sides also animates creating a cool (but undesirable strobing effect) of continuously randomising the number of sides of the RegularPolygon. I'm looking for a way of creating the shapes with random sides, once, and then animating them. Thanks in advance.
link to netlify site
import { Engine, Render, World, Bodies } from 'matter-js';
import { useEffect, useRef } from 'react';
import { paper } from 'paper';
const random = (min, max) => {
return Math.random() * min + Math.random() * max + min;
};
function Canvas(props) {
const scene = useRef();
const engine = useRef(Engine.create());
const paperRef = useRef();
const raf = useRef();
console.log(random);
useEffect(() => {
const time = 0;
window.addEventListener('load', () => {
paper.setup(paperRef.current);
const cw = document.body.clientWidth;
const ch = document.body.clientHeight;
const render = Render.create({
element: scene.current,
engine: engine.current,
options: {
width: cw,
height: ch,
wireframes: false,
background: 'transparent',
},
});
World.add(engine.current.world, [
Bodies.rectangle(cw / 2, -10, cw, 20, {
isStatic: true,
}),
Bodies.rectangle(-10, ch / 2, 20, ch, {
isStatic: true,
fillStyle: '#F35e66',
}),
Bodies.rectangle(cw / 2, ch + 10, cw, 20, {
isStatic: true,
fillStyle: '#F35e66',
}),
Bodies.rectangle(cw + 10, ch / 2, 20, ch, {
isStatic: true,
fillStyle: '#F35e66',
}),
]);
Engine.run(engine.current);
Render.run(render);
const balls = [];
for (let i = 0; i < 40; i++) {
balls.push(
Bodies.circle(Math.random() * cw, 0, 80, {
density: Math.random(),
friction: 0.01,
frictionAir: 0.00001,
restitution: 0.8,
render: {
fillStyle: '#F35e66',
strokeStyle: 'black',
lineWidth: 1,
},
})
);
}
var ball = Bodies.circle(0, 0, 20, {
density: 0.04,
friction: 0.01,
frictionAir: 0.00001,
restitution: 0.8,
render: {
fillStyle: '#F35e66',
strokeStyle: 'black',
lineWidth: 1,
},
});
for (let i = 0; i < balls.length; i++) {
World.add(engine.current.world, [balls[i]]);
}
const shapes = [];
for (let i = 0; i < balls.length; i++) {
shapes.push(
new paper.Path.RegularPolygon({
position: new paper.Point([
balls[i].position.x,
balls[i].position.y,
]),
sides: Math.floor(random(0, 8)),
radius: 100,
fillColor: 'tomato',
})
);
}
class Shape {
constructor(x, y, angle) {
this.angle = angle;
this.x = x;
this.y = y;
this.draw();
this.shape = new paper.Path.RegularPolygon({
position: new paper.Point([this.x, this.y]),
sides: Math.floor(random(0, 8)),
radius: 100,
fillColor: 'tomato',
});
}
draw() {
// this.shape.rotate(this.angle);
}
}
console.log(ball);
const callback = () => {
// paper.view.update();
paper.project.clear();
balls.forEach((ball) => {
new Shape(ball.position.x, ball.position.y, ball.angle);
// let shape = new paper.Path.RegularPolygon({
// position: new paper.Point([
// ball.position.x,
// ball.position.y,
// ]),
// sides: Math.floor(Math.random() * 8),
// radius: 100,
// fillColor: 'tomato',
// });
// shape.rotate(ball.angle);
});
const shape = new paper.Path.RegularPolygon({
position: new paper.Point([
ball.position.x,
ball.position.y,
]),
sides: 5,
radius: 100,
fillColor: 'tomato',
});
shape.rotate(ball.angle);
raf.current = requestAnimationFrame(callback);
};
raf.current = requestAnimationFrame(callback);
return () => {
cancelAnimationFrame(raf.current);
Render.stop(render);
World.clear(engine.current.world);
Engine.clear(engine.current);
render.canvas.remove();
render.canvas = null;
render.context = null;
render.textures = {};
};
});
}, []);
return (
<div className=''>
<div className='paper'>
<canvas
resize='true'
ref={paperRef}
style={{
width: '100%',
height: '100%',
position: 'fixed',
top: 0,
left: 0,
}}
/>
</div>
<div
ref={scene}
style={{
display: 'none',
width: '100%',
height: '100%',
position: 'fixed',
top: 0,
left: 0,
}}
/>
</div>
);
}
export default Canvas;

Evenly distribute a given number of segments along an existing path

without seeing the codepen it is tricky to explain my situation, but here goes. I'm creating some paths by getting pathData using opentype.js. I am then placing random shapes at the position of the path's segment's points. Because of the nature of a font's paths some paths have far more segments than others for example '1' has way fewer segments thant '0'. I would like to average out the number of segments along each path so that when I add the shapes they look a consistent number of segments. Thanks in advance.
Is it possible to evenly distribute a given number of segments along an existing path?
Here is a link to the Codepen
paper.install(window);
const minMax = (min, max) => {
return Math.floor(Math.random() * max + min);
};
window.onload = () => {
paper.setup("canvas");
let pathData;
const font = opentype.load(
"https://assets.codepen.io/1070/pphatton-ultralight-webfont.woff",
(err, font) => {
if (err) {
console.log(err);
} else {
class Doughnut {
constructor(x, y) {
this.x = x;
this.y = y;
this.shape = new paper.Path.RegularPolygon({
position: [this.x, this.y],
sides: minMax(3, 8),
radius: minMax(6, 12),
fillColor: "black"
});
}
// makeShape(){
// this.shape
// }
}
pathData = font.getPath("100", 0, 600, 600).toSVG();
// const rect = new paper.Path.Rectangle({
// point: [80, 25],
// size: [300, 200],
// fillColor: "black"
// });
const number = new paper.Path(pathData);
number.selected = true;
// number.flatten(10);
const amount = 50;
const length = number.length
const points = [];
const segments = number.segments;
number.fitBounds(paper.view.bounds);
for(let i = 0; i < amount; i++){
const offset = i / amount * length
const point = number.getPointAt(offset)
new Doughnut(point.x, point.y);
}
segments.forEach((seg) => {
points.push(number.getPointAt(seg));
});
points.forEach((point) => {
console.log(point);
new Doughnut(point.x, point.y);
});
number.reduce();
}
}
);
const shapes = [];
class Doughnut {
constructor(x, y) {
this.x = x;
this.y = y;
this.shape = new paper.Path.RegularPolygon({
position: [this.x, this.y],
sides: minMax(3, 8),
radius: minMax(6, 12),
fillColor: "black"
});
}
// makeShape(){
// this.shape
// }
}
// for (let i = 0; i < 10; i++) {
// shapes.push(new Doughnut(minMax(100, 500), minMax(100, 500)));
// }
// console.log(shapes)
// shapes.makeShape()
};
The path.divideAt() method can help you greatly.
What is tricky in your case is that, in order to preserve the path appearance, you can't move the existing segments. So you'll have to find a way to only add segment where it is needed.
Otherwise, here's a simple sketch demonstrating a possible solution. It should get you on the track to find a solution more specific to your use case.
const circle = new Path.Circle({
center: [0, 0],
radius: 75,
selected: true
});
const rectangle = new Path.Rectangle({
from: [0, 0],
to: [200, 100],
selected: true
});
rectangle.position = circle.position + [circle.bounds.width + rectangle.bounds.width, 0];
const cloneAndAddSegments = (item) => {
const clone = item.clone().translate(0, 200);
const length = clone.length;
const step = 20;
const iterations = Math.floor(length / step);
for (let i = 1; i <= iterations; i++) {
const offset = i * step;
clone.divideAt(offset);
}
return clone;
};
const circleClone = cloneAndAddSegments(circle);
const rectangleClone = cloneAndAddSegments(rectangle);
const showSegments = (item) => {
item.segments.forEach(({ point }) => new Path.Circle({
center: point,
radius: 5,
fillColor: 'orange'
}))
}
showSegments(circle);
showSegments(rectangle);
showSegments(circleClone);
showSegments(rectangleClone);
project.activeLayer.fitBounds(view.bounds.scale(0.8));

How to get bounding box that contains all items in Paper.js?

How can we get the bounding Rectangle that contains all items in the project?
Currently I'm calculating them one by one:
calc-bounds = (scope) ->
fit = {}
for layer in scope.project.layers
for item in layer.children
for <[ top left ]>
if item.bounds[..] < fit[..] or not fit[..]?
fit[..] = item.bounds[..]
for <[ bottom right ]>
if item.bounds[..] > fit[..] or not fit[..]?
fit[..] = item.bounds[..]
#console.log "fit bounds: ", fit
top-left = new scope.Point fit.left, fit.top
bottom-right = new scope.Point fit.right, fit.bottom
new scope.Rectangle top-left, bottom-right
Rationale
A "Fit all" function that sets project.center and project.zoom will need this calculation.
You can just unite (merge) all the bounds from all elements.
This will return a Rectangle that tightly fits all the elements, a.k.a a Bounding Box.
Here's a Sketch.
And here's the code:
var items = [
new Path.Circle({
position: new Point(100, 200),
radius: 50,
fillColor: 'blue'
}),
new Path.Circle({
position: new Point(200, 70),
radius: 50,
fillColor: 'purple'
}),
new Path.Circle({
position: new Point(400, 100),
radius: 50,
fillColor: 'magenta'
})
]
// Unite all bounds from all items.
var unitedBounds = items.reduce((bbox, item) => {
return !bbox ? item.bounds : bbox.unite(item.bounds)
}, null)
// Draw the united bounds.
var bbox = new Path.Rectangle(unitedBounds)
bbox.strokeColor = 'black'
You can use the layer bounds or group the element you want to bound instead of doing the reduce trick from #nicholaswmin
Here is a sketch with both solutions
const items = [
new Path.Circle({
position: new Point(100, 200),
radius: 50,
fillColor: 'blue'
}),
new Path.Circle({
position: new Point(200, 70),
radius: 50,
fillColor: 'purple'
}),
new Path.Circle({
position: new Point(400, 100),
radius: 50,
fillColor: 'magenta'
})
]
// Use the layer bounds
const layerBounds = project.activeLayer.bounds
// Group all needed items and use the bounds
const groupBounds = (new Group(items)).bounds
// Draw the bounds.
const bbox = new Path.Rectangle(layerBounds)
const bbox2 = new Path.Rectangle(groupBounds)
bbox.strokeWidth = 3
bbox.strokeColor = 'blue'
bbox2.strokeWidth = 6
bbox2.strokeColor = 'red'
bbox2.dashArray = [10, 10]

wrong border coords on draw a polygon with the fabric.js

i try to draw a polygon with the mouse , and i have found that example on jsfiddle: http://jsfiddle.net/Kienz/ujefxh7w/.
the problem is that when we finish the draw and try to selet the object, the borders are outside of the shape.
can we fixe that or is it a fabric.js bug?
as we can also see on the official fabricjs.com/ page, on the front page examples, the free hand drawings are also out of the border frame.
// initialize fabric canvas and assign to global windows object for debug
var canvas = window._canvas = new fabric.Canvas('c');
// Do some initializing stuff
fabric.Object.prototype.set({
transparentCorners: false,
cornerColor: 'rgba(102,153,255,0.5)',
cornerSize: 12,
padding: 7
});
// ADD YOUR CODE HERE
var mode = "add",
currentShape;
canvas.observe("mouse:move", function (event) {
var pos = canvas.getPointer(event.e);
if (mode === "edit" && currentShape) {
var points = currentShape.get("points");
points[points.length - 1].x = pos.x - currentShape.get("left");
points[points.length - 1].y = pos.y - currentShape.get("top");
currentShape.set({
points: points
});
canvas.renderAll();
}
});
canvas.observe("mouse:down", function (event) {
var pos = canvas.getPointer(event.e);
if (mode === "add") {
var polygon = new fabric.Polygon([{
x: pos.x,
y: pos.y
}, {
x: pos.x + 0.5,
y: pos.y + 0.5
}], {
fill: 'blue',
opacity: 0.5,
selectable: false
});
currentShape = polygon;
canvas.add(currentShape);
mode = "edit";
} else if (mode === "edit" && currentShape && currentShape.type === "polygon") {
var points = currentShape.get("points");
points.push({
x: pos.x - currentShape.get("left"),
y: pos.y - currentShape.get("top")
});
currentShape.set({
points: points
});
canvas.renderAll();
}
});
fabric.util.addListener(window, 'keyup', function (e) {
if (e.keyCode === 27) {
if (mode === 'edit' || mode === 'add') {
mode = 'normal';
currentShape.set({
selectable: true
});
currentShape._calcDimensions(false);
currentShape.setCoords();
} else {
mode = 'add';
}
currentShape = null;
}
canvas.renderAll();
})
THE PROBLEM
_calcDimensions() calculates width, height, minX and minY of the polygon, so as you can see with this fiddle http://jsfiddle.net/ujefxh7w/90/ the centerpoint of our polygon (that is calculated using width and height) will be changed after calling _calcDimensions() (before this, the centerpoint's value was equal to the top-left point because width and height were zero).
However we inserted all points to the polygon by subtracting the left-top position to them, but after calling _calcDimensions() those points will be rendered by starting from the "new" (correct) centerpoint, as you can see from your fiddle.
Moreover we have to handle minX and minY offsets introduced by _calcDimensions(). If we don't do this you can manage to draw some shapes with some area of out the bounds. (e.g. draw a triangle with those coordinates [(100,100),(150,50),(200,200)], the second point will be out of bound).
THE SOLUTION
We have to
add minX and minY offset to the left-top position of our shape and subtracting them from the new points's values
recalculate points's coordinates by using the actual centerpoint and the left-top position of our polygon
http://jsfiddle.net/ujefxh7w/115/
fabric.util.addListener(window, 'keyup', function (e) {
if (e.keyCode === 27) {
if (mode === 'edit' || mode === 'add') {
mode = 'normal';
// remove last useless point
var points = currentShape.get("points");
points.pop();
currentShape.set({points: points});
// call helpers
currentShape._calcDimensions();
currentShape.setCoords();
// adjust shape position by using minX and minY offsets
var minx = currentShape.get("minX");
var miny = currentShape.get("minY");
currentShape.set({
left: currentShape.get("left") + minx,
top: currentShape.get("top") + miny
});
// adjust points coordinates by
// 1- subtracting the center point coords
// 2- adding the left-top coords
// 3- subtracting minX and minY offsets
var pCenter = currentShape.getCenterPoint();
var l = currentShape.get("left");
var t = currentShape.get("top");
var adjPoints = currentShape.get("points").map(function(p) {
return {
x: p.x - pCenter.x + l - minx,
y: p.y - pCenter.y + t - miny
};
});
currentShape.set({
points: adjPoints,
selectable: true
});
canvas.setActiveObject(currentShape);
canvas.renderAll();
} else {
mode = 'add';
}
currentShape = null;
}
});

famo.us lightbox demo transition

I'm trying to get a transition that is similar to the lightbox demo that famous has put out. I have a grid layout, when I click a surface in the grid, Id like to have the surface transition, grow in size and be centered in the browser window.
--edit
Here is the demo, what I would like to nail is the flyout of the clicked image from its location to the center of the screen. http://demo.famo.us/lightbox/
I have the following code that I've been using as a basis. http://codepen.io/heyimlance/pen/JooQMX
var Engine = famous.core.Engine;
var Surface = famous.core.Surface;
var GridLayout = famous.views.GridLayout;
var StateModifier = famous.modifiers.StateModifier;
var Transform = famous.core.Transform;
var RenderNode = famous.core.RenderNode;
var Easing = famous.transitions.Easing;
var mainContext = Engine.createContext();
var grid = new GridLayout({
dimensions: [8, 8],
});
var surfaces = [];
grid.sequenceFrom(surfaces);
function newSurface(id) {
var surface = new Surface({
content: id + 1,
properties: {
backgroundColor: "hsl(" + (id * 70 / 64) + ", 60%, 70%)",
lineHeight: '50px',
textAlign: 'center'
}
});
var smod = new StateModifier({
size: [50,50],
transform: Transform.translate(0,0,1),
origin: [.5,.5]
});
var rnode = new RenderNode();
rnode.add(smod).add(surface);
surfaces.push(rnode);
surface.on('click', function() {
console.log(smod)
var zpos = (this.up || this.up == undefined) ? 0 : -180;
if (!zpos) {
this.up = false;
smod.setTransform(Transform.translate(0,0,2000), { curve:Easing.outElastic, duration: 1000 })
gridModifier.setTransform(Transform.translate(0,0,-2000), { curve:Easing.outElastic, duration: 500 })
} else {
this.up = true;
gridModifier.setTransform(Transform.translate(0,0,0), { curve:Easing.outElastic, duration: 400 })
smod.setTransform(Transform.translate(0,0,0), { curve:Easing.outElastic, duration: 1000 })
}
});
}
for(var i = 0; i < 64; i++) {
newSurface(i);
}
var gridModifier = new StateModifier({
size: [400, 400],
align: [.5, .5],
origin: [.5, .5],
transform : Transform.translate(0,0,0),
});
var gridRotate = new StateModifier({
transform : Transform.rotate(0,0,0),
});
mainContext.add(gridModifier).add(grid);
mainContext.setPerspective(1000);
Using your code, I made a few changes to use the Lightbox render contoller at the time of the click. Not sure what transition you would like for the grid and surface, this should give you options to transition as you like.
Here is a codepen of the example
The code:
var Engine = famous.core.Engine;
var Surface = famous.core.Surface;
var GridLayout = famous.views.GridLayout;
var StateModifier = famous.modifiers.StateModifier;
var Transform = famous.core.Transform;
var RenderNode = famous.core.RenderNode;
var RenderController = famous.views.RenderController;
var Lightbox = famous.views.Lightbox;
var Easing = famous.transitions.Easing;
var mainContext = Engine.createContext();
var grid = new GridLayout({
dimensions: [8, 8],
});
var surfaces = [];
var showing;
grid.sequenceFrom(surfaces);
var cmod = new StateModifier({
origin: [0.5, 0.5],
align: [0.5, 0.5]
});
var controller = new Lightbox({
inTransition: true,
outTransition: false,
overlap: true
});
controller.hide();
function newSurface(id) {
var surface = new Surface({
size: [undefined, undefined],
content: id + 1,
properties: {
backgroundColor: "hsl(" + (id * 70 / 64) + ", 60%, 70%)",
lineHeight: '50px',
textAlign: 'center',
cursor: 'pointer'
}
});
surface._smod = new StateModifier({
size: [420,420],
origin: [0.5, 0.5],
align: [0.5, 0.5]
});
surface._rnode = new RenderNode();
surface._rnode.add(surface._smod).add(surface);
surfaces.push(surface);
surface.on('click', function(context, e) {
if (this === showing) {
controller.hide({ curve:Easing.inElastic, duration: 1000 }, function(){
gridModifier.setTransform(Transform.scale(1,1,1),
{ curve:Easing.outElastic, duration: 1000 });
});
showing = null;
} else {
showing = this;
gridModifier.setTransform(Transform.scale(0.001, 0.001, 0.001),
{ curve:Easing.outCurve, duration: 300 });
cmod.setTransform(Transform.translate(0, 0, 0.0001));
controller.show(this._rnode, { curve:Easing.outElastic, duration: 2400 });
}
}.bind(surface, mainContext));
}
for(var i = 0; i < 64; i++) {
newSurface(i);
}
var gridModifier = new StateModifier({
size: [400, 400],
align: [0.5, 0.5],
origin: [0.5, 0.5]
});
mainContext.add(gridModifier).add(grid);
mainContext.add(cmod).add(controller);
mainContext.setPerspective(1000);
I think the best way is to follow "StateModifier" example that you can find in famo.us university : http://famo.us/university/lessons/#/famous-102/transitionables/2
Do a scale :
// animates x- and y-scale to 1
stateModifier.setTransform(
Transform.scale(1, 1, 1),
{ duration : 2000, curve: Easing.outBack }
);
and then a align [0.5, 0.5] :
var alignModifier = new Modifier({
align: [0.5, 0.5]
});
and if you want background to be minified, you have to apply 'scale' modifier too to make all other surfaces smaller.

Resources