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.List;
030    import railo.runtime.type.Struct;
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":"")+" ["+List.arrayToList(parameters.keysAsString(),", ")+"] "+(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    }