diff --git a/README.md b/README.md index 620603fa79b24938a364b4bd46f34f8168f372c0..7eb4fa57b39a8251373f527077a7de69f24cf402 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ -# Gestion des notes des étudiants +# Mandelbrot -## Description du projet +## Description du projet -Le but de ce TP est de créer des classes permettant de représenter des étudiants (classe `Student`), des notes (classe `Grade`), des résultats à une unité d'enseignement (classe `TeachingUnitResult`) et des promotions d'étudiants (classe `Cohort`). +On va travailler sur ce TP sur l'affichage de l'[ensemble de Mandelbrot](https://en.wikipedia.org/wiki/Mandelbrot_set). Pour cela, on va utiliser un code pré-existant. +Le but du TP sera de corriger le code de la classe `Complex` en s'aidant de tests unitaires. ## Membres du projet diff --git a/build.gradle b/build.gradle index b2184c7b55f18065361b00f429f9cf48647eef62..3af223ee845607eb06a1f70bc54a4dcbf3f326eb 100644 --- a/build.gradle +++ b/build.gradle @@ -1,10 +1,15 @@ plugins { - id "application" + id 'application' + id "org.openjfx.javafxplugin" version "0.0.10" } -apply plugin : "java" -group 'l2info' -version '1.0-SNAPSHOT' +javafx { + version = "17" + modules = [ 'javafx.controls', 'javafx.fxml' ] +} + +sourceCompatibility = "16" +targetCompatibility = "16" repositories { mavenCentral() @@ -20,7 +25,7 @@ test { } ext { - javaMainClass = "Main" + javaMainClass = "viewer.Main" } application { diff --git a/src/main/java/Cohort.java b/src/main/java/Cohort.java deleted file mode 100644 index b997326978841098c0235774f90bd70fd6c389c1..0000000000000000000000000000000000000000 --- a/src/main/java/Cohort.java +++ /dev/null @@ -1,56 +0,0 @@ -import java.util.ArrayList; -import java.util.List; - -/** - * A group of students. - */ - -public class Cohort { - private final String name; - private final List<Student> students; - - /** - * Constructs a cohort with a name equals to the specified {@code name} and no students. - * @param name the name of the constructed Cohort - */ - - public Cohort(String name) { - this.name = name; - this.students = new ArrayList<>(); - } - - /** - * Add the specified {@code student} to the students of the cohort. - * @param student the student to be added to the cohort - */ - public void addStudent(Student student){ - // TODO : add code - } - - /** - * Returns the list of students of the cohort. - * @return the list of students of the cohort. - */ - public List<Student> getStudents(){ - // TODO : change code - return null; - } - - /** - * Print via the standard output the name of the cohort and all results associated to the students with their average - * grade. - */ - public void printStudentsResults(){ - // TODO : add code - } - - /** - * Returns the name of the cohort. - * @return the name of the cohort - */ - @Override - public String toString() { - // TODO : change code - return null; - } -} diff --git a/src/main/java/Grade.java b/src/main/java/Grade.java deleted file mode 100644 index 12154f6b79462c66071149335b102342b1b409e0..0000000000000000000000000000000000000000 --- a/src/main/java/Grade.java +++ /dev/null @@ -1,83 +0,0 @@ -import java.util.List; - -/** - * A grade with a float value comprised between 0 and 20. - * - */ -public class Grade { - /** - * The maximum value of a grade. - */ - private static final int MAXIMUM_GRADE = 20; - private final double value; - - /** - * Constructs a grade with a value equals to the specified {@code value}. - * - * @param value the value of the constructed grade - */ - - public Grade(double value) { - this.value = value; - } - - /** - * Returns the value of the grade as a double. - * - * @return the value of the grade - */ - - public double getValue() { - // TODO : change code - return 0.; - } - - /** - * Returns a string representation of the grade in the format X.X/20. - * @return a string representation of the grade - */ - @Override - public String toString() { - // TODO : change code - return null; - } - - /** - * Returns a grade with a value equals to the arithmetic mean of the values of the grade in - * the specified list. - * - * @param grades a list of grades - * @return a grade corresponding to the mean of grade in {@code grades} - */ - public static Grade averageGrade(List<Grade> grades){ - // TODO : change code - return null; - } - - /** - * Determines whether or not two grades are equal. Two instances of Grade are equal if the values - * of their {@code value} member field are the same. - * @param o an object to be compared with this Grade - * @return {@code true} if the object to be compared is an instance of Grade and has the same value; {@code false} - * otherwise. - */ - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - Grade grade = (Grade) o; - - return Double.compare(grade.value, value) == 0; - } - - /** - * Returns a hash code value for the object. - * @return a hash code value for this object. - */ - @Override - public int hashCode() { - long temp = Double.doubleToLongBits(value); - return (int) (temp ^ (temp >>> 32)); - } -} diff --git a/src/main/java/Main.java b/src/main/java/Main.java deleted file mode 100644 index 43057dd29d12bbbe5656d926c6def3add7985b4b..0000000000000000000000000000000000000000 --- a/src/main/java/Main.java +++ /dev/null @@ -1,5 +0,0 @@ -public class Main { - public static void main(String[] args){ - // TODO: add code. - } -} diff --git a/src/main/java/Student.java b/src/main/java/Student.java deleted file mode 100644 index 3f018e120474735e1ac185c427691ae5dd11f9ba..0000000000000000000000000000000000000000 --- a/src/main/java/Student.java +++ /dev/null @@ -1,108 +0,0 @@ -import java.util.ArrayList; -import java.util.List; - -/** - * A students with results. - */ - -public class Student { - private final String firstName; - private final String lastName; - private final List<TeachingUnitResult> results; - - /** - * Constructs a student with the specified first name and last name and no associated results. - * - * @param firstName the first name of the constructed student - * @param lastName the last name of the constructed student - */ - - public Student(String firstName, String lastName) { - this.firstName = firstName; - this.lastName = lastName; - this.results = new ArrayList<>(); - } - - /** - * Add a grade associated to a teaching unit to the results of the student. - * - * @param teachingUnitName the name of the teaching unit of the added result - * @param grade the grade of the added result - */ - public void addResult(String teachingUnitName, Grade grade){ - // TODO : add code - } - - /** - * Returns a string representation of the student in the format first name last name. - * @return a string representation of the student - */ - @Override - public String toString() { - // TODO : change code - return null; - } - - - /** - * Returns the grades of the student. - * - * @return the grades of the student - */ - public List<Grade> getGrades(){ - // TODO : change code - return null; - } - - /** - * Returns the average grade of the student. - * - * @return the average grade of the student - */ - public Grade averageGrade() { - // TODO : change code - return null; - } - - /** - * Print via the standard output the name of the student, all results associated to the students and - * the average grade of the student. - */ - public void printResults(){ - // TODO : add code - } - - - /** - * Determines whether or not two students are equal. Two instances of Student are equal if the values - * of their {@code firtName} and {@code lastName} member fields are the same. - * @param o an object to be compared with this Student - * @return {@code true} if the object to be compared is an instance of Student and has the same name; {@code false} - * otherwise. - */ - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - Student student = (Student) o; - - if (!firstName.equals(student.firstName)) return false; - return lastName.equals(student.lastName); - } - - /** - * Returns a hash code value for the object. - * @return a hash code value for this object. - */ - @Override - public int hashCode() { - int result = firstName.hashCode(); - result = 31 * result + lastName.hashCode(); - return result; - } - - - - -} diff --git a/src/main/java/TeachingUnitResult.java b/src/main/java/TeachingUnitResult.java deleted file mode 100644 index 38ddcb51104cc52b8674099ede96e00f0ee9218d..0000000000000000000000000000000000000000 --- a/src/main/java/TeachingUnitResult.java +++ /dev/null @@ -1,72 +0,0 @@ -/** - * A result corresponding to a grade associated with a teaching unit. - */ - -public class TeachingUnitResult { - private final String teachingUnitName; - private final Grade grade; - - - /** - * Constructs an instance of TeachingUnitResult with a grade equals to the specified {@code grade} - * and a teaching unit name equals to the specified {@code teachingUnitName}. - * - * @param teachingUnitName the name of the teaching unit of the constructed TeachingUnitResult - * @param grade the grade of the constructed TeachingUnitResult - */ - - public TeachingUnitResult(String teachingUnitName, Grade grade) { - this.teachingUnitName = teachingUnitName; - this.grade = grade; - } - - /** - * Returns the grade associated to the result. - * - * @return the grade associated to the result - */ - public Grade getGrade() { - // TODO : change code - return null; - } - - /** - * Returns a string representation of the result in the format Name of the teaching unit : X.X. - * @return a string representation of the result - */ - @Override - public String toString() { - // TODO : change code - return null; - } - - - /** - * Determines whether or not two results are equal. Two instances of TeachingUnitResult are equal if the values - * of their {@code teachingUnitName} and {@code grade} member fields are the same. - * @param o an object to be compared with this TeachingUnitResult - * @return {@code true} if the object to be compared is an instance of TeachingUnitResult and has the same grad and - * teaching unit name; {@code false} otherwise. - */ - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - TeachingUnitResult that = (TeachingUnitResult) o; - - if (!teachingUnitName.equals(that.teachingUnitName)) return false; - return grade.equals(that.grade); - } - - /** - * Returns a hash code value for the object. - * @return a hash code value for this object. - */ - @Override - public int hashCode() { - int result = teachingUnitName.hashCode(); - result = 31 * result + grade.hashCode(); - return result; - } -} diff --git a/src/main/java/mandelbrot/Complex.java b/src/main/java/mandelbrot/Complex.java new file mode 100644 index 0000000000000000000000000000000000000000..5dd4e6b04d86e298aa9e8258c7b40e8e63eddde9 --- /dev/null +++ b/src/main/java/mandelbrot/Complex.java @@ -0,0 +1,246 @@ +package mandelbrot; + +import java.util.Objects; + +/** + * The {@code Complex} class represents a complex number. + * Complex numbers are immutable: their values cannot be changed after they + * are created. + * It includes methods for addition, subtraction, multiplication, division, + * conjugation, and other common functions on complex numbers. + * + * @author Arnaud Labourel + * @author Guyslain Naves + */ +public class Complex { + + /** + * The real part of a complex number. + */ + private final double real; + + /** + * The imaginary part of a complex number. + */ + private final double imaginary; + + + /** + * Initializes a complex number with the specified real and imaginary parts. + * + * @param real the real part + * @param imaginary the imaginary part + */ + public Complex(double real, double imaginary) { + this.real = real; + this.imaginary = imaginary; + } + + /** + * Zero as a complex number, i.e., a number representing "0.0 + 0.0i". + */ + public static final Complex ZERO = new Complex(0, 0); + + /** + * One as a complex number, i.e., a number representing "1.0 + 0.0i". + */ + public static final Complex ONE = new Complex(1, 0); + + + /** + * The square root of -1, i.e., a number representing "0.0 + 1.0i". + */ + public static final Complex I = new Complex(0, 1); + + /** + * Returns the real part of this complex number. + * + * @return the real part of this complex number + */ + public double getReal() { + return real; + } + + /** + * Returns the imaginary part of this complex number. + * + * @return the imaginary part of this complex number + */ + public double getImaginary() { + return imaginary; + } + + /** + * Returns a complex number, whose multiplication corresponds to a rotation by the given angle in the complex plane. + * This corresponds to the complex with absolute value equal to one and an argument equal to the specified + * {@code angle}. + * + * @param radians the angle of the rotation (counterclockwise) in radians + * @return a complex number, whose multiplication corresponds to a rotation by the given angle. + */ + public static Complex rotation(double radians) { + return new Complex(Math.cos(radians), Math.sin(radians)); + } + + /** + * Creates a complex number with the specified real part and an imaginary part equal to zero. + * + * @param real the real component + * @return the complex {@code real + 0i} + */ + public static Complex real(double real) { + return new Complex(real, 0); + } + + /** + * Returns a {@code Complex} whose value is {@code (this + addend)}. + * + * @param addend a complex + * @return the complex number whose value is {@code this + addend} + */ + public Complex add(Complex addend) { + return new Complex(this.real + addend.real, + this.imaginary + addend.imaginary); + } + + /** + * Returns the negation of this complex number. + * + * @return A complex <code>c</code> such that <code>this + c = 0</code> + */ + public Complex negate() { + return new Complex(-this.real, -this.imaginary); + } + + /** + * Returns the conjugate of this complex number. + * + * @return A complex <code>c</code> such that <code>this * c = ||this|| ** 2</code> + */ + public Complex conjugate() { + return new Complex(this.real, -this.imaginary); + } + + /** + * Returns a {@code Complex} whose value is {@code (this - subtrahend)}. + * + * @param subtrahend the complex to be subtracted from {@code this} + * @return the complex number {@code (this - subtrahend)} + */ + public Complex subtract(Complex subtrahend) { + return new Complex(this.real - subtrahend.real, this.imaginary - subtrahend.imaginary); + } + + /** + * Returns a {@code Complex} whose value is {@code this * factor} + * + * @param factor the complex number to multiply to {@code this} + * @return the complex number {@code this * factor} + */ + public Complex multiply(Complex factor) { + return new Complex( + this.real * factor.real - this.imaginary * factor.imaginary, + this.real * factor.imaginary + this.imaginary * factor.real); + } + + /** + * Returns the squared modulus of this complex number. + * + * @return <code>||this|| ** 2</code> + */ + public double squaredModulus() { + return real * real + imaginary * imaginary; + } + + /** + * Returns the modulus (distance to zero) of this complex number. + * + * @return <code>||this||</code> + */ + public double modulus() { + return Math.sqrt(squaredModulus()); + } + + + /** + * Returns the reciprocal of this complex number. + * + * @return a complex number <code>c</code> such that <code>this * c = 1</code> + */ + public Complex reciprocal() { + if (this.equals(ZERO)){ + throw new ArithmeticException("divide by zero"); + } + double m = squaredModulus(); + return this.conjugate().scale(1. / m); + } + + /** + * Returns a {@code Complex} whose value is <code>this / divisor</code>. + * + * @param divisor the denominator (a complex number) + * @return the complex number <code>this / divisor</code> + */ + public Complex divide(Complex divisor) { + return this.multiply(divisor.reciprocal()); + } + + + /** + * Returns the integral power of this complex number. + * + * @param p a non-negative integer + * @return the complex number <code>this ** p</code> + */ + public Complex pow(int p) { + if (p == 0) + return ONE; + Complex result = (this.multiply(this)).pow(p / 2); + if (p % 2 == 1) + result = result.multiply(this); + return result; + } + + /** + * Returns the scalar multiplication of this complex number. + * + * @param lambda a scalar number + * @return the complex number <code>lambda * this</code> + */ + public Complex scale(double lambda) { + return new Complex(lambda * real, lambda * imaginary); + } + + /** + * Test for equality with another object. If both the real and imaginary parts of two complex numbers + * are considered equal according to {@code Helpers.doubleCompare} (i.e., within {@code Helpers.RANGE}), the two + * Complex objects are considered to be equal. + * + * @param other Object to test for equality with this instance. + * @return {@code true} if the objects are equal, {@code false} if object is {@code null}, not an instance of + * {@code Complex}, or not equal to this instance. + */ + @Override + public boolean equals(Object other) { + if (this == other) + return true; + if (!(other instanceof Complex)) + return false; + Complex complex = (Complex) other; + return Helpers.doubleCompare(complex.real, real) == 0 && + Helpers.doubleCompare(complex.imaginary, imaginary) == 0; + } + + /** + * Returns a string representation of this complex number. + * + * @return a string representation of this complex number of the form 42.0 - 1024.0i. + */ + @Override + public String toString() { + if (Helpers.doubleCompare(imaginary, 0) == 0) return real + ""; + if (Helpers.doubleCompare(real, 0) == 0) return imaginary + "i"; + if (Helpers.doubleCompare(imaginary, 0) < 0) return real + " - " + (-imaginary) + "i"; + return real + " + " + imaginary + "i"; + } +} diff --git a/src/main/java/mandelbrot/Helpers.java b/src/main/java/mandelbrot/Helpers.java new file mode 100644 index 0000000000000000000000000000000000000000..39708dc402de32073f9823293b07f685ba1ced3a --- /dev/null +++ b/src/main/java/mandelbrot/Helpers.java @@ -0,0 +1,29 @@ +package mandelbrot; + +/** + * Some helpful functions and values. + */ +class Helpers { + /** + * A small double used to bound the precision of the comparison of doubles. + */ + final static double EPSILON = 1e-9; + + /** + * Comparison of doubles (up to <code>EPSILON</code>) + * <p> + * Please note that floating-point comparison is very tricky, this function + * is not suited to compare small floating-point numbers. + * + * @param d1 an arbitrary double + * @param d2 an arbitrary double + * @return the result of comparing <code>d1</code> and <code>d2</code>. + */ + static int doubleCompare(double d1, double d2) { + double diff = d1 - d2; + return + (diff > EPSILON) ? 1 : + (diff < -EPSILON) ? -1 : + 0; + } +} diff --git a/src/main/java/mandelbrot/Mandelbrot.java b/src/main/java/mandelbrot/Mandelbrot.java new file mode 100644 index 0000000000000000000000000000000000000000..0721c43d78897885f4044e20e2533744e1c8c8a8 --- /dev/null +++ b/src/main/java/mandelbrot/Mandelbrot.java @@ -0,0 +1,115 @@ +package mandelbrot; + +import java.util.function.Function; + +/** + * A class to compute how fast a paramaterized polynomial sequence diverges. + * This is used to compute the colors of point in the Mandelbrot fractal. + */ +public class Mandelbrot { + + /** + * If a complex has modulus above <code>RADIUS</code>, we know that + * the sequence diverges. <code>RADIUS</code> should be at least 2 for + * the usual Mandelbrot sequence. + */ + private static double RADIUS = 10; + + /** + * The square of <code>RADIUS</code>, used in computations. + */ + private static double RADIUS2 = RADIUS * RADIUS; + + + /** + * How many iterations of the sequence do we compute before concluding + * that it probably converges. The more, the better in term of image + * quality, specially in details of the fractal, but also the slower + * the computation is. + */ + private static int MAX_ITERATIONS = 1000; + + + /** + * The degree of the polynomial defining the sequence. + */ + private static int DEGREE = 2; + + /** + * Compute how divergent is the sequence generated by <code>z -> z ** 2 + c</code> + * + * @param c A complex parameter, defining the polynomial to use. + * @return Some value, <code>POSITIVE_INFINITY</code> if the sequence + * converges (or does not seem to converge after + * <code>MAX_ITERATIONS</code>, or a indicative floating-point number of + * the number of iterations needed to goes above the <code>RADIUS</code>. + */ + public double divergence(Complex c) { + if (isConvergent(c)) return Double.POSITIVE_INFINITY; + Function<Complex, Complex> f = z -> z.pow(DEGREE).add(c); + Sequence seq = new Sequence(c, f); + int countIterations = 0; + for (Complex z : seq) { + if (isDiverging(z)) + return smoothIterationCount(countIterations, z); + if (countIterations >= MAX_ITERATIONS) + return Double.POSITIVE_INFINITY; + countIterations++; + } + return 0.; + } + + /** + * This method is used to smooth the number of iterations until + * getting out of the <code>RADIUS</code>, so that we get a + * floating-point value and thus smooth coloring. + * + * @param countIterations the iteration on which <code>RADIUS</code> is beaten. + * @param z the first complex of the sequence whose modulus is above <code>RADIUS</code> + * @return a double close to <code>countIterations</code>. + */ + private double smoothIterationCount(int countIterations, Complex z) { + double x = Math.log(z.modulus()) / Math.log(RADIUS); + return (double) countIterations - Math.log(x) / Math.log(DEGREE); + + } + + + /** + * Checks whether a term of the sequence is out of the given + * <code>RADIUS</code>, which guarantees that the sequence diverges. + * + * @param z a term of the sequence + * @return <code>true</code> if we are sure that the sequence diverges. + */ + private boolean isDiverging(Complex z) { + return z.squaredModulus() > RADIUS2; + } + + + /** + * Checks whether the parameter of the sequence is in some region + * that guarantees that the sequence is convergent. This does not + * capture all convergent parameters. + * + * @param c the parameter for the polynomial + * @return <code>true</code> if we are sure that the sequence converges. + */ + private boolean isConvergent(Complex c) { + return isIn2Bulb(c) || isInCardioid(c); + } + + /* The cardioid black shape of the fractal */ + private boolean isInCardioid(Complex z) { + double m = z.squaredModulus(); + return Helpers.doubleCompare(m * (8 * m - 3), 3. / 32. - z.getReal()) <= 0; + } + + /* The main black disc of the fractal */ + private boolean isIn2Bulb(Complex z) { + Complex zMinusOne = z.subtract(new Complex(-1, 0)); + return Helpers.doubleCompare(zMinusOne.squaredModulus(), 1. / 16.) < 0; + } + + +} diff --git a/src/main/java/mandelbrot/Sequence.java b/src/main/java/mandelbrot/Sequence.java new file mode 100644 index 0000000000000000000000000000000000000000..8d84cf1f1e82d90e3cc9bb9c91523ae7759ae2fc --- /dev/null +++ b/src/main/java/mandelbrot/Sequence.java @@ -0,0 +1,59 @@ +package mandelbrot; + +import java.util.Iterator; +import java.util.function.Function; + +/** + * A class to compute the term of a sequence of complex numbers, generated + * by a function <code>f</code> and an initial term <code>u_0</code>, such + * that <code> u_{n+1} = f(u_n)</code>. + * <p> + * It implements <code>Iterable</code>, allowing to traverse the sequence + * with <code>for (Complex z : mySequence)</code> + */ +public class Sequence implements Iterable<Complex> { + + /* The generating function */ + private final Function<Complex, Complex> f; + + /* The initial term */ + private final Complex u0; + + + /** + * Creates a sequence given the initial term and the function. + * + * @param u0 the first term of the sequence, + * @param f the function over complexes whose repeated application generates the sequence + */ + Sequence(Complex u0, Function<Complex, Complex> f) { + this.f = f; + this.u0 = u0; + } + + + /** + * Creates an iterator iterating all terms of the sequence in order. + * + * @return an iterator + */ + @Override + public Iterator<Complex> iterator() { + return new SeqIterator(); + } + + private class SeqIterator implements Iterator<Complex> { + private Complex current = u0; + + @Override + public boolean hasNext() { + return true; + } + + @Override + public Complex next() { + current = f.apply(current); + return current; + } + } +} diff --git a/src/main/java/viewer/Camera.java b/src/main/java/viewer/Camera.java new file mode 100644 index 0000000000000000000000000000000000000000..7edad181d76e01e557c127d0c0fe9f934935f5d1 --- /dev/null +++ b/src/main/java/viewer/Camera.java @@ -0,0 +1,55 @@ +package viewer; + +import mandelbrot.Complex; + +/** + * A class to represent the view (a rectangle over the complex plane) + * to be displayed. Some interesting views are already defined. + */ +class Camera { + + /** + * The high-level view of the Mandelbrot set. + */ + static Camera camera0 = + new Camera( + -0.5, + 0., + 3, + 4. / 3.); + + + + + private Complex center; /* Center of the rectangle */ + private Complex width; /* Vector for the width of the rectangle */ + private Complex height; /* Vector for the height of the rectangle */ + + + /** + * Creates a view. + * + * @param centerX the real part of the point on which the view is centered + * @param centerY the imaginary part of the point on which the view is centered + * @param width the width of the rectangle to display + * @param aspectRatio the ratio width/height of the rectangle to display + */ + private Camera(double centerX, double centerY, double width, double aspectRatio) { + this.width = Complex.real(width); + this.height = new Complex(0, width / aspectRatio); + this.center = new Complex(centerX, centerY); + } + + /** + * Converts position relative to the rectangle defining the view + * into absolute complex numbers. + * + * @param tx horizontal relative position, between 0 (left) and 1 (right) + * @param ty vertical relative position, between 0 (bottom) and 1 (top) + * @return the complex at this position of the rectangle + */ + Complex toComplex(double tx, double ty) { + return center.add(width.scale(tx - 0.5)).add(height.scale(ty - 0.5)); + } + +} diff --git a/src/main/java/viewer/Controller.java b/src/main/java/viewer/Controller.java new file mode 100644 index 0000000000000000000000000000000000000000..df7db671bb5017fa25af48494a2ca5ac5610377c --- /dev/null +++ b/src/main/java/viewer/Controller.java @@ -0,0 +1,160 @@ +package viewer; + +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.paint.Color; +import mandelbrot.Complex; +import mandelbrot.Mandelbrot; + +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.ResourceBundle; + +/** + * Controls the color of the pixels of the canvas. + */ +public class Controller implements Initializable { + + /** + * Dimension of the grid used to supersample each pixel. + * The number of subpixels for each pixel is the square of <code>SUPERSAMPLING</code> + */ + private static final int SUPERSAMPLING = 3; + + @FXML + private Canvas canvas; /* The canvas to draw on */ + + private Camera camera = Camera.camera0; /* The view to display */ + + private Mandelbrot mandelbrot = new Mandelbrot(); /* the algorithm */ + + + /* positions of colors in the histogram */ + private double[] breakpoints = {0., 0.75, 0.85, 0.95, 0.99, 1.0}; + /* colors of the histogram */ + private Color[] colors = + {Color.gray(0.2), + Color.gray(0.7), + Color.rgb(55, 118, 145), + Color.rgb(63, 74, 132), + Color.rgb(145, 121, 82), + Color.rgb(250, 250, 200) + }; + /* algorithm to generate the distribution of colors */ + private Histogram histogram = new Histogram(breakpoints, colors); + + /** + * Method called when the graphical interface is loaded + * + * @param location location + * @param resources resources + */ + @Override + public void initialize(URL location, ResourceBundle resources) { + render(); + } + + /** + * compute and display the image. + */ + private void render() { + List<Pixel> pixels = getPixels(); + renderPixels(pixels); + } + + /** + * display each pixel + * + * @param pixels the list of all the pixels to display + */ + private void renderPixels(List<Pixel> pixels) { + GraphicsContext context = canvas.getGraphicsContext2D(); + for (Pixel pix : pixels) { + pix.render(context); + } + } + + /** + * Attributes to each subpixel a color + * + * @param subPixels the list of all subpixels to display + */ + private void setSubPixelsColors(List<SubPixel> subPixels) { + int nonBlackPixelsCount = countNonBlackSubPixels(subPixels); + if (nonBlackPixelsCount == 0) return; + Color[] colors = histogram.generate(nonBlackPixelsCount); + subPixels.sort(SubPixel::compare); + int pixCount = 0; + for (SubPixel pix : subPixels) { + pix.setColor(colors[pixCount]); + pixCount++; + if (pixCount >= colors.length) // remaining subpixels stay black (converge). + break; + } + } + + + /** + * Count how many subpixel diverge. + * + * @param subPixels the subpixels to display + * @return the number of diverging subpixels + */ + private int countNonBlackSubPixels(List<SubPixel> subPixels) { + return (int) + subPixels.stream() + .filter(pix -> pix.value != Double.POSITIVE_INFINITY) + .count(); + } + + /** + * Generates the list of all the pixels in the canvas + * + * @return the list of pixels + */ + private List<Pixel> getPixels() { + int width = (int) canvas.getWidth(); + int height = (int) canvas.getHeight(); + List<SubPixel> subPixels = + new ArrayList<>(width * height * SUPERSAMPLING * SUPERSAMPLING); + List<Pixel> pixels = + new ArrayList<>(width * height); + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + Pixel pix = preparePixel(x, y); + subPixels.addAll(pix.getSubPixels()); + pixels.add(pix); + } + } + setSubPixelsColors(subPixels); + return pixels; + } + + /** + * Create the pixel with given coordinates + * + * @param x horizontal coordinate of the pixel + * @param y vertical coordinate of the pixel + * @return the computed pixel with given coordinates + */ + private Pixel preparePixel(int x, int y) { + double width = SUPERSAMPLING * canvas.getWidth(); + double height = SUPERSAMPLING * canvas.getHeight(); + List<SubPixel> sampledSubPixels = new ArrayList<>(); + for (int i = 0; i < SUPERSAMPLING; i++) { + for (int j = 0; j < SUPERSAMPLING; j++) { + Complex z = + camera.toComplex( + ((double) (SUPERSAMPLING * x) + i) / width, + 1 - ((double) (SUPERSAMPLING * y) + j) / height // invert y-axis + ); + double divergence = mandelbrot.divergence(z); + sampledSubPixels.add(new SubPixel(divergence)); + } + } + return new Pixel(x, y, sampledSubPixels); + } +} diff --git a/src/main/java/viewer/Histogram.java b/src/main/java/viewer/Histogram.java new file mode 100644 index 0000000000000000000000000000000000000000..2292ac62f7b294613c494bae03ba5b7838397dfc --- /dev/null +++ b/src/main/java/viewer/Histogram.java @@ -0,0 +1,56 @@ +package viewer; + +import javafx.scene.paint.Color; + +/** + * Histogram of colors, used to generate a list of colors made + * from several gradients combined together, so that the list looks smooth. + */ +class Histogram { + + private double[] breakpoints; + private Color[] colors; + + /** + * Creates a schema of colors. + * <code>breakpoints</code> and <code>colors</code> must have the same length. + * Two consecutive indices of <code>colors</code> define a gradient of colors. + * Those colors will be linearly mapped to the interval defined by the same + * indices taken in <code>breakpoints</code> + * For instance, { 0, 0.4, 1.} with { BLACK, RED, WHITE} represents a black + * to red to white spectrum, where 40% of the point are the black to red + * gradient, 60% are the red to white gradient. + * + * @param breakpoints values from 0 to 1, in increasing order, the first value must be 0 and the last one. + * @param colors colors assigned to each breakpoint. + */ + Histogram(double[] breakpoints, Color[] colors) { + assert (breakpoints[0] == 0); + assert (breakpoints[breakpoints.length - 1] == 1); + assert (colors.length == breakpoints.length); + this.breakpoints = breakpoints; + this.colors = colors; + } + + + /** + * Generates a list of colors of given length representing this spectrum. + * + * @param howManyPoints the number of colors returned + * @return a list of colors following the schema defined in the constructor + */ + Color[] generate(int howManyPoints) { + Color[] result = new Color[howManyPoints]; + double length = (double) howManyPoints; + int bpIndex = 0; + for (int ptIndex = 0; ptIndex < howManyPoints; ptIndex++) { + double absolute = (double) ptIndex / length; + while (absolute > breakpoints[bpIndex + 1] && bpIndex < breakpoints.length - 1) + bpIndex++; + double relative = (absolute - breakpoints[bpIndex]) / (breakpoints[bpIndex + 1] - breakpoints[bpIndex]); + result[ptIndex] = colors[bpIndex].interpolate(colors[bpIndex + 1], relative); + } + return result; + } + +} diff --git a/src/main/java/viewer/Main.java b/src/main/java/viewer/Main.java new file mode 100644 index 0000000000000000000000000000000000000000..f1d621c406af2fc6f009990736f78ae17544a061 --- /dev/null +++ b/src/main/java/viewer/Main.java @@ -0,0 +1,23 @@ +package viewer; + +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; + +public class Main extends Application { + + @Override + public void start(Stage primaryStage) throws Exception { + Parent root = FXMLLoader.load(getClass().getClassLoader().getResource("viewer/viewer.fxml")); + primaryStage.setTitle("Mandelbrot"); + primaryStage.setScene(new Scene(root, 1200, 900)); + primaryStage.show(); + } + + + public static void main(String[] args) { + launch(args); + } +} diff --git a/src/main/java/viewer/Pixel.java b/src/main/java/viewer/Pixel.java new file mode 100644 index 0000000000000000000000000000000000000000..7c77c2894f1ae1543ff4184c636ddfac37e3cacc --- /dev/null +++ b/src/main/java/viewer/Pixel.java @@ -0,0 +1,69 @@ +package viewer; + +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.paint.Color; + +import java.util.Collection; + +/** + * A Pixel. Because of antialiasing, each pixel is further decomposed into + * subpixels. Each subpixels has a color, the color of the pixel is the average + * of the subpixels' colors. + */ +class Pixel { + + private final int x; + private final int y; + private final Collection<SubPixel> subPixels; + + /** + * Creates a pixel with given coordinates and subpixels. + * + * @param x the horizontal coordinate of the pixel on the screen + * @param y the vertical coordinate of the pixel on the screen + * @param subPixels a collection of subpixels for this pixel + */ + Pixel(int x, int y, Collection<SubPixel> subPixels) { + this.x = x; + this.y = y; + this.subPixels = subPixels; + } + + + /** + * @return the list of subpixels in this pixel + */ + Collection<SubPixel> getSubPixels() { + return subPixels; + } + + + private Color getAverageColor() { + double red = 0; + double green = 0; + double blue = 0; + int count = 0; + for (SubPixel subPixel : subPixels) { + count++; + Color col = subPixel.getColor(); + red += col.getRed(); + green += col.getGreen(); + blue += col.getBlue(); + } + double c = (double) count; + return new Color(red / c, green / c, blue / c, 1.); + } + + + /** + * Displays the pixel. + * + * @param context the context of the canvas on which to paint. + */ + void render(GraphicsContext context) { + context.setFill(getAverageColor()); + context.fillRect((double) x, (double) y, 1, 1); + } + + +} diff --git a/src/main/java/viewer/SubPixel.java b/src/main/java/viewer/SubPixel.java new file mode 100644 index 0000000000000000000000000000000000000000..570b70f48743cf627c85a5f37657fbaf2e11df43 --- /dev/null +++ b/src/main/java/viewer/SubPixel.java @@ -0,0 +1,56 @@ +package viewer; + + +import javafx.scene.paint.Color; + +/** + * A subpixel contributes to the color of one pixel. Pixels are usually + * composed of several subpixels, whose colors are averaged. + */ + +class SubPixel { + + private Color color = Color.BLACK; + + /** + * Each subpixel has a value that will be used to color them. + */ + final double value; + + + /** + * Creates a subpixel. + * + * @param value divergence for the corresponding pixel. This will be mapped to a color. + */ + SubPixel(double value) { + this.value = value; + } + + /** + * Attributes a color to a subpixel. + * + * @param color the color to give to the subpixel + */ + void setColor(Color color) { + this.color = color; + } + + /** + * @return the color of the subpixel. Default is black. + */ + Color getColor() { + return color; + } + + /** + * Comparison of two subpixels by their values. + * + * @param pix1 first subpixel to compare + * @param pix2 second subpixel to compare + * @return an integer representing the result of the comparison, with the usual convention. + */ + static int compare(SubPixel pix1, SubPixel pix2) { + return Double.compare(pix1.value, pix2.value); + } +} diff --git a/src/main/resources/viewer/viewer.fxml b/src/main/resources/viewer/viewer.fxml new file mode 100644 index 0000000000000000000000000000000000000000..93e243f8c4aa72de69ed3034b666ce3777bef6ba --- /dev/null +++ b/src/main/resources/viewer/viewer.fxml @@ -0,0 +1,8 @@ +<?import javafx.scene.layout.GridPane?> + +<?import javafx.scene.canvas.Canvas?> +<GridPane fx:controller="viewer.Controller" + xmlns:fx="http://javafx.com/fxml" alignment="center" hgap="10" vgap="10"> + + <Canvas fx:id="canvas" width="1200" height="900"/> +</GridPane> \ No newline at end of file diff --git a/src/test/java/StandardOutputSandbox.java b/src/test/java/StandardOutputSandbox.java deleted file mode 100644 index b8bc1dbae6823585699aaba671439a3cce1462b6..0000000000000000000000000000000000000000 --- a/src/test/java/StandardOutputSandbox.java +++ /dev/null @@ -1,27 +0,0 @@ -import java.io.ByteArrayOutputStream; -import java.io.OutputStream; -import java.io.PrintStream; - - -public class StandardOutputSandbox implements Runnable { - static String NEW_LINE = System.getProperty("line.separator"); - private Runnable runnable; - private OutputStream outputStream; - - StandardOutputSandbox(Runnable runnable) { - this.runnable = runnable; - } - - public void run(){ - outputStream = new ByteArrayOutputStream(); - PrintStream printStream = new PrintStream(outputStream); - System.setOut(printStream); - runnable.run(); - PrintStream originalOut = System.out; - System.setOut(originalOut); - } - - String getProducedOutput() { - return outputStream.toString(); - } -} diff --git a/src/test/java/TestCohort.java b/src/test/java/TestCohort.java deleted file mode 100644 index 0ff9b4f9974b90e7bfab2839f8bfb805ecc5e0db..0000000000000000000000000000000000000000 --- a/src/test/java/TestCohort.java +++ /dev/null @@ -1,43 +0,0 @@ -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -class TestCohort { - private static Cohort cohort = new Cohort("L2 informatique"); - - @BeforeAll - static void addStudentsToCohort(){ - Student paulCalcul = new Student("Paul", "Calcul"); - Student pierreKiroul = new Student("Pierre", "Kiroul"); - pierreKiroul.addResult("Programmation 2", TestGrade.ten); - pierreKiroul.addResult("Structures discrètes", TestGrade.zero); - paulCalcul.addResult("Programmation 2", TestGrade.ten); - paulCalcul.addResult("Structures discrètes", TestGrade.twenty); - cohort.addStudent(paulCalcul); - cohort.addStudent(pierreKiroul); - } - - @Test - void testGetStudents(){ - assertEquals(List.of(TestStudent.paulCalcul, TestStudent.pierreKiroul), cohort.getStudents()); - } - - @Test - void testPrintStudentsResults() { - StandardOutputSandbox standardOutputSandbox = new StandardOutputSandbox(() ->cohort.printStudentsResults()); - String expectedOutput = "L2 informatique" + StandardOutputSandbox.NEW_LINE + StandardOutputSandbox.NEW_LINE - + "Paul Calcul" + StandardOutputSandbox.NEW_LINE - + "Programmation 2 : 10.0/20" + StandardOutputSandbox.NEW_LINE - + "Structures discrètes : 20.0/20" + StandardOutputSandbox.NEW_LINE - + "Note moyenne : 15.0/20" + StandardOutputSandbox.NEW_LINE + StandardOutputSandbox.NEW_LINE - + "Pierre Kiroul" + StandardOutputSandbox.NEW_LINE - + "Programmation 2 : 10.0/20" + StandardOutputSandbox.NEW_LINE - + "Structures discrètes : 0.0/20" + StandardOutputSandbox.NEW_LINE - + "Note moyenne : 5.0/20" + StandardOutputSandbox.NEW_LINE + StandardOutputSandbox.NEW_LINE; - standardOutputSandbox.run(); - assertEquals(expectedOutput, standardOutputSandbox.getProducedOutput()); - } -} diff --git a/src/test/java/TestGrade.java b/src/test/java/TestGrade.java deleted file mode 100644 index 8c25076faab40a0adc21be2687a746d58765c884..0000000000000000000000000000000000000000 --- a/src/test/java/TestGrade.java +++ /dev/null @@ -1,32 +0,0 @@ -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; - -import org.junit.jupiter.api.Test; - -import java.util.List; - -class TestGrade { - static Grade twenty = new Grade(20); - static Grade zero = new Grade(0); - static Grade ten = new Grade(10); - private static List<Grade> grades = List.of(zero, twenty, ten); - private static List<Grade> gradesZero = List.of(zero, zero); - - @Test - void testGetValue() { - assertEquals(20, twenty.getValue()); - assertEquals(0, zero.getValue()); - } - - @Test - void testToString() { - assertEquals("20.0/20", twenty.toString()); - assertEquals("0.0/20", zero.toString()); - } - - @Test - void testAverageGrade(){ - assertEquals(ten, Grade.averageGrade(grades)); - assertEquals(zero, Grade.averageGrade(gradesZero)); - } -} diff --git a/src/test/java/TestStudent.java b/src/test/java/TestStudent.java deleted file mode 100644 index 97ece12db7d2047da0e81ba2033da091b450d9dd..0000000000000000000000000000000000000000 --- a/src/test/java/TestStudent.java +++ /dev/null @@ -1,54 +0,0 @@ -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import java.util.List; - -class TestStudent { - private static Student arnaudLabourel = new Student("Arnaud", "Labourel"); - static Student paulCalcul = new Student("Paul", "Calcul"); - static Student pierreKiroul = new Student("Pierre", "Kiroul"); - - @BeforeAll - static void addResultsToStudents(){ - arnaudLabourel.addResult("Programmation 2", TestGrade.twenty); - arnaudLabourel.addResult("Structures discrètes", TestGrade.twenty); - pierreKiroul.addResult("Programmation 2", TestGrade.ten); - pierreKiroul.addResult("Structures discrètes", TestGrade.zero); - paulCalcul.addResult("Programmation 2", TestGrade.ten); - paulCalcul.addResult("Structures discrètes", TestGrade.twenty); - } - - @Test - void testToString() { - assertEquals("Paul Calcul", paulCalcul.toString()); - assertEquals("Pierre Kiroul", pierreKiroul.toString()); - } - - @Test - void testGetGrades() { - assertEquals(List.of(TestGrade.twenty, TestGrade.twenty), arnaudLabourel.getGrades()); - assertEquals(List.of(TestGrade.ten, TestGrade.zero), pierreKiroul.getGrades()); - assertEquals(List.of(TestGrade.ten, TestGrade.twenty), paulCalcul.getGrades()); - } - - @Test - void testGetAverageGrade() { - assertEquals(TestGrade.twenty, arnaudLabourel.averageGrade()); - assertEquals(new Grade(5), pierreKiroul.averageGrade()); - assertEquals(new Grade(15), paulCalcul.averageGrade()); - } - - @Test - void testPrintResults() { - StandardOutputSandbox standardOutputSandbox = new StandardOutputSandbox(() ->arnaudLabourel.printResults()); - String expectedOutput = - "Arnaud Labourel" + StandardOutputSandbox.NEW_LINE - + "Programmation 2 : 20.0/20" + StandardOutputSandbox.NEW_LINE - + "Structures discrètes : 20.0/20" + StandardOutputSandbox.NEW_LINE - + "Note moyenne : 20.0/20" + StandardOutputSandbox.NEW_LINE; - standardOutputSandbox.run(); - assertEquals(expectedOutput, standardOutputSandbox.getProducedOutput()); - } -} \ No newline at end of file diff --git a/src/test/java/TestTeachingUnitResult.java b/src/test/java/TestTeachingUnitResult.java deleted file mode 100644 index 10525b19b7b9759576fc8195fc39c5b96d579c59..0000000000000000000000000000000000000000 --- a/src/test/java/TestTeachingUnitResult.java +++ /dev/null @@ -1,21 +0,0 @@ -import static org.junit.jupiter.api.Assertions.assertEquals; -import org.junit.jupiter.api.Test; - -class TestTeachingUnitResult { - private static TeachingUnitResult twentyAtProg = - new TeachingUnitResult("Programmation 2", TestGrade.twenty); - private static TeachingUnitResult zeroAtStructDiscrete = - new TeachingUnitResult("Structures discrètes", TestGrade.zero); - - @Test - void testGetGrade() { - assertEquals(TestGrade.twenty, twentyAtProg.getGrade()); - assertEquals(TestGrade.zero, zeroAtStructDiscrete.getGrade()); - } - - @Test - void testToString() { - assertEquals("Programmation 2 : 20.0/20", twentyAtProg.toString()); - assertEquals("Structures discrètes : 0.0/20", zeroAtStructDiscrete.toString()); - } -}