1 /*
   2  * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
   3  * 
   4  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   5  *
   6  * The contents of this file are subject to the terms of either the Universal Permissive License
   7  * v 1.0 as shown at http://oss.oracle.com/licenses/upl
   8  *
   9  * or the following license:
  10  *
  11  * Redistribution and use in source and binary forms, with or without modification, are permitted
  12  * provided that the following conditions are met:
  13  * 
  14  * 1. Redistributions of source code must retain the above copyright notice, this list of conditions
  15  * and the following disclaimer.
  16  * 
  17  * 2. Redistributions in binary form must reproduce the above copyright notice, this list of
  18  * conditions and the following disclaimer in the documentation and/or other materials provided with
  19  * the distribution.
  20  * 
  21  * 3. Neither the name of the copyright holder nor the names of its contributors may be used to
  22  * endorse or promote products derived from this software without specific prior written permission.
  23  * 
  24  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
  25  * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
  26  * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
  27  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  28  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  29  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
  30  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
  31  * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  32  */
  33 package org.openjdk.jmc.ui.charts;
  34 
  35 import java.awt.BasicStroke;
  36 import java.awt.Color;
  37 import java.awt.FontMetrics;
  38 import java.awt.Graphics2D;
  39 import java.awt.Paint;
  40 import java.awt.Polygon;
  41 import java.awt.Rectangle;
  42 import java.awt.RenderingHints;
  43 import java.awt.Shape;
  44 import java.awt.Stroke;
  45 import java.awt.TexturePaint;
  46 import java.awt.geom.AffineTransform;
  47 import java.awt.image.BufferedImage;
  48 
  49 import org.openjdk.jmc.common.unit.IFormatter;
  50 import org.openjdk.jmc.common.unit.IIncrementalFormatter;
  51 import org.openjdk.jmc.common.unit.IQuantity;
  52 import org.openjdk.jmc.common.unit.IRange;
  53 import org.openjdk.jmc.common.unit.QuantityRange;
  54 import org.openjdk.jmc.ui.UIPlugin;
  55 import org.openjdk.jmc.ui.preferences.PreferenceConstants;
  56 
  57 public class AWTChartToolkit {
  58 
  59         public static interface IColorProvider<T> {
  60 
  61                 Color getColor(T o);
  62         }
  63 
  64         private static final int PLOT_RADIUS = 2;
  65         private static final int TICK_LINE = 3;
  66         private static final int TICK_SIZE = 6;
  67         private static final Stroke DASH_STROKE = new BasicStroke(.5f, 0, 0, 1.0f, new float[] {4, 3}, 0);
  68         private static final BasicStroke EXTRAPOLATION_STROKE = new BasicStroke(1f, BasicStroke.CAP_ROUND,
  69                         BasicStroke.JOIN_ROUND, 1f, new float[] {3f, 2f}, 1f);
  70         private static final Paint EXTRAPOLATION_PAINT;
  71         // The amount of pixels at the top of the yAxis not to draw
  72         private static final int Y_AXIS_TOP_SPACE = 1;
  73         // The size of the arrow (real width/height will be ARROW_SIZE * 2 - 1)
  74         private static final int ARROW_SIZE = 3;
  75 
  76         private static boolean USE_AA = UIPlugin.getDefault().getPreferenceStore()
  77                         .getBoolean(PreferenceConstants.P_ANTI_ALIASING);
  78 
  79         static {
  80                 BufferedImage bi = new BufferedImage(5, 5, BufferedImage.TYPE_INT_RGB);
  81                 Graphics2D big = bi.createGraphics();
  82                 big.setColor(new Color(255, 255, 255));
  83                 big.fillRect(0, 0, 5, 5);
  84                 big.setColor(new Color(200, 200, 200));
  85                 big.drawLine(0, 0, 5, 5);
  86                 Rectangle rect = new Rectangle(0, 0, 5, 5);
  87                 EXTRAPOLATION_PAINT = new TexturePaint(bi, rect);
  88 
  89                 UIPlugin.getDefault().getPreferenceStore().addPropertyChangeListener(e -> USE_AA = (boolean) e.getNewValue());
  90         }
  91 
  92         public static <T> IColorProvider<T> staticColor(final Color color) {
  93                 return new IColorProvider<T>() {
  94 
  95                         @Override
  96                         public Color getColor(T o) {
  97                                 return color;
  98                         }
  99 
 100                 };
 101         }
 102 
 103         private static Object getAntiAliasingHint() {
 104                 return USE_AA ? RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF;
 105         }
 106 
 107         /**
 108          * Draw a horizontal dashed extrapolation line and optionally a striped fill area below. Please
 109          * observe that all the coordinates should be in the actual drawable area, at least roughly, to
 110          * avoid <b>huge</b> performance issues on some machines, including some Macs.
 111          *
 112          * @param ctx
 113          * @param x1
 114          * @param y1
 115          * @param x2
 116          * @param y2
 117          * @param fill
 118          */
 119         private static void drawExtrapolation(Graphics2D ctx, int x1, int y1, int x2, int y2, boolean fill) {
 120                 int x = Math.min(x1, x2);
 121                 int y = Math.min(y1, y2);
 122                 int width = Math.abs(x2 - x1);
 123                 int heigth = Math.abs(y2 - y1);
 124                 if (fill) {
 125                         Paint p = ctx.getPaint();
 126                         ctx.setPaint(EXTRAPOLATION_PAINT);
 127                         ctx.fillRect(x, y, width, heigth);
 128                         ctx.setPaint(p);
 129                 }
 130                 Stroke oldStroke = ctx.getStroke();
 131                 ctx.setStroke(EXTRAPOLATION_STROKE);
 132                 /*
 133                  * On OS X 10.11, at least, these coordinates must be clamped to the visible bounds.
 134                  * Otherwise it may use huge resources and take time proportional to width (and possibly
 135                  * height). That is, _seconds_ with moderate zooming, and possibly much worse due to memory
 136                  * usage.
 137                  */
 138                 ctx.drawLine(x, y + heigth, x + width, y + heigth);
 139                 ctx.setStroke(oldStroke);
 140         }
 141 
 142         public static void drawPlot(Graphics2D ctx, IXYDisplayableSet<?, ?> points, int height, boolean fill) {
 143                 Object oldAntiAliasing = ctx.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
 144                 ctx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, getAntiAliasingHint());
 145                 int diameter = PLOT_RADIUS * 2;
 146                 for (int i = 0; i < points.getSize(); i++) {
 147                         double yCoord = points.getPixelY(i);
 148                         if (!Double.isNaN(yCoord)) {
 149                                 int x = (int) points.getPixelX(i) - PLOT_RADIUS;
 150                                 int y = height - 1 - (int) yCoord - PLOT_RADIUS;
 151                                 if (fill) {
 152                                         ctx.fillOval(x, y, diameter + 1, diameter + 1);
 153                                 } else {
 154                                         ctx.drawOval(x, y, diameter, diameter);
 155                                 }
 156                         }
 157                 }
 158                 ctx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldAntiAliasing);
 159         }
 160 
 161         public static void drawLineChart(
 162                 Graphics2D ctx, IXYDisplayableSet<?, ?> points, int width, int height, boolean fill) {
 163                 Object oldAntiAliasing = ctx.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
 164                 ctx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, getAntiAliasingHint());
 165 
 166                 AffineTransform oldTransform = ctx.getTransform();
 167                 // Flipping integer coordinates 0 to (height - 1). May need to rethink for HiDPI.
 168                 ctx.scale(1, -1);
 169                 ctx.translate(0, 1 - height);
 170 
 171                 Polygon p = getLineChart(points);
 172                 int lastPoint = p.npoints - 1;
 173                 /*
 174                  * On OS X 10.11, at least, these coordinates must be clamped to the visible bounds.
 175                  * Otherwise it may use huge resources and take time proportional to width (and possibly
 176                  * height). That is, _seconds_ with moderate zooming, and possibly much worse due to memory
 177                  * usage.
 178                  */
 179                 if (p.xpoints[0] > 0) {
 180                         drawExtrapolation(ctx, Math.min(p.xpoints[0], width), p.ypoints[0], 0, 0, fill);
 181                 }
 182                 if (p.xpoints[lastPoint] < width) {
 183                         drawExtrapolation(ctx, Math.max(p.xpoints[lastPoint], 0), p.ypoints[lastPoint], width, 0, fill);
 184                 }
 185 
 186                 if (fill) {
 187                         p.ypoints[0] = 0;
 188                         p.ypoints[lastPoint] = 0;
 189                         ctx.fillPolygon(p);
 190                         ctx.setPaint(Color.BLACK);
 191                         ctx.drawPolygon(p);
 192                 } else {
 193                         ctx.drawPolyline(p.xpoints, p.ypoints, p.npoints);
 194                 }
 195 
 196                 ctx.setTransform(oldTransform);
 197                 ctx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldAntiAliasing);
 198         }
 199 
 200         private static Polygon getLineChart(IXYDisplayableSet<?, ?> points) {
 201                 int maxCoordinates = points.getSize() + 2;
 202                 int[] xs = new int[maxCoordinates];
 203                 int[] ys = new int[maxCoordinates];
 204 
 205                 int index = 1;
 206                 for (int i = 0; i < points.getSize(); i++) {
 207                         double yCoord = points.getPixelY(i);
 208                         if (!Double.isNaN(yCoord)) {
 209                                 xs[index] = (int) points.getPixelX(i);
 210                                 ys[index] = (int) yCoord;
 211                                 index++;
 212                         }
 213                 }
 214                 xs[0] = xs[1];
 215                 ys[0] = ys[1];
 216                 xs[index] = xs[index - 1];
 217                 ys[index] = ys[index - 1];
 218                 return new Polygon(xs, ys, index > 1 ? ++index : 0);
 219         }
 220 
 221         private static Polygon getRightAngleChart(IXYDisplayableSet<?, ?> points, int width) {
 222                 int maxCoordinates = points.getSize() * 2 + 2;
 223                 int[] xs = new int[maxCoordinates];
 224                 int[] ys = new int[maxCoordinates];
 225 
 226                 int index = 0;
 227                 int currentY = 0;
 228                 for (int i = 0; i < points.getSize(); i++) {
 229                         double yCoord = points.getPixelY(i);
 230                         int nextY = Double.isNaN(yCoord) ? 0 : (int) yCoord;
 231                         if (nextY != currentY) {
 232                                 int x = (int) points.getPixelX(i);
 233                                 xs[index] = x;
 234                                 ys[index] = currentY;
 235                                 index++;
 236                                 xs[index] = x;
 237                                 ys[index] = nextY;
 238                                 index++;
 239                                 currentY = nextY;
 240                         }
 241                 }
 242                 if (index > 0) {
 243                         xs[index] = width - 1;
 244                         ys[index] = currentY;
 245                         index++;
 246                         xs[index] = width - 1;
 247                         ys[index] = 0;
 248                         index++;
 249                 }
 250                 return new Polygon(xs, ys, index);
 251         }
 252 
 253         public static void drawRightAngleChart(Graphics2D ctx, IXYDisplayableSet<?, ?> points, int width, int height) {
 254                 Polygon p = getRightAngleChart(points, width);
 255 
 256                 AffineTransform oldTransform = ctx.getTransform();
 257                 ctx.scale(1, -1);
 258                 ctx.translate(0, -height);
 259 
 260                 ctx.fillPolygon(p);
 261                 ctx.drawPolyline(p.xpoints, p.ypoints, p.npoints);
 262 
 263                 ctx.setTransform(oldTransform);
 264         }
 265 
 266         public static <T> void drawBarChart(
 267                 Graphics2D ctx, IXYDisplayableSet<T[], ?> points, IColorProvider<? super T> cp, int width, int height) {
 268                 AffineTransform oldTransform = ctx.getTransform();
 269                 ctx.scale(1, -1);
 270                 ctx.translate(0, -height);
 271 
 272                 Paint paint = ctx.getPaint();
 273                 T[] payload = points.getPayload();
 274 
 275                 for (int i = 0; i < points.getSize(); i++) {
 276                         int barHeight = (int) points.getPixelY(i);
 277                         int x1 = (int) points.getPixelX(i);
 278                         int x2 = (int) points.getPixelX(i + 1);
 279                         int barWidth = x2 - x1;
 280                         if (barWidth > 10) {
 281                                 barWidth -= 4;
 282                                 x1 += 2;
 283                         }
 284                         // FIXME: Draw with gradient fill?
 285                         ctx.setPaint(cp == null ? paint : cp.getColor((payload == null) ? null : payload[i]));
 286                         ctx.fillRect(x1, 0, barWidth, barHeight);
 287                         ctx.setPaint(Color.GRAY);
 288                         ctx.drawRect(x1, 0, barWidth, barHeight);
 289                 }
 290                 ctx.setTransform(oldTransform);
 291         }
 292 
 293         public static void drawAxis(
 294                 Graphics2D ctx, SubdividedQuantityRange range, int axisPos, boolean labelAhead, int labelLimit,
 295                 boolean vertical) {
 296                 int axisSize = range.getPixelExtent();
 297                 FontMetrics fm = ctx.getFontMetrics();
 298                 int textAscent = fm.getAscent();
 299                 int textYadjust = textAscent / 2;
 300                 int labelYPos = labelAhead ? axisPos - TICK_SIZE : axisPos + TICK_SIZE + textAscent;
 301                 final int labelSpacing;
 302 
 303                 if (vertical) {
 304                         ctx.drawLine(axisPos, Y_AXIS_TOP_SPACE, axisPos, axisSize - 1);
 305                         drawUpArrow(ctx, axisPos, Y_AXIS_TOP_SPACE, Math.min(ARROW_SIZE, axisSize - 2));
 306                         labelSpacing = fm.getHeight() - textAscent;
 307                 } else {
 308                         ctx.drawLine(0, axisPos, axisSize - 1, axisPos);
 309                         labelSpacing = fm.charWidth(' ') * 2;
 310                 }
 311 
 312                 IRange<IQuantity> firstBucket = QuantityRange.createWithEnd(range.getSubdivider(0), range.getSubdivider(1));
 313                 IQuantity lastShownTick = null;
 314                 final IFormatter<IQuantity> formatter = range.getStart().getType().getFormatterResolving(firstBucket);
 315                 final IIncrementalFormatter changeFormatter;
 316                 if (formatter instanceof IIncrementalFormatter) {
 317                         changeFormatter = (IIncrementalFormatter) formatter;
 318                         if (!vertical && (labelLimit < 0)) {
 319                                 lastShownTick = range.getSubdivider(0);
 320                                 if (lastShownTick.compareTo(range.getStart()) < 0) {
 321                                         lastShownTick = range.getSubdivider(1);
 322                                 }
 323                                 String label = changeFormatter.formatContext(lastShownTick);
 324                                 int labelWidth = fm.stringWidth(label);
 325                                 ctx.drawString(label, labelLimit, labelYPos);
 326                                 labelLimit += labelWidth + labelSpacing;
 327                         }
 328                 } else {
 329                         changeFormatter = null;
 330                 }
 331 
 332                 int numTicks = range.getNumSubdividers();
 333                 for (int i = 0; i < numTicks; i++) {
 334                         int tickPos = (int) range.getSubdividerPixel(i);
 335                         if (tickPos >= axisSize) {
 336                                 break;
 337                         } else if (tickPos >= 0) {
 338                                 IQuantity currentTick = range.getSubdivider(i);
 339                                 final String label;
 340                                 if (vertical) {
 341                                         ctx.drawLine(axisPos - TICK_LINE, axisSize - 1 - tickPos, axisPos + TICK_LINE,
 342                                                         axisSize - 1 - tickPos);
 343                                         if ((tickPos + textYadjust) >= axisSize) {
 344                                                 break;
 345                                         } else if ((tickPos - textYadjust) >= labelLimit) {
 346                                                 label = formatter.format(currentTick);
 347                                                 int labelXPos = labelAhead ? axisPos - TICK_SIZE - fm.stringWidth(label) : axisPos + TICK_SIZE;
 348                                                 ctx.drawString(label, labelXPos, axisSize - 1 - tickPos + textYadjust);
 349                                                 labelLimit = tickPos + textYadjust + labelSpacing;
 350                                         }
 351                                 } else {
 352                                         if (changeFormatter != null) {
 353                                                 label = changeFormatter.formatAdjacent(lastShownTick, range.getSubdivider(i));
 354                                         } else {
 355                                                 label = formatter.format(currentTick);
 356                                         }
 357                                         ctx.drawLine(tickPos, axisPos - TICK_LINE, tickPos, axisPos + TICK_LINE);
 358                                         int textXadjust = fm.stringWidth(label) / 2;
 359                                         // FIXME: Decide if truncated labels should be shown
 360 //                                      if ((tickPos + textXadjust) >= axisSize) {
 361                                         if (tickPos >= axisSize) {
 362                                                 break;
 363                                         } else if ((tickPos - textXadjust) >= labelLimit) {
 364                                                 ctx.drawString(label, tickPos - textXadjust, labelYPos);
 365                                                 labelLimit = tickPos + textXadjust + labelSpacing;
 366                                                 lastShownTick = currentTick;
 367                                         }
 368                                 }
 369                         }
 370                 }
 371         }
 372 
 373         private static void drawUpArrow(Graphics2D ctx, int axisX, int axisYTop, int size) {
 374                 int yArrow = axisYTop + size;
 375                 ctx.drawLine(axisX - size, yArrow, axisX, axisYTop);
 376                 ctx.drawLine(axisX + size, yArrow, axisX, axisYTop);
 377         }
 378 
 379         public static void drawGrid(Graphics2D ctx, SubdividedQuantityRange range, int gridSize, boolean verticalAxis) {
 380                 int axisSize = range.getPixelExtent();
 381                 Stroke oldStroke = ctx.getStroke();
 382                 ctx.setStroke(DASH_STROKE);
 383                 int numTicks = range.getNumSubdividers();
 384                 for (int i = 0; i < numTicks; i++) {
 385                         int pos = (int) range.getSubdividerPixel(i);
 386                         if (pos >= axisSize) {
 387                                 break;
 388                         } else if (pos >= 0) {
 389                                 if (verticalAxis) {
 390                                         ctx.drawLine(0, axisSize - 1 - pos, gridSize - 1, axisSize - 1 - pos);
 391                                 } else {
 392                                         ctx.drawLine(pos, 0, pos, gridSize - 1);
 393                                 }
 394 
 395                         }
 396                 }
 397                 ctx.setStroke(oldStroke);
 398         }
 399 
 400         /**
 401          * Draw ranges by treating the coordinate pairs of {@code points} not as x and y, but as start
 402          * and end on the x axis. As a consequence, {@link IXYDisplayableSet#getWidth()
 403          * points.getWidth()} and {@link IXYDisplayableSet#getHeight() points.getHeight()} should return
 404          * the same value. (Not to be confused with the {@code height} parameter, which is the actual
 405          * number of y pixels that will be filled.)
 406          *
 407          * @param g2
 408          * @param points
 409          * @param height
 410          * @param fill
 411          */
 412         public static void drawRanges(Graphics2D g2, IXYDisplayableSet<?, ?> points, int height, boolean fill) {
 413                 int width = points.getWidth();
 414                 Shape oldClip = g2.getClip();
 415                 g2.setClip(new Rectangle(width, height));
 416                 for (int n = 0; n < points.getSize(); n++) {
 417                         double x1 = points.getPixelX(n);
 418                         double x2 = points.getPixelY(n);
 419                         int start = x1 < 0 ? -1 : (int) x1;
 420                         int end = x2 > width ? width + 1 : (int) x2;
 421                         if (end > 0 && start < width) {
 422                                 if (fill) {
 423                                         g2.fillRect(start, 0, end - start, height);
 424                                 } else {
 425                                         g2.drawRect(start, 0, end - start, height - 1);
 426                                 }
 427                         }
 428                 }
 429                 g2.setClip(oldClip);
 430         }
 431 
 432         /**
 433          * Draw spans by treating the coordinate pairs of {@code points} not as x and y, but as start
 434          * and end on the x axis. As a consequence, {@link IXYDisplayableSet#getWidth()
 435          * points.getWidth()} and {@link IXYDisplayableSet#getHeight() points.getHeight()} should return
 436          * the same value. (Not to be confused with the {@code height} parameter, which is the actual
 437          * number of y pixels that will be filled.)
 438          *
 439          * @param g2
 440          * @param points
 441          * @param height
 442          * @param markBoundaries
 443          * @param cp
 444          */
 445         public static <T> void drawSpan(
 446                 Graphics2D g2, IXYDisplayableSet<T[], ?> points, int height, boolean markBoundaries,
 447                 IColorProvider<? super T> cp) {
 448                 int width = points.getWidth();
 449                 int[] buffer = new int[width];
 450                 int[] secondBuffer = markBoundaries ? new int[width] : buffer;
 451                 T[] payload = points.getPayload();
 452                 for (int n = 0; n < points.getSize(); n++) {
 453                         T item = payload[n];
 454                         if (item != null) {
 455                                 int x1 = (int) points.getPixelX(n);
 456                                 int x2 = (int) points.getPixelY(n);
 457                                 int start = Math.max(0, Math.min(x1, x2));
 458                                 int end = Math.min(width - 1, Math.max(x1, x2));
 459                                 int color = cp.getColor(item).getRGB();
 460                                 if (markBoundaries && (end - start) > 2) {
 461                                         double damp = 0.85 - 3.0 / (end - start);
 462                                         int shade = (int) (50 * damp * damp);
 463                                         for (int i = start; i <= end; i++) {
 464                                                 if (shade > 0) {
 465                                                         buffer[i] = shade(color, shade);
 466                                                         shade = (int) (shade * damp);
 467                                                 } else {
 468                                                         buffer[i] = color;
 469                                                 }
 470                                                 secondBuffer[i] = i == start ? Color.BLACK.getRGB() : buffer[i];
 471                                         }
 472                                 } else {
 473                                         for (int i = start; i <= end; i++) {
 474                                                 secondBuffer[i] = buffer[i] = color;
 475                                         }
 476                                 }
 477                         }
 478                 }
 479                 BufferedImage image = new BufferedImage(width, 1, BufferedImage.TYPE_INT_ARGB);
 480                 BufferedImage cpImage = markBoundaries ? new BufferedImage(width, 1, BufferedImage.TYPE_INT_ARGB) : image;
 481                 image.setRGB(0, 0, width, 1, buffer, 0, width);
 482                 cpImage.setRGB(0, 0, width, 1, secondBuffer, 0, width);
 483 
 484                 for (int n = 0; n < height; n++) {
 485                         if ((n & 2) == 0) {
 486                                 g2.drawImage(cpImage, 0, n, null, null);
 487                         } else {
 488                                 g2.drawImage(image, 0, n, null, null);
 489                         }
 490                 }
 491         }
 492 
 493         private static int shade(int color, int shade) {
 494                 return 0xff000000 & color | shift(color, shade, 16) | shift(color, shade, 8) | shift(color, shade, 0);
 495         }
 496 
 497         private static int shift(int color, int shade, int componentOffset) {
 498                 int comp = ((color >>> componentOffset) & 0xff);
 499                 return (comp > shade ? comp - shade : 0) << componentOffset;
 500         }
 501 
 502 }