001/*
002Copyright 2006 Jerry Huxtable
003
004Licensed under the Apache License, Version 2.0 (the "License");
005you may not use this file except in compliance with the License.
006You may obtain a copy of the License at
007
008   http://www.apache.org/licenses/LICENSE-2.0
009
010Unless required by applicable law or agreed to in writing, software
011distributed under the License is distributed on an "AS IS" BASIS,
012WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013See the License for the specific language governing permissions and
014limitations under the License.
015*/
016
017package com.jhlabs.image;
018
019import java.awt.*;
020import java.awt.geom.*;
021import java.awt.image.*;
022
023/**
024 * A Filter to pixellate images.
025 */
026public class ColorHalftoneFilter extends AbstractBufferedImageOp {
027        
028        private float dotRadius = 2;
029    private float cyanScreenAngle = (float)Math.toRadians( 108 );
030    private float magentaScreenAngle = (float)Math.toRadians( 162 );
031    private float yellowScreenAngle = (float)Math.toRadians( 90 );
032
033        public ColorHalftoneFilter() {
034        }
035
036        /**
037         * Set the pixel block size.
038         * @param dotRadius the number of pixels along each block edge
039     * @min-value 1
040     * @max-value 100+
041     * @see #getdotRadius
042         */
043        public void setdotRadius( float dotRadius ) {
044                this.dotRadius = dotRadius;
045        }
046
047        /**
048         * Get the pixel block size.
049         * @return the number of pixels along each block edge
050     * @see #setdotRadius
051         */
052        public float getdotRadius() {
053                return dotRadius;
054        }
055
056        /**
057         * Get the cyan screen angle.
058         * @return the cyan screen angle (in radians)
059     * @see #setCyanScreenAngle
060         */
061        public float getCyanScreenAngle() {
062                return cyanScreenAngle;
063        }
064
065        /**
066         * Set the cyan screen angle.
067         * @param cyanScreenAngle the cyan screen angle (in radians)
068     * @see #getCyanScreenAngle
069         */
070        public void setCyanScreenAngle( float cyanScreenAngle ) {
071                this.cyanScreenAngle = cyanScreenAngle;
072        }
073
074        /**
075         * Get the magenta screen angle.
076         * @return the magenta screen angle (in radians)
077     * @see #setMagentaScreenAngle
078         */
079        public float getMagentaScreenAngle() {
080                return magentaScreenAngle;
081        }
082
083        /**
084         * Set the magenta screen angle.
085         * @param magentaScreenAngle the magenta screen angle (in radians)
086     * @see #getMagentaScreenAngle
087         */
088        public void setMagentaScreenAngle( float magentaScreenAngle ) {
089                this.magentaScreenAngle = magentaScreenAngle;
090        }
091
092        /**
093         * Get the yellow screen angle.
094         * @return the yellow screen angle (in radians)
095     * @see #setYellowScreenAngle
096         */
097        public float getYellowScreenAngle() {
098                return yellowScreenAngle;
099        }
100
101        /**
102         * Set the yellow screen angle.
103         * @param yellowScreenAngle the yellow screen angle (in radians)
104     * @see #getYellowScreenAngle
105         */
106        public void setYellowScreenAngle( float yellowScreenAngle ) {
107                this.yellowScreenAngle = yellowScreenAngle;
108        }
109
110    public BufferedImage filter( BufferedImage src, BufferedImage dst ) {
111        int width = src.getWidth();
112        int height = src.getHeight();
113                int type = src.getType();
114                WritableRaster srcRaster = src.getRaster();
115
116        if ( dst == null )
117            dst = createCompatibleDestImage( src, null );
118
119        float gridSize = 2*dotRadius*1.414f;
120        float[] angles = { cyanScreenAngle, magentaScreenAngle, yellowScreenAngle };
121        float[] mx = new float[] { 0, -1, 1,  0, 0 };
122        float[] my = new float[] { 0, 0, 0, -1, 1 };
123        float halfGridSize = (float)gridSize/2;
124        int[] outPixels = new int[width];
125        int[] inPixels = getRGB( src, 0, 0, width, height, null );
126        for ( int y = 0; y < height; y++ ) {
127            for ( int x = 0, ix = y*width; x < width; x++, ix++ )
128                outPixels[x] = (inPixels[ix] & 0xff000000) | 0xffffff;
129            for ( int channel = 0; channel < 3; channel++ ) {
130                int shift = 16-8*channel;
131                int mask = 0x000000ff << shift;
132                float angle = angles[channel];
133                float sin = (float)Math.sin( angle );
134                float cos = (float)Math.cos( angle );
135
136                for ( int x = 0; x < width; x++ ) {
137                    // Transform x,y into halftone screen coordinate space
138                    float tx = x*cos + y*sin;
139                    float ty = -x*sin + y*cos;
140                    
141                    // Find the nearest grid point
142                    tx = tx-ImageMath.mod( tx-halfGridSize, gridSize )+halfGridSize;
143                    ty = ty-ImageMath.mod( ty-halfGridSize, gridSize )+halfGridSize;
144
145                    float f = 1;
146
147                    // TODO: Efficiency warning: Because the dots overlap, we need to check neighbouring grid squares.
148                    // We check all four neighbours, but in practice only one can ever overlap any given point.
149                    for ( int i = 0; i < 5; i++ ) {
150                        // Find neigbouring grid point
151                        float ttx = tx + mx[i]*gridSize;
152                        float tty = ty + my[i]*gridSize;
153                        // Transform back into image space
154                        float ntx = ttx*cos - tty*sin;
155                        float nty = ttx*sin + tty*cos;
156                        // Clamp to the image
157                        int nx = ImageMath.clamp( (int)ntx, 0, width-1 );
158                        int ny = ImageMath.clamp( (int)nty, 0, height-1 );
159                        int argb = inPixels[ny*width+nx];
160                        int nr = (argb >> shift) & 0xff;
161                        float l = nr/255.0f;
162                        l = 1-l*l;
163                        l *= halfGridSize*1.414;
164                        float dx = x-ntx;
165                        float dy = y-nty;
166                        float dx2 = dx*dx;
167                        float dy2 = dy*dy;
168                        float R = (float)Math.sqrt( dx2+dy2 );
169                        float f2 = 1-ImageMath.smoothStep( R, R+1, l );
170                        f = Math.min( f, f2 );
171                    }
172
173                    int v = (int)(255 * f);
174                    v <<= shift;
175                    v ^= ~mask;
176                                        v |= 0xff000000;
177                    outPixels[x] &= v;
178                }
179            }
180                        setRGB( dst, 0, y, width, 1, outPixels );
181        }
182
183        return dst;
184    }
185
186        public String toString() {
187                return "Pixellate/Color Halftone...";
188        }
189}
190