From 9244a24abbad1187fc9daa5914528200936ba64b Mon Sep 17 00:00:00 2001 From: itays123 Date: Thu, 2 Nov 2023 11:43:20 +0200 Subject: [PATCH] FEATURE: Zooming! --- FunctionCanvas.java | 4 +- FunctionCurve.java | 24 +++++++++--- axis/Axis.java | 21 +++++++++- axis/Range.java | 16 +++----- axis/SteppedAxis.java | 27 ++++++++++++- curve/CartesianAxesCanvas.java | 4 +- curve/CurveCanvas.java | 5 ++- curve/boundable/Boundable.java | 8 ++++ curve/boundable/BoundableCanvas.java | 57 ++++++++++++++++++++++++++++ 9 files changed, 143 insertions(+), 23 deletions(-) create mode 100644 curve/boundable/Boundable.java create mode 100644 curve/boundable/BoundableCanvas.java diff --git a/FunctionCanvas.java b/FunctionCanvas.java index 9bc86bd..453f13d 100644 --- a/FunctionCanvas.java +++ b/FunctionCanvas.java @@ -1,11 +1,11 @@ -import curve.CartesianAxesCanvas; +import curve.boundable.BoundableCanvas; import function.Function; import javafx.scene.paint.Color; /** * Expands the curve canvas to directly add functions and their derivatives */ -public class FunctionCanvas extends CartesianAxesCanvas { +public class FunctionCanvas extends BoundableCanvas { public FunctionCanvas(double width, double height) { super(width, height); diff --git a/FunctionCurve.java b/FunctionCurve.java index 3ca1b76..2f1bca5 100644 --- a/FunctionCurve.java +++ b/FunctionCurve.java @@ -1,4 +1,5 @@ import curve.Curve; +import curve.boundable.Boundable; import function.Function; import javafx.geometry.Point2D; import javafx.scene.paint.Paint; @@ -6,12 +7,14 @@ /** * Represents evaluation that is done based on a rule */ -public class FunctionCurve extends Curve { +public class FunctionCurve extends Curve implements Boundable { public static final double EVALUATION_INTERVAL = 0.01; public static final double JUMP_TRESHOLD = 2000; // the maximal amount of units that can be jumped in one x inteval + private Function function; + /** * Constructs a new function path * @@ -22,13 +25,24 @@ public class FunctionCurve extends Curve { */ public FunctionCurve(Function func, double startX, double endX, Paint paint) { super(paint); - Function derivative = func.derive(); + this.function = func; + evaluate(startX, endX); + } + + @Override + public void setBounds(double startX, double endX) { + clear(); + evaluate(startX, endX); + } + + private void evaluate(double startX, double endX) { + Function derivative = function.derive(); double y, dy; for (double x = startX; x <= endX; x += EVALUATION_INTERVAL) { try { - y = func.evaluate(x); - dy = derivative.evaluate(x); - if (Math.abs(dy * EVALUATION_INTERVAL) >= JUMP_TRESHOLD) + y = function.evaluate(x); + dy = derivative.evaluate(x) * EVALUATION_INTERVAL; + if (Math.abs(dy) >= JUMP_TRESHOLD) throw new ArithmeticException("Jump is too big!"); points.add(new Point2D(x, y)); } catch (ArithmeticException e) { diff --git a/axis/Axis.java b/axis/Axis.java index 1c7e53c..2625e53 100644 --- a/axis/Axis.java +++ b/axis/Axis.java @@ -23,7 +23,7 @@ private void initNegativeShare() { if (this.getLength() == 0) this.negativeShare = 0.5; else - this.negativeShare = Math.min(Math.max((-this.start) / getLength(), 0), 1); + this.negativeShare = -this.start / getLength(); } @Override @@ -39,11 +39,30 @@ public double getPixelsPerUnit() { } public double getOriginLocation() { + return Math.min(pxSize, Math.max(0, getOriginLocationUnsafe())); + } + + protected double getOriginLocationUnsafe() { return pxSize * negativeShare; } + public double getPxSize() { + return pxSize; + } + public double getPixelsOfUnits(double units) { return ((units - this.start) / getLength()) * pxSize; } + public double getUnitsOfPixels(double pixels) { + double axisShare = pixels / pxSize; + return start + axisShare * getLength(); + } + + @Override + public void setRange(double start, double end) { + super.setRange(start, end); + initNegativeShare(); + } + } diff --git a/axis/Range.java b/axis/Range.java index 175cf58..a8d887a 100644 --- a/axis/Range.java +++ b/axis/Range.java @@ -13,8 +13,7 @@ public Range() { } public Range(double start, double end) { - this.start = start; - this.end = end; + setRange(start, end); } public Range(Range other) { @@ -25,18 +24,10 @@ public double getStart() { return start; } - public void expandStart(double x) { - start = Math.min(start, x); - } - public double getEnd() { return end; } - public void expandEnd(double x) { - end = Math.max(end, x); - } - /** * Expand the range to include the number x * @@ -59,4 +50,9 @@ public double getLength() { return end - start; } + public void setRange(double start, double end) { + this.start = start; + this.end = end; + } + } diff --git a/axis/SteppedAxis.java b/axis/SteppedAxis.java index 5d062d6..d939364 100644 --- a/axis/SteppedAxis.java +++ b/axis/SteppedAxis.java @@ -25,10 +25,20 @@ public boolean expandTo(double x) { boolean modified = super.expandTo(x); if (!modified) return false; + return setSteps(); + } + + /** + * Set the steps. Return false if cannot instanciate, since axis is too small to + * contain range + * + * @return + */ + private boolean setSteps() { double maxNumOfSteps = pxSize / MIN_PX_PER_STEP; unitsPerStep = (int) Math.ceil(getLength() / (maxNumOfSteps - STEP_MARGIN)); // divide the number of units by // the number of usable steps (not - if (unitsPerStep < 0) // this will happen when the length is 0, or where the axis size is too small + if (unitsPerStep <= 0) // this will happen when the length is 0, or where the axis size is too small return false; double numOfSteps = getLength() / unitsPerStep + STEP_MARGIN; pxPerStep = pxSize / numOfSteps; // including the step margin) @@ -41,7 +51,14 @@ public double getPixelsOfUnits(double units) { return super.getPixelsOfUnits(units); double stepsFromOrigin = units / unitsPerStep; double pxFromOrigin = stepsFromOrigin * pxPerStep; - return getOriginLocation() + pxFromOrigin; + return getOriginLocationUnsafe() + pxFromOrigin; + } + + @Override + public double getUnitsOfPixels(double pixels) { + double pxFromOrigin = pixels - getOriginLocationUnsafe(); + double stepsFromOrigin = pxFromOrigin / pxPerStep; + return stepsFromOrigin * unitsPerStep; } public Iterable getSteps() { @@ -61,4 +78,10 @@ public Iterable getSteps() { return steps; } + @Override + public void setRange(double start, double end) { + super.setRange(start, end); + setSteps(); + } + } diff --git a/curve/CartesianAxesCanvas.java b/curve/CartesianAxesCanvas.java index 5beabce..434eeff 100644 --- a/curve/CartesianAxesCanvas.java +++ b/curve/CartesianAxesCanvas.java @@ -13,8 +13,8 @@ public class CartesianAxesCanvas extends CurveCanvas { public static final int MARK_SIZE_PX = 10; public static final Paint AXES_COLOR = Color.BLACK; - private SteppedAxis xAxis; - private SteppedAxis yAxis; + protected SteppedAxis xAxis; + protected SteppedAxis yAxis; public CartesianAxesCanvas(double width, double height) { super(width, height); diff --git a/curve/CurveCanvas.java b/curve/CurveCanvas.java index d053cd9..90e4183 100644 --- a/curve/CurveCanvas.java +++ b/curve/CurveCanvas.java @@ -25,6 +25,10 @@ public void addCurve(Curve curve) { curves.add(curve); } + public boolean isDrawn() { + return isDrawn; + } + private void strokeCurve(Curve curve) { setStroke(curve.getPaint()); Point2D prev = null; @@ -42,5 +46,4 @@ public void draw() { strokeCurve(curve); } } - } diff --git a/curve/boundable/Boundable.java b/curve/boundable/Boundable.java new file mode 100644 index 0000000..93d444f --- /dev/null +++ b/curve/boundable/Boundable.java @@ -0,0 +1,8 @@ +package curve.boundable; + +/** + * Represents a curve that can be expanded + */ +public interface Boundable { + void setBounds(double startX, double endX); +} diff --git a/curve/boundable/BoundableCanvas.java b/curve/boundable/BoundableCanvas.java new file mode 100644 index 0000000..9681de1 --- /dev/null +++ b/curve/boundable/BoundableCanvas.java @@ -0,0 +1,57 @@ +package curve.boundable; + +import axis.Axis; +import curve.CartesianAxesCanvas; +import curve.Curve; +import javafx.event.EventHandler; +import javafx.scene.input.ScrollEvent; + +/** + * Represents a cartesian axes canvas that supports zooming + */ +public class BoundableCanvas extends CartesianAxesCanvas implements EventHandler { + + public static final double ZOOM_SENSITIVITY = 1.01; // this is the zoom factor when scrolling one pixel + + public BoundableCanvas(double width, double height) { + super(width, height); + setOnScroll(this); + } + + @Override + public void addCurve(Curve curve) { + if (!(curve instanceof Boundable)) + throw new UnsupportedOperationException("Cannot add non-boundable curve to a boundable canvas"); + super.addCurve(curve); + } + + private void zoomAxis(Axis axis, double fixedPx, double zoomFactor) { + if (fixedPx < 0 || fixedPx > axis.getPxSize()) + throw new IllegalArgumentException("fixed point must be inside axis"); + double edgeWeight = 1 / zoomFactor; + double fixedWeight = 1 - edgeWeight; + double fixedUnits = axis.getUnitsOfPixels(fixedPx); + double newStart = fixedWeight * fixedUnits + edgeWeight * axis.getStart(); + double newEnd = fixedWeight * fixedUnits + edgeWeight * axis.getEnd(); + axis.setRange(newStart, newEnd); + } + + @Override + public void handle(ScrollEvent event) { + if (!isDrawn()) + return; + clear(); + double x = event.getX(); + double y = getHeight() - event.getY(); + double zoomFactor = Math.pow(ZOOM_SENSITIVITY, event.getDeltaY()); + // if we zoom by x2, each unit will double its pixel range + zoomAxis(xAxis, x, zoomFactor); + zoomAxis(yAxis, y, zoomFactor); + + for (Curve curve : curves) { + ((Boundable) curve).setBounds(xAxis.getStart(), xAxis.getEnd()); + } + draw(); + } + +}