1 /*
   2  * Copyright (c) 2012, 2014, Oracle and/or its affiliates.
   3  * All rights reserved. Use is subject to license terms.
   4  *
   5  * This file is available and licensed under the following license:
   6  *
   7  * Redistribution and use in source and binary forms, with or without
   8  * modification, are permitted provided that the following conditions
   9  * are met:
  10  *
  11  *  - Redistributions of source code must retain the above copyright
  12  *    notice, this list of conditions and the following disclaimer.
  13  *  - Redistributions in binary form must reproduce the above copyright
  14  *    notice, this list of conditions and the following disclaimer in
  15  *    the documentation and/or other materials provided with the distribution.
  16  *  - Neither the name of Oracle Corporation nor the names of its
  17  *    contributors may be used to endorse or promote products derived
  18  *    from this software without specific prior written permission.
  19  *
  20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  24  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  25  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  26  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  27  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  28  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  29  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  30  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  31  */
  32 package ensemble.control;
  33 
  34 
  35 import javafx.animation.AnimationTimer;
  36 import javafx.beans.InvalidationListener;
  37 import javafx.beans.Observable;
  38 import javafx.beans.value.ChangeListener;
  39 import javafx.beans.value.ObservableValue;
  40 import javafx.geometry.Point2D;
  41 import javafx.scene.Node;
  42 import javafx.scene.Scene;
  43 import javafx.scene.effect.DisplacementMap;
  44 import javafx.scene.effect.FloatMap;
  45 import javafx.scene.paint.*;
  46 import javafx.scene.shape.*;
  47 
  48 public class BookBend {
  49     private Color bendEndColor = Color.LIGHTBLUE.interpolate(Color.BLACK, 0.6);
  50     private Color bendStartColor = Color.LIGHTBLUE.darker();
  51     private Color pathColor = Color.LIGHTBLUE;
  52     private int newWidth, newHeight;
  53     private double oldTargetX, oldTargetY;
  54     private double targetX = 250;
  55     private double targetY = 250;
  56     private Path clip;
  57     private Path p;
  58     private Path shadow;
  59     private float[][] buffer = new float[1000][2];
  60     private FloatMap map = new FloatMap();
  61     private Node node;
  62     private boolean updateNeeded = false;
  63     private boolean isAnimationTimerActive = false;
  64     private AnimationTimer animationTimer = new AnimationTimer() {
  65 
  66         @Override
  67         public void handle(long l) {
  68             if(updateNeeded){
  69                 update();
  70             }
  71         }
  72     };
  73 
  74     private void startAnimationTimer(){
  75         if(!isAnimationTimerActive){
  76             this.animationTimer.start();
  77             isAnimationTimerActive = true;
  78         }
  79     }
  80 
  81     private void stopAnimationTimer(){
  82         this.animationTimer.stop();
  83         isAnimationTimerActive = false;
  84     }
  85 
  86     private void setUpdateNeeded(boolean value){
  87         if(isAnimationTimerActive == false && value == true && node.getScene() != null){
  88             startAnimationTimer();
  89         }
  90         this.updateNeeded = value;
  91     }
  92 
  93     public BookBend(final Node node, Path path) {
  94         this(node, path, null, null);
  95     }
  96 
  97     public BookBend(final Node node, Path path, Path shadow) {
  98         this(node, path, shadow, null);
  99     }
 100 
 101     /**
 102      * Creates DisplacementMap effect for the bend
 103      * @param node target node
 104      * @param path path that is used to draw the opposite side of a bend
 105      * Its fill is updated with linear gradient.
 106      * @param shadow path that is used to draw the shadow of a bend
 107      * Its fill is updated with linear gradient.
 108      * @param clip path that is used to clip the content of the page either
 109      * for mouse operations or for visuals
 110      */
 111     public BookBend(final Node node, Path path, Path shadow, Path clip) {
 112         this.node = node;
 113         this.p = path;
 114         this.shadow = shadow;
 115         this.clip = clip;
 116 
 117         node.setEffect(new DisplacementMap(map));
 118         node.layoutBoundsProperty().addListener((Observable arg0) -> {
 119             newWidth = (int) Math.round(node.getLayoutBounds().getWidth());
 120             newHeight = (int) Math.round(node.getLayoutBounds().getHeight());
 121             if (newWidth != map.getWidth() || newHeight != map.getHeight()) {
 122                 setUpdateNeeded(true);
 123             }
 124         });
 125         node.sceneProperty().addListener((ObservableValue<? extends Scene> ov, Scene oldValue, Scene newValue) -> {
 126             if(newValue == null){
 127                 stopAnimationTimer();
 128             }
 129         });
 130         newWidth = (int) Math.round(node.getLayoutBounds().getWidth());
 131         newHeight = (int) Math.round(node.getLayoutBounds().getHeight());
 132     }
 133 
 134     /**
 135      * Sets colors for path gradient. Values are used on next update().
 136      * @param pathColor Path Color
 137      * @param bendStartColor Starting path color
 138      * @param bendEndColor  Ending path color
 139      */
 140     public void setColors(Color pathColor, Color bendStartColor, Color bendEndColor) {
 141         this.pathColor = pathColor;
 142         this.bendEndColor = bendEndColor;
 143         this.bendStartColor = bendStartColor;
 144 
 145         Paint fill = p.getFill();
 146         if (fill instanceof LinearGradient) {
 147             LinearGradient lg = (LinearGradient) fill;
 148             if (lg.getStops().size() >= 3) {
 149                 p.setFill(new LinearGradient(lg.getStartX(), lg.getStartY(), lg.getEndX(), lg.getEndY(), false, CycleMethod.NO_CYCLE, 
 150                         new Stop(lg.getStops().get(0).getOffset(), pathColor), 
 151                         new Stop(lg.getStops().get(1).getOffset(), bendStartColor), 
 152                         new Stop(lg.getStops().get(2).getOffset(), bendEndColor)));
 153             }
 154         }
 155     }
 156 
 157     public double getTargetX() {
 158         return targetX;
 159     }
 160 
 161     public double getTargetY() {
 162         return targetY;
 163     }
 164 
 165     public Color getPathColor() {
 166         return pathColor;
 167     }
 168 
 169     public Color getBendEndColor() {
 170         return bendEndColor;
 171     }
 172 
 173     public Color getBendStartColor() {
 174         return bendStartColor;
 175     }
 176     
 177     /**
 178      * Updates DisplacementMap and path to target coordinates.
 179      * @param targetX target X
 180      * @param targetY  target Y
 181      */
 182     public void update(double targetX, double targetY) {
 183         this.targetX = targetX;
 184         this.targetY = targetY;
 185         if (this.targetX != oldTargetX || this.targetY != oldTargetY) {
 186             setUpdateNeeded(true);
 187         }
 188     }
 189     
 190     /**
 191      * Updates DisplacementMap and path for current coordinates.
 192      */
 193     public void update() {
 194         setUpdateNeeded(false);
 195         
 196         if (newWidth == map.getWidth() && newHeight == map.getHeight()
 197                 && targetX == oldTargetX && targetY == oldTargetY) {
 198             return;
 199         }
 200         oldTargetX = targetX;
 201         oldTargetY = targetY;
 202         if (newWidth != map.getWidth() || newHeight != map.getHeight()) {
 203             map.setWidth(newWidth);
 204             map.setHeight(newHeight);
 205         }
 206         
 207         final double W = node.getLayoutBounds().getWidth();
 208         final double H = node.getLayoutBounds().getHeight();
 209 
 210         // target point F for folded corner
 211         final double xF = Math.min(targetX, W - 1);
 212         final double yF = Math.min(targetY, H - 1);
 213 
 214         final Point2D F = new Point2D(xF, yF);
 215 
 216         // corner point O
 217         final double xO = W;
 218         final double yO = H;
 219 
 220         // distance between them
 221         final double FO = Math.hypot(xF - xO, yF - yO);
 222         final double AF = FO / 2;
 223 
 224         final double AC = Math.min(AF * 0.5, 200);
 225         
 226         // radius of the fold as seen along the l2 line
 227         final double R = AC / Math.PI * 1.5;
 228         final double BC = R;
 229         final double flat_R = AC;
 230         
 231         // Gradient for the line from target point to corner point
 232         final double K = (yO - yF) / (xO - xF);
 233 
 234         // angle of a line l1
 235         final double ANGLE = Math.atan(1 / K);
 236 
 237         // point A (on line l1 - the mirror line of target and corner points)
 238         final double xA = (xO + xF) / 2;
 239         final double yA = (yO + yF) / 2;
 240 
 241         // end points of line l1
 242         final double bottomX = xA - (H - yA) * K;
 243         final double bottomY = H;
 244         final double rightX = W;
 245         final double rightY = yA - (W - xA) / K;
 246 
 247         final Point2D RL1 = new Point2D(rightX, rightY);
 248         final Point2D BL1 = new Point2D(bottomX, bottomY);
 249 
 250         // point C (on line l2 - the line when distortion begins)
 251         final double kC = AC / AF;
 252         final double xC = xA - (xA - xF) * kC;
 253         final double yC = yA - (yA - yF) * kC;
 254 
 255         final Point2D C = new Point2D(xC, yC);
 256 
 257         final Point2D RL2 = new Point2D(W, yC - (W - xC) / K);
 258         final Point2D BL2 = new Point2D(xC - (H - yC) * K, H);
 259 
 260         // point B (on line l3 - the line where distortion ends)
 261         final double kB = BC / AC;
 262         final double xB = xC + (xA - xC) * kB;
 263         final double yB = yC + (yA - yC) * kB;
 264 
 265         // Bottom ellipse calculations
 266         final Point2D BP1 = calcIntersection(F, BL1, BL2, C);
 267         final Point2D BP3 = BL2;
 268         final Point2D BP2 = middle(BP1, BP3, 0.5);
 269         final Point2D BP4 = new Point2D(xB + BP2.getX() - xC, yB + BP2.getY() - yC);
 270 
 271         final double bE_x1 = hypot(BP2, BP3);
 272         final double bE_y2 = -hypot(BP2, BP4);
 273         final double bE_yc = -hypot(BP2, BL1);
 274         final double bE_y0 = bE_y2 * bE_y2 / (2 * bE_y2 - bE_yc);
 275         final double bE_b = bE_y0 - bE_y2;
 276         final double bE_a = Math.sqrt(-bE_x1 * bE_x1 / bE_y0 * bE_b * bE_b / bE_yc);
 277 
 278         // Right ellipse calculations
 279         final Point2D RP1 = calcIntersection(F, RL1, RL2, C);
 280         final Point2D RP3 = RL2;
 281         final Point2D RP2 = middle(RP1, RP3, 0.5);
 282         final Point2D RP4 = new Point2D(xB + RP2.getX() - xC, yB + RP2.getY() - yC);
 283 
 284         final double rE_x1 = hypot(RP2, RP3);
 285         final double rE_y2 = -hypot(RP2, RP4);
 286         final double rE_yc = -hypot(RP2, RL1);
 287         final double rE_y0 = rE_y2 * rE_y2 / (2 * rE_y2 - rE_yc);
 288         final double rE_b = rE_y0 - rE_y2;
 289         final double rE_a = Math.sqrt(-rE_x1 * rE_x1 / rE_y0 * rE_b * rE_b / rE_yc);
 290         
 291         p.setFill(new LinearGradient(xF, yF, xO, yO, false, CycleMethod.NO_CYCLE, 
 292                 new Stop(0, pathColor), 
 293                 new Stop((xC - xF) / (xO - xF), bendStartColor), 
 294                 new Stop((xB - xF) / (xO - xF), bendEndColor)));
 295 
 296         ArcTo arcTo1 = new ArcTo();
 297         arcTo1.setXAxisRotation(Math.toDegrees(-ANGLE));
 298         arcTo1.setRadiusX(bE_a);
 299         arcTo1.setRadiusY(bE_b);
 300         arcTo1.setX(BP1.getX());
 301         arcTo1.setY(BP1.getY());
 302         ArcTo arcTo2 = new ArcTo();
 303         arcTo2.setXAxisRotation(Math.toDegrees(-ANGLE));
 304         arcTo2.setRadiusX(rE_a);
 305         arcTo2.setRadiusY(rE_b);
 306         arcTo2.setX(RP4.getX());
 307         arcTo2.setY(RP4.getY());
 308 
 309         p.getElements().setAll(
 310                 new MoveTo(BP4.getX(), BP4.getY()),
 311                 arcTo1,
 312                 new LineTo(xF, yF), 
 313                 new LineTo(RP1.getX(), RP1.getY()),
 314                 arcTo2,
 315                 new ClosePath());
 316 
 317         if (shadow != null) {
 318             double level0 = (xB - xF) / (xO - xF) - R / FO * 0.5;
 319             double level1 = (xB - xF) / (xO - xF) + (0.3 + (200 - AC) / 200) * R / FO;
 320             shadow.setFill(new LinearGradient(xF, yF, xO, yO, false, CycleMethod.NO_CYCLE, 
 321                     new Stop(level0, Color.rgb(0, 0, 0, 0.7)), 
 322                     new Stop(level0 * 0.3 + level1 * 0.7, Color.rgb(0, 0, 0, 0.25)), 
 323                     new Stop(level1, Color.rgb(0, 0, 0, 0.0)), 
 324                     new Stop(1, Color.rgb(0, 0, 0, 0))));
 325 
 326             ArcTo arcTo3 = new ArcTo();
 327             arcTo3.setXAxisRotation(Math.toDegrees(-ANGLE));
 328             arcTo3.setRadiusX(rE_a);
 329             arcTo3.setRadiusY(rE_b);
 330             arcTo3.setX(RP4.getX());
 331             arcTo3.setY(RP4.getY());
 332             arcTo3.setSweepFlag(true);
 333             ArcTo arcTo4 = new ArcTo();
 334             arcTo4.setXAxisRotation(Math.toDegrees(-ANGLE));
 335             arcTo4.setRadiusX(bE_a);
 336             arcTo4.setRadiusY(bE_b);
 337             arcTo4.setX(BP3.getX());
 338             arcTo4.setY(BP3.getY());
 339             arcTo4.setSweepFlag(true);
 340 
 341             shadow.getElements().setAll(
 342                     new MoveTo(RP3.getX(), RP3.getY()),
 343                     arcTo3,
 344                     new LineTo(BP4.getX(), BP4.getY()),
 345                     arcTo4,
 346                     new LineTo(xO, yO),
 347                     new ClosePath());
 348         }
 349         
 350         if (clip != null) {
 351             final Point2D RL3 = new Point2D(W, yB - (W - xB) / K);
 352             final Point2D BL3 = new Point2D(xB - (H - yB) * K, H);
 353         
 354             clip.getElements().setAll(
 355                     new MoveTo(0, 0),
 356                     RL3.getY() > 0 ? new LineTo(W, 0) : new LineTo(0, 0),
 357                     RL3.getY() >= 0 ? new LineTo(RL3.getX(), RL3.getY()) : new LineTo(xB - (0 - yB) * K, 0),
 358                     BL3.getX() >= 0 ? new LineTo(BL3.getX(), BL3.getY()) : new LineTo(0, yB - (0 - xB) / K), 
 359                     BL3.getX() > 0 ? new LineTo(0, H) : new LineTo(0, 0),
 360                     new ClosePath());
 361         }
 362 
 363         final double K2 = -K;
 364         final double C2 = BP3.getX() - K2 * H;
 365 
 366         final double K3 = -K;
 367         final double C3 = xB - K3 * yB;
 368 
 369         final double STEP = Math.max(0.1, R / (buffer.length - 1));
 370         final double HYPOT = Math.hypot(1, K);
 371         final double yR = 1.5 * R; 
 372         double x_1 = 0, y_1 = 0, cur_len = 0;
 373         for (double len = 0; len <= R; len += STEP) {
 374             final int index = (int) Math.round(len / STEP);
 375             final double angle = Math.asin(len / R);
 376             final double y = yR * Math.cos(angle); 
 377             if (len > 0) {
 378                 cur_len += Math.hypot(y - y_1, len - x_1);
 379             }
 380             buffer[index][0] = (float) angle;
 381             buffer[index][1] = (float) (cur_len * flat_R);
 382             x_1 = len;
 383             y_1 = y;
 384         }
 385         double total_len = cur_len;
 386         for (double len = 0; len <= R; len += STEP) {
 387             final int index = (int) Math.round(len / STEP);
 388             final double flat_len = buffer[index][1] / total_len;
 389             final double delta_len = flat_len - len;
 390             final double xs = delta_len / HYPOT;
 391             final double ys = K * delta_len / HYPOT;
 392             buffer[index][0] = (float) (xs / W);
 393             buffer[index][1] = (float) (ys / H);
 394         }
 395 
 396         for (int y = 0; y < map.getHeight(); y++) {
 397             final double lx2 = K2 * (y + 0.5) + C2;
 398             final double lx3 = K3 * (y + 0.5) + C3;
 399             for (int x = 0; x < map.getWidth(); x++) {
 400                 if (x + 0.5 < lx2) {
 401                     map.setSamples(x, y, 0, 0);
 402                 } else if (x + 0.5 >= lx3 - 1) {
 403                     map.setSamples(x, y, 1, 0);
 404                 } else {
 405                     final double len = Math.abs((x + 0.5) - K2 * (y + 0.5) - C2) / HYPOT;
 406                     final int index = (int) Math.round(len / STEP);
 407                     map.setSamples(x, y, buffer[index][0], buffer[index][1]);
 408                 }
 409             }
 410         }         
 411     }
 412     
 413     private static Point2D calcIntersection(Point2D ap1, Point2D ap2, Point2D bp1, Point2D bp2) {
 414         final double a1 = ap1.getY() - ap2.getY();
 415         final double b1 = ap2.getX() - ap1.getX();
 416         final double c1 = ap1.getX() * ap2.getY() - ap2.getX() * ap1.getY();
 417         final double a2 = bp1.getY() - bp2.getY();
 418         final double b2 = bp2.getX() - bp1.getX();
 419         final double c2 = bp1.getX() * bp2.getY() - bp2.getX() * bp1.getY();
 420         final double d = a1 * b2 - a2 * b1;
 421         return new Point2D(
 422                 (b1 * c2 - b2 * c1) / d, 
 423                 (c1 * a2 - c2 * a1) / d);
 424     }    
 425     
 426     private static Point2D middle(Point2D a, Point2D a1, double value) {
 427         return new Point2D(
 428                 a1.getX() * value + a.getX() * (1 - value), 
 429                 a1.getY() * value + a.getY() * (1 - value));
 430     }
 431     
 432     private static double hypot(Point2D p1, Point2D p2) {
 433         return Math.hypot(p1.getX() - p2.getX(), p1.getY() - p2.getY());
 434     }
 435 
 436     public void detach() {
 437         node.setEffect(null);
 438     }
 439 }