001 /* 002 * 003 004 Licensed under the Apache License, Version 2.0 (the "License"); 005 you may not use this file except in compliance with the License. 006 You may obtain a copy of the License at 007 008 http://www.apache.org/licenses/LICENSE-2.0 009 010 Unless required by applicable law or agreed to in writing, software 011 distributed under the License is distributed on an "AS IS" BASIS, 012 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 See the License for the specific language governing permissions and 014 limitations under the License. 015 */ 016 017 package railo.runtime.img.filter;import java.awt.Rectangle; 018 import java.awt.image.BufferedImage; 019 import java.util.Random; 020 021 import railo.runtime.engine.ThreadLocalPageContext; 022 import railo.runtime.exp.ExpressionException; 023 import railo.runtime.exp.FunctionException; 024 import railo.runtime.exp.PageException; 025 import railo.runtime.img.ImageUtil; 026 import railo.runtime.img.math.Function2D; 027 import railo.runtime.img.math.Noise; 028 import railo.runtime.type.KeyImpl; 029 import railo.runtime.type.Struct; 030 import railo.runtime.type.util.CollectionUtil; 031 /** 032 * A filter which produces an image with a cellular texture. 033 */ 034 public class CellularFilter extends WholeImageFilter implements Function2D, Cloneable, DynFiltering { 035 036 protected float scale = 32; 037 protected float stretch = 1.0f; 038 protected float angle = 0.0f; 039 public float amount = 1.0f; 040 public float turbulence = 1.0f; 041 public float gain = 0.5f; 042 public float bias = 0.5f; 043 public float distancePower = 2; 044 public boolean useColor = false; 045 protected Colormap colormap = new Gradient(); 046 protected float[] coefficients = { 1, 0, 0, 0 }; 047 protected float angleCoefficient; 048 protected Random random = new Random(); 049 protected float m00 = 1.0f; 050 protected float m01 = 0.0f; 051 protected float m10 = 0.0f; 052 protected float m11 = 1.0f; 053 protected Point[] results = null; 054 protected float randomness = 0; 055 protected int gridType = HEXAGONAL; 056 //private float min; 057 //private float max; 058 private static byte[] probabilities; 059 private float gradientCoefficient; 060 061 public final static int RANDOM = 0; 062 public final static int SQUARE = 1; 063 public final static int HEXAGONAL = 2; 064 public final static int OCTAGONAL = 3; 065 public final static int TRIANGULAR = 4; 066 067 public CellularFilter() { 068 results = new Point[3]; 069 for (int j = 0; j < results.length; j++) 070 results[j] = new Point(); 071 if (probabilities == null) { 072 probabilities = new byte[8192]; 073 float factorial = 1; 074 float total = 0; 075 float mean = 2.5f; 076 for (int i = 0; i < 10; i++) { 077 if (i > 1) 078 factorial *= i; 079 float probability = (float)Math.pow(mean, i) * (float)Math.exp(-mean) / factorial; 080 int start = (int)(total * 8192); 081 total += probability; 082 int end = (int)(total * 8192); 083 for (int j = start; j < end; j++) 084 probabilities[j] = (byte)i; 085 } 086 } 087 } 088 089 /** 090 * Specifies the scale of the texture. 091 * @param scale the scale of the texture. 092 * @min-value 1 093 * @max-value 300+ 094 * @see #getScale 095 */ 096 public void setScale(float scale) { 097 this.scale = scale; 098 } 099 100 /** 101 * Returns the scale of the texture. 102 * @return the scale of the texture. 103 * @see #setScale 104 */ 105 public float getScale() { 106 return scale; 107 } 108 109 /** 110 * Specifies the stretch factor of the texture. 111 * @param stretch the stretch factor of the texture. 112 * @min-value 1 113 * @max-value 50+ 114 * @see #getStretch 115 */ 116 public void setStretch(float stretch) { 117 this.stretch = stretch; 118 } 119 120 /** 121 * Returns the stretch factor of the texture. 122 * @return the stretch factor of the texture. 123 * @see #setStretch 124 */ 125 public float getStretch() { 126 return stretch; 127 } 128 129 /** 130 * Specifies the angle of the texture. 131 * @param angle the angle of the texture. 132 * @angle 133 * @see #getAngle 134 */ 135 public void setAngle(float angle) { 136 this.angle = angle; 137 float cos = (float)Math.cos(angle); 138 float sin = (float)Math.sin(angle); 139 m00 = cos; 140 m01 = sin; 141 m10 = -sin; 142 m11 = cos; 143 } 144 145 /** 146 * Returns the angle of the texture. 147 * @return the angle of the texture. 148 * @see #setAngle 149 */ 150 public float getAngle() { 151 return angle; 152 } 153 154 public void setCoefficient(int i, float v) { 155 coefficients[i] = v; 156 } 157 158 public float getCoefficient(int i) { 159 return coefficients[i]; 160 } 161 162 public void setAngleCoefficient(float angleCoefficient) { 163 this.angleCoefficient = angleCoefficient; 164 } 165 166 public float getAngleCoefficient() { 167 return angleCoefficient; 168 } 169 170 public void setGradientCoefficient(float gradientCoefficient) { 171 this.gradientCoefficient = gradientCoefficient; 172 } 173 174 public float getGradientCoefficient() { 175 return gradientCoefficient; 176 } 177 178 public void setF1( float v ) { 179 coefficients[0] = v; 180 } 181 182 public float getF1() { 183 return coefficients[0]; 184 } 185 186 public void setF2( float v ) { 187 coefficients[1] = v; 188 } 189 190 public float getF2() { 191 return coefficients[1]; 192 } 193 194 public void setF3( float v ) { 195 coefficients[2] = v; 196 } 197 198 public float getF3() { 199 return coefficients[2]; 200 } 201 202 public void setF4( float v ) { 203 coefficients[3] = v; 204 } 205 206 public float getF4() { 207 return coefficients[3]; 208 } 209 210 /** 211 * Set the colormap to be used for the filter. 212 * @param colormap the colormap 213 * @see #getColormap 214 */ 215 public void setColormap(Colormap colormap) { 216 this.colormap = colormap; 217 } 218 219 /** 220 * Get the colormap to be used for the filter. 221 * @return the colormap 222 * @see #setColormap 223 */ 224 public Colormap getColormap() { 225 return colormap; 226 } 227 228 public void setRandomness(float randomness) { 229 this.randomness = randomness; 230 } 231 232 public float getRandomness() { 233 return randomness; 234 } 235 236 /** 237 * the grid type to set, one of the following: 238 * - RANDOM 239 * - SQUARE 240 * - HEXAGONAL 241 * - OCTAGONAL 242 * - TRIANGULAR 243 */ 244 public void setGridType(String gridType) throws ExpressionException { 245 gridType=gridType.trim().toLowerCase(); 246 if("random".equals(gridType)) this.gridType = RANDOM; 247 else if("square".equals(gridType)) this.gridType = SQUARE; 248 else if("hexagonal".equals(gridType)) this.gridType = HEXAGONAL; 249 else if("octagonal".equals(gridType)) this.gridType = OCTAGONAL; 250 else if("triangular".equals(gridType)) this.gridType = TRIANGULAR; 251 else 252 throw new ExpressionException("invalid value ["+gridType+"] for gridType, valid values are [random,square,hexagonal,octagonal,triangular]"); 253 } 254 255 public int getGridType() { 256 return gridType; 257 } 258 259 public void setDistancePower(float distancePower) { 260 this.distancePower = distancePower; 261 } 262 263 public float getDistancePower() { 264 return distancePower; 265 } 266 267 /** 268 * Specifies the turbulence of the texture. 269 * @param turbulence the turbulence of the texture. 270 * @min-value 0 271 * @max-value 1 272 * @see #getTurbulence 273 */ 274 public void setTurbulence(float turbulence) { 275 this.turbulence = turbulence; 276 } 277 278 /** 279 * Returns the turbulence of the effect. 280 * @return the turbulence of the effect. 281 * @see #setTurbulence 282 */ 283 public float getTurbulence() { 284 return turbulence; 285 } 286 287 /** 288 * Set the amount of effect. 289 * @param amount the amount 290 * @min-value 0 291 * @max-value 1 292 * @see #getAmount 293 */ 294 public void setAmount(float amount) { 295 this.amount = amount; 296 } 297 298 /** 299 * Get the amount of texture. 300 * @return the amount 301 * @see #setAmount 302 */ 303 public float getAmount() { 304 return amount; 305 } 306 307 public class Point { 308 public int index; 309 public float x, y; 310 public float dx, dy; 311 public float cubeX, cubeY; 312 public float distance; 313 } 314 315 private float checkCube(float x, float y, int cubeX, int cubeY, Point[] results) { 316 int numPoints; 317 random.setSeed(571*cubeX + 23*cubeY); 318 switch (gridType) { 319 case RANDOM: 320 default: 321 numPoints = probabilities[random.nextInt() & 0x1fff]; 322 break; 323 case SQUARE: 324 numPoints = 1; 325 break; 326 case HEXAGONAL: 327 numPoints = 1; 328 break; 329 case OCTAGONAL: 330 numPoints = 2; 331 break; 332 case TRIANGULAR: 333 numPoints = 2; 334 break; 335 } 336 for (int i = 0; i < numPoints; i++) { 337 float px = 0, py = 0; 338 float weight = 1.0f; 339 switch (gridType) { 340 case RANDOM: 341 px = random.nextFloat(); 342 py = random.nextFloat(); 343 break; 344 case SQUARE: 345 px = py = 0.5f; 346 if (randomness != 0) { 347 px += randomness * (random.nextFloat()-0.5); 348 py += randomness * (random.nextFloat()-0.5); 349 } 350 break; 351 case HEXAGONAL: 352 if ((cubeX & 1) == 0) { 353 px = 0.75f; py = 0; 354 } else { 355 px = 0.75f; py = 0.5f; 356 } 357 if (randomness != 0) { 358 px += randomness * Noise.noise2(271*(cubeX+px), 271*(cubeY+py)); 359 py += randomness * Noise.noise2(271*(cubeX+px)+89, 271*(cubeY+py)+137); 360 } 361 break; 362 case OCTAGONAL: 363 switch (i) { 364 case 0: px = 0.207f; py = 0.207f; break; 365 case 1: px = 0.707f; py = 0.707f; weight = 1.6f; break; 366 } 367 if (randomness != 0) { 368 px += randomness * Noise.noise2(271*(cubeX+px), 271*(cubeY+py)); 369 py += randomness * Noise.noise2(271*(cubeX+px)+89, 271*(cubeY+py)+137); 370 } 371 break; 372 case TRIANGULAR: 373 if ((cubeY & 1) == 0) { 374 if (i == 0) { 375 px = 0.25f; py = 0.35f; 376 } else { 377 px = 0.75f; py = 0.65f; 378 } 379 } else { 380 if (i == 0) { 381 px = 0.75f; py = 0.35f; 382 } else { 383 px = 0.25f; py = 0.65f; 384 } 385 } 386 if (randomness != 0) { 387 px += randomness * Noise.noise2(271*(cubeX+px), 271*(cubeY+py)); 388 py += randomness * Noise.noise2(271*(cubeX+px)+89, 271*(cubeY+py)+137); 389 } 390 break; 391 } 392 float dx = Math.abs(x-px); 393 float dy = Math.abs(y-py); 394 float d; 395 dx *= weight; 396 dy *= weight; 397 if (distancePower == 1.0f) 398 d = dx + dy; 399 else if (distancePower == 2.0f) 400 d = (float)Math.sqrt(dx*dx + dy*dy); 401 else 402 d = (float)Math.pow((float)Math.pow(dx, distancePower) + (float)Math.pow(dy, distancePower), 1/distancePower); 403 404 // Insertion sort the long way round to speed it up a bit 405 if (d < results[0].distance) { 406 Point p = results[2]; 407 results[2] = results[1]; 408 results[1] = results[0]; 409 results[0] = p; 410 p.distance = d; 411 p.dx = dx; 412 p.dy = dy; 413 p.x = cubeX+px; 414 p.y = cubeY+py; 415 } else if (d < results[1].distance) { 416 Point p = results[2]; 417 results[2] = results[1]; 418 results[1] = p; 419 p.distance = d; 420 p.dx = dx; 421 p.dy = dy; 422 p.x = cubeX+px; 423 p.y = cubeY+py; 424 } else if (d < results[2].distance) { 425 Point p = results[2]; 426 p.distance = d; 427 p.dx = dx; 428 p.dy = dy; 429 p.x = cubeX+px; 430 p.y = cubeY+py; 431 } 432 } 433 return results[2].distance; 434 } 435 436 public float evaluate(float x, float y) { 437 for (int j = 0; j < results.length; j++) 438 results[j].distance = Float.POSITIVE_INFINITY; 439 440 int ix = (int)x; 441 int iy = (int)y; 442 float fx = x-ix; 443 float fy = y-iy; 444 445 float d = checkCube(fx, fy, ix, iy, results); 446 if (d > fy) 447 d = checkCube(fx, fy+1, ix, iy-1, results); 448 if (d > 1-fy) 449 d = checkCube(fx, fy-1, ix, iy+1, results); 450 if (d > fx) { 451 checkCube(fx+1, fy, ix-1, iy, results); 452 if (d > fy) 453 d = checkCube(fx+1, fy+1, ix-1, iy-1, results); 454 if (d > 1-fy) 455 d = checkCube(fx+1, fy-1, ix-1, iy+1, results); 456 } 457 if (d > 1-fx) { 458 d = checkCube(fx-1, fy, ix+1, iy, results); 459 if (d > fy) 460 d = checkCube(fx-1, fy+1, ix+1, iy-1, results); 461 if (d > 1-fy) 462 d = checkCube(fx-1, fy-1, ix+1, iy+1, results); 463 } 464 465 float t = 0; 466 for (int i = 0; i < 3; i++) 467 t += coefficients[i] * results[i].distance; 468 if (angleCoefficient != 0) { 469 float angle = (float)Math.atan2(y-results[0].y, x-results[0].x); 470 if (angle < 0) 471 angle += 2*(float)Math.PI; 472 angle /= 4*(float)Math.PI; 473 t += angleCoefficient * angle; 474 } 475 if (gradientCoefficient != 0) { 476 float a = 1/(results[0].dy+results[0].dx); 477 t += gradientCoefficient * a; 478 } 479 return t; 480 } 481 482 public float turbulence2(float x, float y, float freq) { 483 float t = 0.0f; 484 485 for (float f = 1.0f; f <= freq; f *= 2) 486 t += evaluate(f*x, f*y) / f; 487 return t; 488 } 489 490 public int getPixel(int x, int y, int[] inPixels, int width, int height) { 491 float nx = m00*x + m01*y; 492 float ny = m10*x + m11*y; 493 nx /= scale; 494 ny /= scale * stretch; 495 nx += 1000; 496 ny += 1000; // Reduce artifacts around 0,0 497 float f = turbulence == 1.0f ? evaluate(nx, ny) : turbulence2(nx, ny, turbulence); 498 // Normalize to 0..1 499 // f = (f-min)/(max-min); 500 f *= 2; 501 f *= amount; 502 int a = 0xff000000; 503 int v; 504 if (colormap != null) { 505 v = colormap.getColor(f); 506 if (useColor) { 507 int srcx = ImageMath.clamp((int)((results[0].x-1000)*scale), 0, width-1); 508 int srcy = ImageMath.clamp((int)((results[0].y-1000)*scale), 0, height-1); 509 v = inPixels[srcy * width + srcx]; 510 f = (results[1].distance - results[0].distance) / (results[1].distance + results[0].distance); 511 f = ImageMath.smoothStep(coefficients[1], coefficients[0], f); 512 v = ImageMath.mixColors(f, 0xff000000, v); 513 } 514 return v; 515 } 516 v = PixelUtils.clamp((int)(f*255)); 517 int r = v << 16; 518 int g = v << 8; 519 int b = v; 520 return a|r|g|b; 521 } 522 523 protected int[] filterPixels( int width, int height, int[] inPixels, Rectangle transformedSpace ) { 524 int index = 0; 525 int[] outPixels = new int[width * height]; 526 527 for (int y = 0; y < height; y++) { 528 for (int x = 0; x < width; x++) { 529 outPixels[index++] = getPixel(x, y, inPixels, width, height); 530 } 531 } 532 return outPixels; 533 } 534 535 public Object clone() { 536 CellularFilter f = (CellularFilter)super.clone(); 537 f.coefficients = coefficients.clone(); 538 f.results = results.clone(); 539 f.random = new Random(); 540 // if (colormap != null) 541 // f.colormap = (Colormap)colormap.clone(); 542 return f; 543 } 544 545 public String toString() { 546 return "Texture/Cellular..."; 547 } 548 549 public BufferedImage filter(BufferedImage src, Struct parameters) throws PageException {BufferedImage dst=ImageUtil.createBufferedImage(src); 550 Object o; 551 if((o=parameters.removeEL(KeyImpl.init("Colormap")))!=null)setColormap(ImageFilterUtil.toColormap(o,"Colormap")); 552 if((o=parameters.removeEL(KeyImpl.init("Amount")))!=null)setAmount(ImageFilterUtil.toFloatValue(o,"Amount")); 553 if((o=parameters.removeEL(KeyImpl.init("Turbulence")))!=null)setTurbulence(ImageFilterUtil.toFloatValue(o,"Turbulence")); 554 if((o=parameters.removeEL(KeyImpl.init("Stretch")))!=null)setStretch(ImageFilterUtil.toFloatValue(o,"Stretch")); 555 if((o=parameters.removeEL(KeyImpl.init("Angle")))!=null)setAngle(ImageFilterUtil.toFloatValue(o,"Angle")); 556 if((o=parameters.removeEL(KeyImpl.init("AngleCoefficient")))!=null)setAngleCoefficient(ImageFilterUtil.toFloatValue(o,"AngleCoefficient")); 557 if((o=parameters.removeEL(KeyImpl.init("GradientCoefficient")))!=null)setGradientCoefficient(ImageFilterUtil.toFloatValue(o,"GradientCoefficient")); 558 if((o=parameters.removeEL(KeyImpl.init("F1")))!=null)setF1(ImageFilterUtil.toFloatValue(o,"F1")); 559 if((o=parameters.removeEL(KeyImpl.init("F2")))!=null)setF2(ImageFilterUtil.toFloatValue(o,"F2")); 560 if((o=parameters.removeEL(KeyImpl.init("F3")))!=null)setF3(ImageFilterUtil.toFloatValue(o,"F3")); 561 if((o=parameters.removeEL(KeyImpl.init("F4")))!=null)setF4(ImageFilterUtil.toFloatValue(o,"F4")); 562 if((o=parameters.removeEL(KeyImpl.init("Randomness")))!=null)setRandomness(ImageFilterUtil.toFloatValue(o,"Randomness")); 563 if((o=parameters.removeEL(KeyImpl.init("GridType")))!=null)setGridType(ImageFilterUtil.toString(o,"GridType")); 564 if((o=parameters.removeEL(KeyImpl.init("DistancePower")))!=null)setDistancePower(ImageFilterUtil.toFloatValue(o,"DistancePower")); 565 if((o=parameters.removeEL(KeyImpl.init("Scale")))!=null)setScale(ImageFilterUtil.toFloatValue(o,"Scale")); 566 567 // check for arguments not supported 568 if(parameters.size()>0) { 569 throw new FunctionException(ThreadLocalPageContext.get(), "ImageFilter", 3, "parameters", "the parameter"+(parameters.size()>1?"s":"")+" ["+CollectionUtil.getKeyList(parameters,", ")+"] "+(parameters.size()>1?"are":"is")+" not allowed, only the following parameters are supported [Colormap, Amount, Turbulence, Stretch, Angle, Coefficient, AngleCoefficient, GradientCoefficient, F1, F2, F3, F4, Randomness, GridType, DistancePower, Scale]"); 570 } 571 572 return filter(src, dst); 573 } 574 }