/* ChibiPaint Copyright (c) 2006-2008 Marc Schefer This file is part of ChibiPaint. ChibiPaint is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. ChibiPaint is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with ChibiPaint. If not, see <http://www.gnu.org/licenses/>. */ package com.chibipaint.engine; import java.util.*; import android.content.Context; import com.chibipaint.*; import com.chibipaint.engine.CPBrushManager.*; import com.chibipaint.util.*; //FIXME: BROKEN: use setForegroundColor and setBrush, controller's layerChanged replaced by the ICPArtworkListener mechanism public class CPArtwork { public int width, height; private Context context; Vector<CPLayer> layers; CPLayer curLayer; int activeLayer; CPRect curSelection = new CPRect(); CPLayer fusion, undoBuffer, opacityBuffer; CPRect fusionArea, undoArea, opacityArea; Random rnd = new Random(); public interface ICPArtworkListener { void updateRegion(CPArtwork artwork, CPRect region); void layerChange(CPArtwork artwork); } private LinkedList<ICPArtworkListener> artworkListeners = new LinkedList<ICPArtworkListener>(); // Clipboard static private class CPClip { CPColorBmp bmp; int x, y; CPClip(CPColorBmp bmp, int x, int y) { this.bmp = bmp; this.x = x; this.y = y; } }; CPClip clipboard = null; LinkedList<CPUndo> undoList, redoList; private CPBrushInfo curBrush; // FIXME: shouldn't be public public CPBrushManager brushManager = new CPBrushManager(); float lastX, lastY, lastPressure; int[] brushBuffer = null; private int maxUndo = 30; // // Current Engine Parameters // boolean sampleAllLayers = false; boolean lockAlpha = false; int curColor; CPBrushTool paintingModes[] = { new CPBrushToolSimpleBrush(), new CPBrushToolEraser(), new CPBrushToolDodge(), new CPBrushToolBurn(), new CPBrushToolWatercolor(), new CPBrushToolBlur(), new CPBrushToolSmudge(), new CPBrushToolOil(), }; static final int BURN_CONSTANT = 260; static final int BLUR_MIN = 64; static final int BLUR_MAX = 1; public CPArtwork(Context context, int width, int height) { this.width = width; this.height = height; this.context = context; layers = new Vector<CPLayer>(); CPLayer defaultLayer = new CPLayer(width, height); defaultLayer.name = getDefaultLayerName(); defaultLayer.clear(0xffffffff); layers.add(defaultLayer); curLayer = layers.get(0); fusionArea = new CPRect(0, 0, width, height); undoArea = new CPRect(); opacityArea = new CPRect(); activeLayer = 0; curSelection.makeEmpty(); undoBuffer = new CPLayer(width, height); // we reserve a double sized buffer to be used as a 16bits per channel // buffer opacityBuffer = new CPLayer(width, height); fusion = new CPLayer(width, height); undoList = new LinkedList<CPUndo>(); redoList = new LinkedList<CPUndo>(); } public long getDocMemoryUsed() { return (long) width * height * 4 * (3 + layers.size()) + (clipboard != null ? clipboard.bmp.getWidth() * clipboard.bmp.getHeight() * 4 : 0); } public long getUndoMemoryUsed() { long total = 0; CPColorBmp lastBitmap = clipboard != null ? clipboard.bmp : null; for (int i = redoList.size() - 1; i >= 0; i--) { CPUndo undo = redoList.get(i); total += undo.getMemoryUsed(true, lastBitmap); } for (CPUndo undo : undoList) total += undo.getMemoryUsed(false, lastBitmap); return total; } public CPLayer getDisplayBM() { fusionLayers(); return fusion; // for(int i=0; i<opacityBuffer.data.length; i++) // opacityBuffer.data[i] |= 0xff000000; // return opacityBuffer; } public void fusionLayers() { if (fusionArea.isEmpty()) return; mergeOpacityBuffer(curColor, false); fusion.clear(fusionArea, 0x00ffffff); boolean fullAlpha = true, first = true; for (CPLayer l : layers) { if (!first) fullAlpha = fullAlpha && fusion.hasAlpha(fusionArea); if (l.visible) { first = false; if (fullAlpha) l.fusionWithFullAlpha(fusion, fusionArea); else l.fusionWith(fusion, fusionArea); } } fusionArea.makeEmpty(); } // /////////////////////////////////////////////////////////////////////////////////// // Listeners // /////////////////////////////////////////////////////////////////////////////////// public void addListener(ICPArtworkListener listener) { artworkListeners.addLast(listener); } public void callListenersUpdateRegion(CPRect region) { for (ICPArtworkListener l : artworkListeners) l.updateRegion(this, region); } public void callListenersLayerChange() { for (ICPArtworkListener l : artworkListeners) l.layerChange(this); } // /////////////////////////////////////////////////////////////////////////////////// // Global Parameters // /////////////////////////////////////////////////////////////////////////////////// public void setSampleAllLayers(boolean b) { sampleAllLayers = b; } public void setLockAlpha(boolean b) { lockAlpha = b; } public void setForegroundColor(int color) { curColor = color; } public void setBrush(CPBrushInfo brush) { curBrush = brush; } // /////////////////////////////////////////////////////////////////////////////////// // Paint engine // /////////////////////////////////////////////////////////////////////////////////// public void beginStroke(float x, float y, float pressure) { if (curBrush == null) return; paintingModes[curBrush.paintMode].beginStroke(x, y, pressure); } public void continueStroke(float x, float y, float pressure) { if (curBrush == null) return; paintingModes[curBrush.paintMode].continueStroke(x, y, pressure); } public void endStroke() { if (curBrush == null) return; paintingModes[curBrush.paintMode].endStroke(); } void mergeOpacityBuffer(int color, boolean clear) { if (!opacityArea.isEmpty()) { if (curBrush.paintMode != CPBrushInfo.M_ERASE || !lockAlpha) paintingModes[curBrush.paintMode].mergeOpacityBuf(opacityArea, color); else // FIXME: it would be nice to be able to set the paper color paintingModes[CPBrushInfo.M_PAINT].mergeOpacityBuf(opacityArea, 0xffffff); if (lockAlpha) restoreAlpha(opacityArea); if (clear) opacityBuffer.clear(opacityArea, 0); opacityArea.makeEmpty(); } } void restoreAlpha(CPRect r) { getActiveLayer().copyAlphaFrom(undoBuffer, r); } // Extend this class to create new tools and brush types abstract class CPBrushTool { abstract public void beginStroke(float x, float y, float pressure); abstract public void continueStroke(float x, float y, float pressure); abstract public void endStroke(); abstract public void mergeOpacityBuf(CPRect dstRect, int color); } abstract class CPBrushToolBase extends CPBrushTool { @Override public void beginStroke(float x, float y, float pressure) { undoBuffer.copyFrom(curLayer); undoArea.makeEmpty(); opacityBuffer.clear(); opacityArea.makeEmpty(); lastX = x; lastY = y; lastPressure = pressure; paintDab(x, y, pressure); } @Override public void continueStroke(float x, float y, float pressure) { float dist = (float) Math.sqrt((lastX - x) * (lastX - x) + (lastY - y) * (lastY - y)); float spacing = Math.max(curBrush.minSpacing, curBrush.curSize * curBrush.spacing); if (dist > spacing) { float nx = lastX, ny = lastY, np = lastPressure; float df = (spacing - 0.001f) / dist; for (float f = df; f <= 1.f; f += df) { nx = f * x + (1.f - f) * lastX; ny = f * y + (1.f - f) * lastY; np = f * pressure + (1.f - f) * lastPressure; paintDab(nx, ny, np); } lastX = nx; lastY = ny; lastPressure = np; } } @Override public void endStroke() { undoArea.clip(getSize()); if (!undoArea.isEmpty()) { mergeOpacityBuffer(curColor, false); addUndo(new CPUndoPaint()); } brushBuffer = null; } void paintDab(float x, float y, float pressure) { curBrush.applyPressure(pressure); if (curBrush.scattering > 0f) { x += rnd.nextGaussian() * curBrush.curScattering / 4f; y += rnd.nextGaussian() * curBrush.curScattering / 4f; // x += (rnd.nextFloat() - .5f) * tool.scattering; // y += (rnd.nextFloat() - .5f) * tool.scattering; } CPBrushDab dab = brushManager.getDab(x, y, curBrush); paintDab(dab); } void paintDab(CPBrushDab dab) { CPRect srcRect = new CPRect(dab.width, dab.height); CPRect dstRect = new CPRect(dab.width, dab.height); dstRect.translate(dab.x, dab.y); clipSourceDest(srcRect, dstRect); // drawing entirely outside the canvas if (dstRect.isEmpty()) return; undoArea.union(dstRect); opacityArea.union(dstRect); invalidateFusion(dstRect); paintDabImplementation(srcRect, dstRect, dab); } abstract void paintDabImplementation(CPRect srcRect, CPRect dstRect, CPBrushDab dab); } class CPBrushToolSimpleBrush extends CPBrushToolBase { @Override void paintDabImplementation(CPRect srcRect, CPRect dstRect, CPBrushDab dab) { // FIXME: there should be no reference to a specific tool here // create a new brush parameter instead if (curBrush.isAirbrush) paintFlow(srcRect, dstRect, dab.brush, dab.width, Math.max(1, dab.alpha / 8)); else if (curBrush.toolNb == CPController.T_PEN) paintFlow(srcRect, dstRect, dab.brush, dab.width, Math.max(1, dab.alpha / 2)); else // paintOpacityFlow(srcRect, dstRect, brush, dab.stride, alpha, 255); // paintOpacityFlow(srcRect, dstRect, brush, dab.stride, 128, alpha); paintOpacity(srcRect, dstRect, dab.brush, dab.width, dab.alpha); } @Override public void mergeOpacityBuf(CPRect dstRect, int color) { int[] opacityData = opacityBuffer.data; int[] undoData = undoBuffer.data; for (int j = dstRect.top; j < dstRect.bottom; j++) { int dstOffset = dstRect.left + j * width; for (int i = dstRect.left; i < dstRect.right; i++, dstOffset++) { int opacityAlpha = opacityData[dstOffset] / 255; if (opacityAlpha > 0) { int destColor = undoData[dstOffset]; int destAlpha = destColor >>> 24; int newLayerAlpha = opacityAlpha + destAlpha * (255 - opacityAlpha) / 255; int realAlpha = 255 * opacityAlpha / newLayerAlpha; int invAlpha = 255 - realAlpha; int newColor = ((color >>> 16 & 0xff) * realAlpha + (destColor >>> 16 & 0xff) * invAlpha) / 255 << 16 & 0xff0000 | ((color >>> 8 & 0xff) * realAlpha + (destColor >>> 8 & 0xff) * invAlpha) / 255 << 8 & 0xff00 | ((color & 0xff) * realAlpha + (destColor & 0xff) * invAlpha) / 255 & 0xff; newColor |= newLayerAlpha << 24 & 0xff000000; curLayer.data[dstOffset] = newColor; } } } } void paintOpacity(CPRect srcRect, CPRect dstRect, byte[] brush, int w, int alpha) { int[] opacityData = opacityBuffer.data; int by = srcRect.top; for (int j = dstRect.top; j < dstRect.bottom; j++, by++) { int srcOffset = srcRect.left + by * w; int dstOffset = dstRect.left + j * width; for (int i = dstRect.left; i < dstRect.right; i++, srcOffset++, dstOffset++) { int brushAlpha = (brush[srcOffset] & 0xff) * alpha; if (brushAlpha != 0) { int opacityAlpha = opacityData[dstOffset]; if (brushAlpha > opacityAlpha) opacityData[dstOffset] = brushAlpha; } } } } void paintFlow(CPRect srcRect, CPRect dstRect, byte[] brush, int w, int alpha) { int[] opacityData = opacityBuffer.data; int by = srcRect.top; for (int j = dstRect.top; j < dstRect.bottom; j++, by++) { int srcOffset = srcRect.left + by * w; int dstOffset = dstRect.left + j * width; for (int i = dstRect.left; i < dstRect.right; i++, srcOffset++, dstOffset++) { int brushAlpha = (brush[srcOffset] & 0xff) * alpha; if (brushAlpha != 0) { int opacityAlpha = Math.min(255 * 255, opacityData[dstOffset] + (255 - opacityData[dstOffset] / 255) * brushAlpha / 255); opacityData[dstOffset] = opacityAlpha; } } } } void paintOpacityFlow(CPRect srcRect, CPRect dstRect, byte[] brush, int w, int opacity, int flow) { int[] opacityData = opacityBuffer.data; int by = srcRect.top; for (int j = dstRect.top; j < dstRect.bottom; j++, by++) { int srcOffset = srcRect.left + by * w; int dstOffset = dstRect.left + j * width; for (int i = dstRect.left; i < dstRect.right; i++, srcOffset++, dstOffset++) { int brushAlpha = (brush[srcOffset] & 0xff) * flow; if (brushAlpha != 0) { int opacityAlpha = opacityData[dstOffset]; int newAlpha = Math.min(255 * 255, opacityAlpha + (opacity - opacityAlpha / 255) * brushAlpha / 255); newAlpha = Math.min(opacity * (brush[srcOffset] & 0xff), newAlpha); if (newAlpha > opacityAlpha) opacityData[dstOffset] = newAlpha; } } } } } class CPBrushToolEraser extends CPBrushToolSimpleBrush { @Override public void mergeOpacityBuf(CPRect dstRect, int color) { int[] opacityData = opacityBuffer.data; int[] undoData = undoBuffer.data; for (int j = dstRect.top; j < dstRect.bottom; j++) { int dstOffset = dstRect.left + j * width; for (int i = dstRect.left; i < dstRect.right; i++, dstOffset++) { int opacityAlpha = opacityData[dstOffset] / 255; if (opacityAlpha > 0) { int destColor = undoData[dstOffset]; int destAlpha = destColor >>> 24; int realAlpha = destAlpha * (255 - opacityAlpha) / 255; curLayer.data[dstOffset] = destColor & 0xffffff | realAlpha << 24; } } } } } class CPBrushToolDodge extends CPBrushToolSimpleBrush { @Override public void mergeOpacityBuf(CPRect dstRect, int color) { int[] opacityData = opacityBuffer.data; int[] undoData = undoBuffer.data; for (int j = dstRect.top; j < dstRect.bottom; j++) { int dstOffset = dstRect.left + j * width; for (int i = dstRect.left; i < dstRect.right; i++, dstOffset++) { int opacityAlpha = opacityData[dstOffset] / 255; if (opacityAlpha > 0) { int destColor = undoData[dstOffset]; if ((destColor & 0xff000000) != 0) { opacityAlpha += 255; int r = (destColor >>> 16 & 0xff) * opacityAlpha / 255; int g = (destColor >>> 8 & 0xff) * opacityAlpha / 255; int b = (destColor & 0xff) * opacityAlpha / 255; if (r > 255) r = 255; if (g > 255) g = 255; if (b > 255) b = 255; int newColor = destColor & 0xff000000 | r << 16 | g << 8 | b; curLayer.data[dstOffset] = newColor; } } } } } } class CPBrushToolBurn extends CPBrushToolSimpleBrush { @Override public void mergeOpacityBuf(CPRect dstRect, int color) { int[] opacityData = opacityBuffer.data; int[] undoData = undoBuffer.data; for (int j = dstRect.top; j < dstRect.bottom; j++) { int dstOffset = dstRect.left + j * width; for (int i = dstRect.left; i < dstRect.right; i++, dstOffset++) { int opacityAlpha = opacityData[dstOffset] / 255; if (opacityAlpha > 0) { int destColor = undoData[dstOffset]; if ((destColor & 0xff000000) != 0) { // opacityAlpha = 255 - opacityAlpha; int r = destColor >>> 16 & 0xff; int g = destColor >>> 8 & 0xff; int b = destColor & 0xff; r = r - (BURN_CONSTANT - r) * opacityAlpha / 255; g = g - (BURN_CONSTANT - g) * opacityAlpha / 255; b = b - (BURN_CONSTANT - b) * opacityAlpha / 255; if (r < 0) r = 0; if (g < 0) g = 0; if (b < 0) b = 0; int newColor = destColor & 0xff000000 | r << 16 | g << 8 | b; curLayer.data[dstOffset] = newColor; } } } } } } class CPBrushToolBlur extends CPBrushToolSimpleBrush { @Override public void mergeOpacityBuf(CPRect dstRect, int color) { int[] opacityData = opacityBuffer.data; int[] undoData = undoBuffer.data; for (int j = dstRect.top; j < dstRect.bottom; j++) { int dstOffset = dstRect.left + j * width; for (int i = dstRect.left; i < dstRect.right; i++, dstOffset++) { int opacityAlpha = opacityData[dstOffset] / 255; if (opacityAlpha > 0) { int blur = BLUR_MIN + (BLUR_MAX - BLUR_MIN) * opacityAlpha / 255; int destColor = undoData[dstOffset]; int a = blur * (destColor >>> 24 & 0xff); int r = blur * (destColor >>> 16 & 0xff); int g = blur * (destColor >>> 8 & 0xff); int b = blur * (destColor & 0xff); int sum = blur + 4; destColor = undoData[j > 0 ? dstOffset - width : dstOffset]; a += destColor >>> 24 & 0xff; r += destColor >>> 16 & 0xff; g += destColor >>> 8 & 0xff; b += destColor & 0xff; destColor = undoData[j < height - 1 ? dstOffset + width : dstOffset]; a += destColor >>> 24 & 0xff; r += destColor >>> 16 & 0xff; g += destColor >>> 8 & 0xff; b += destColor & 0xff; destColor = undoData[i > 0 ? dstOffset - 1 : dstOffset]; a += destColor >>> 24 & 0xff; r += destColor >>> 16 & 0xff; g += destColor >>> 8 & 0xff; b += destColor & 0xff; destColor = undoData[i < width - 1 ? dstOffset + 1 : dstOffset]; a += destColor >>> 24 & 0xff; r += destColor >>> 16 & 0xff; g += destColor >>> 8 & 0xff; b += destColor & 0xff; a /= sum; r /= sum; g /= sum; b /= sum; curLayer.data[dstOffset] = a << 24 | r << 16 | g << 8 | b; } } } } } // Brushes derived from this class use the opacity buffer // as a simple alpha layer class CPBrushToolDirectBrush extends CPBrushToolSimpleBrush { @Override public void mergeOpacityBuf(CPRect dstRect, int color) { int[] opacityData = opacityBuffer.data; int[] undoData = undoBuffer.data; for (int j = dstRect.top; j < dstRect.bottom; j++) { int dstOffset = dstRect.left + j * width; for (int i = dstRect.left; i < dstRect.right; i++, dstOffset++) { int color1 = opacityData[dstOffset]; int alpha1 = color1 >>> 24; if (alpha1 <= 0) continue; int color2 = undoData[dstOffset]; int alpha2 = (color2 >>> 24) * fusion.alpha / 100; int newAlpha = alpha1 + alpha2 - alpha1 * alpha2 / 255; if (newAlpha > 0) { int realAlpha = alpha1 * 255 / newAlpha; int invAlpha = 255 - realAlpha; curLayer.data[dstOffset] = newAlpha << 24 | ((color1 >>> 16 & 0xff) * realAlpha + (color2 >>> 16 & 0xff) * invAlpha) / 255 << 16 | ((color1 >>> 8 & 0xff) * realAlpha + (color2 >>> 8 & 0xff) * invAlpha) / 255 << 8 | ((color1 & 0xff) * realAlpha + (color2 & 0xff) * invAlpha) / 255; } } } } } class CPBrushToolWatercolor extends CPBrushToolDirectBrush { static final int wcMemory = 50; static final int wxMaxSampleRadius = 64; LinkedList<CPColorFloat> previousSamples; @Override public void beginStroke(float x, float y, float pressure) { previousSamples = null; super.beginStroke(x, y, pressure); } @Override void paintDabImplementation(CPRect srcRect, CPRect dstRect, CPBrushDab dab) { if (previousSamples == null) { CPColorFloat startColor = sampleColor( (dstRect.left + dstRect.right) / 2, (dstRect.top + dstRect.bottom) / 2, Math.max(1, Math.min(wxMaxSampleRadius, dstRect.getWidth() * 2 / 6)), Math.max(1, Math.min(wxMaxSampleRadius, dstRect.getHeight() * 2 / 6))); previousSamples = new LinkedList<CPColorFloat>(); for (int i = 0; i < wcMemory; i++) previousSamples.addLast(startColor); } CPColorFloat wcColor = new CPColorFloat(0, 0, 0); for (CPColorFloat sample : previousSamples) { wcColor.r += sample.r; wcColor.g += sample.g; wcColor.b += sample.b; } wcColor.r /= previousSamples.size(); wcColor.g /= previousSamples.size(); wcColor.b /= previousSamples.size(); // resaturation int color = curColor & 0xffffff; wcColor.mixWith(new CPColorFloat(color), curBrush.resat * curBrush.resat); int newColor = wcColor.toInt(); // bleed wcColor.mixWith( sampleColor( (dstRect.left + dstRect.right) / 2, (dstRect.top + dstRect.bottom) / 2, Math.max(1, Math.min(wxMaxSampleRadius, dstRect.getWidth() * 2 / 6)), Math.max(1, Math.min(wxMaxSampleRadius, dstRect.getHeight() * 2 / 6))), curBrush.bleed); previousSamples.addLast(wcColor); previousSamples.removeFirst(); paintDirect(srcRect, dstRect, dab.brush, dab.width, Math.max(1, dab.alpha / 4), newColor); mergeOpacityBuffer(0, false); if (sampleAllLayers) fusionLayers(); } void paintDirect(CPRect srcRect, CPRect dstRect, byte[] brush, int w, int alpha, int color1) { int[] opacityData = opacityBuffer.data; int by = srcRect.top; for (int j = dstRect.top; j < dstRect.bottom; j++, by++) { int srcOffset = srcRect.left + by * w; int dstOffset = dstRect.left + j * width; for (int i = dstRect.left; i < dstRect.right; i++, srcOffset++, dstOffset++) { int alpha1 = (brush[srcOffset] & 0xff) * alpha / 255; if (alpha1 <= 0) continue; int color2 = opacityData[dstOffset]; int alpha2 = color2 >>> 24; int newAlpha = alpha1 + alpha2 - alpha1 * alpha2 / 255; if (newAlpha > 0) { int realAlpha = alpha1 * 255 / newAlpha; int invAlpha = 255 - realAlpha; // The usual alpha blending formula C = A * alpha + B * (1 - alpha) // has to rewritten in the form C = A + (1 - alpha) * B - (1 - // alpha) *A // that way the rounding up errors won't cause problems int newColor = newAlpha << 24 | (color1 >>> 16 & 0xff) + ((color2 >>> 16 & 0xff) * invAlpha - (color1 >>> 16 & 0xff) * invAlpha) / 255 << 16 | (color1 >>> 8 & 0xff) + ((color2 >>> 8 & 0xff) * invAlpha - (color1 >>> 8 & 0xff) * invAlpha) / 255 << 8 | (color1 & 0xff) + ((color2 & 0xff) * invAlpha - (color1 & 0xff) * invAlpha) / 255; opacityData[dstOffset] = newColor; } } } } CPColorFloat sampleColor(int x, int y, int dx, int dy) { LinkedList<CPColorFloat> samples = new LinkedList<CPColorFloat>(); CPLayer layerToSample = sampleAllLayers ? fusion : getActiveLayer(); samples .addLast(new CPColorFloat(layerToSample.getPixel(x, y) & 0xffffff)); for (float r = 0.25f; r < 1.001f; r += .25f) { samples.addLast(new CPColorFloat(layerToSample.getPixel((int) (x + r * dx), y) & 0xffffff)); samples.addLast(new CPColorFloat(layerToSample.getPixel((int) (x - r * dx), y) & 0xffffff)); samples.addLast(new CPColorFloat(layerToSample.getPixel(x, (int) (y + r * dy)) & 0xffffff)); samples.addLast(new CPColorFloat(layerToSample.getPixel(x, (int) (y - r * dy)) & 0xffffff)); samples.addLast(new CPColorFloat(layerToSample.getPixel((int) (x + r * .7f * dx), (int) (y + r * .7f * dy)) & 0xffffff)); samples.addLast(new CPColorFloat(layerToSample.getPixel((int) (x + r * .7f * dx), (int) (y - r * .7f * dy)) & 0xffffff)); samples.addLast(new CPColorFloat(layerToSample.getPixel((int) (x - r * .7f * dx), (int) (y + r * .7f * dy)) & 0xffffff)); samples.addLast(new CPColorFloat(layerToSample.getPixel((int) (x - r * .7f * dx), (int) (y - r * .7f * dy)) & 0xffffff)); } CPColorFloat average = new CPColorFloat(0, 0, 0); for (CPColorFloat sample : samples) { average.r += sample.r; average.g += sample.g; average.b += sample.b; } average.r /= samples.size(); average.g /= samples.size(); average.b /= samples.size(); return average; } } class CPBrushToolOil extends CPBrushToolDirectBrush { @Override void paintDabImplementation(CPRect srcRect, CPRect dstRect, CPBrushDab dab) { if (brushBuffer == null) { brushBuffer = new int[dab.width * dab.height]; for (int i = brushBuffer.length - 1; --i >= 0;) brushBuffer[i] = 0; // curLayer.copyRect(dstRect, brushBuffer); oilAccumBuffer(srcRect, dstRect, brushBuffer, dab.width, 255); } else { oilResatBuffer( srcRect, dstRect, brushBuffer, dab.width, (int) (curBrush.resat <= 0f ? 0 : Math.max(1, curBrush.resat * curBrush.resat * 255)), curColor & 0xffffff); oilPasteBuffer(srcRect, dstRect, brushBuffer, dab.brush, dab.width, dab.alpha); oilAccumBuffer(srcRect, dstRect, brushBuffer, dab.width, (int) (curBrush.bleed * 255)); } mergeOpacityBuffer(0, false); if (sampleAllLayers) fusionLayers(); } private void oilAccumBuffer(CPRect srcRect, CPRect dstRect, int[] buffer, int w, int alpha) { CPLayer layerToSample = sampleAllLayers ? fusion : getActiveLayer(); int by = srcRect.top; for (int j = dstRect.top; j < dstRect.bottom; j++, by++) { int srcOffset = srcRect.left + by * w; int dstOffset = dstRect.left + j * width; for (int i = dstRect.left; i < dstRect.right; i++, srcOffset++, dstOffset++) { int color1 = layerToSample.data[dstOffset]; int alpha1 = (color1 >>> 24) * alpha / 255; if (alpha1 <= 0) continue; int color2 = buffer[srcOffset]; int alpha2 = color2 >>> 24; int newAlpha = alpha1 + alpha2 - alpha1 * alpha2 / 255; if (newAlpha > 0) { int realAlpha = alpha1 * 255 / newAlpha; int invAlpha = 255 - realAlpha; int newColor = newAlpha << 24 | (color1 >>> 16 & 0xff) + ((color2 >>> 16 & 0xff) * invAlpha - (color1 >>> 16 & 0xff) * invAlpha) / 255 << 16 | (color1 >>> 8 & 0xff) + ((color2 >>> 8 & 0xff) * invAlpha - (color1 >>> 8 & 0xff) * invAlpha) / 255 << 8 | (color1 & 0xff) + ((color2 & 0xff) * invAlpha - (color1 & 0xff) * invAlpha) / 255; buffer[srcOffset] = newColor; } } } } private void oilResatBuffer(CPRect srcRect, CPRect dstRect, int[] buffer, int w, int alpha1, int color1) { if (alpha1 <= 0) return; int by = srcRect.top; for (int j = dstRect.top; j < dstRect.bottom; j++, by++) { int srcOffset = srcRect.left + by * w; @SuppressWarnings("unused") int dstOffset = dstRect.left + j * width; for (int i = dstRect.left; i < dstRect.right; i++, srcOffset++, dstOffset++) { int color2 = buffer[srcOffset]; int alpha2 = color2 >>> 24; int newAlpha = alpha1 + alpha2 - alpha1 * alpha2 / 255; if (newAlpha > 0) { int realAlpha = alpha1 * 255 / newAlpha; int invAlpha = 255 - realAlpha; int newColor = newAlpha << 24 | (color1 >>> 16 & 0xff) + ((color2 >>> 16 & 0xff) * invAlpha - (color1 >>> 16 & 0xff) * invAlpha) / 255 << 16 | (color1 >>> 8 & 0xff) + ((color2 >>> 8 & 0xff) * invAlpha - (color1 >>> 8 & 0xff) * invAlpha) / 255 << 8 | (color1 & 0xff) + ((color2 & 0xff) * invAlpha - (color1 & 0xff) * invAlpha) / 255; buffer[srcOffset] = newColor; } } } } private void oilPasteBuffer(CPRect srcRect, CPRect dstRect, int[] buffer, byte[] brush, int w, int alpha) { int[] opacityData = opacityBuffer.data; int by = srcRect.top; for (int j = dstRect.top; j < dstRect.bottom; j++, by++) { int srcOffset = srcRect.left + by * w; int dstOffset = dstRect.left + j * width; for (int i = dstRect.left; i < dstRect.right; i++, srcOffset++, dstOffset++) { int color1 = buffer[srcOffset]; int alpha1 = (color1 >>> 24) * (brush[srcOffset] & 0xff) * alpha / (255 * 255); if (alpha1 <= 0) continue; int color2 = curLayer.data[dstOffset]; int alpha2 = color2 >>> 24; int newAlpha = alpha1 + alpha2 - alpha1 * alpha2 / 255; if (newAlpha > 0) { int realAlpha = alpha1 * 255 / newAlpha; int invAlpha = 255 - realAlpha; int newColor = newAlpha << 24 | (color1 >>> 16 & 0xff) + ((color2 >>> 16 & 0xff) * invAlpha - (color1 >>> 16 & 0xff) * invAlpha) / 255 << 16 | (color1 >>> 8 & 0xff) + ((color2 >>> 8 & 0xff) * invAlpha - (color1 >>> 8 & 0xff) * invAlpha) / 255 << 8 | (color1 & 0xff) + ((color2 & 0xff) * invAlpha - (color1 & 0xff) * invAlpha) / 255; opacityData[dstOffset] = newColor; } } } } } class CPBrushToolSmudge extends CPBrushToolDirectBrush { @Override void paintDabImplementation(CPRect srcRect, CPRect dstRect, CPBrushDab dab) { if (brushBuffer == null) { brushBuffer = new int[dab.width * dab.height]; smudgeAccumBuffer(srcRect, dstRect, brushBuffer, dab.width, 0); } else { smudgeAccumBuffer(srcRect, dstRect, brushBuffer, dab.width, dab.alpha); smudgePasteBuffer(srcRect, dstRect, brushBuffer, dab.brush, dab.width, dab.alpha); if (lockAlpha) restoreAlpha(dstRect); } opacityArea.makeEmpty(); if (sampleAllLayers) fusionLayers(); } @Override public void mergeOpacityBuf(CPRect dstRect, int color) { } private void smudgeAccumBuffer(CPRect srcRect, CPRect dstRect, int[] buffer, int w, int alpha) { CPLayer layerToSample = sampleAllLayers ? fusion : getActiveLayer(); int by = srcRect.top; for (int j = dstRect.top; j < dstRect.bottom; j++, by++) { int srcOffset = srcRect.left + by * w; int dstOffset = dstRect.left + j * width; for (int i = dstRect.left; i < dstRect.right; i++, srcOffset++, dstOffset++) { int layerColor = layerToSample.data[dstOffset]; int opacityAlpha = 255 - alpha; if (opacityAlpha > 0) { int destColor = buffer[srcOffset]; int destAlpha = 255; int newLayerAlpha = opacityAlpha + destAlpha * (255 - opacityAlpha) / 255; int realAlpha = 255 * opacityAlpha / newLayerAlpha; int invAlpha = 255 - realAlpha; int newColor = ((layerColor >>> 24 & 0xff) * realAlpha + (destColor >>> 24 & 0xff) * invAlpha) / 255 << 24 & 0xff000000 | ((layerColor >>> 16 & 0xff) * realAlpha + (destColor >>> 16 & 0xff) * invAlpha) / 255 << 16 & 0xff0000 | ((layerColor >>> 8 & 0xff) * realAlpha + (destColor >>> 8 & 0xff) * invAlpha) / 255 << 8 & 0xff00 | ((layerColor & 0xff) * realAlpha + (destColor & 0xff) * invAlpha) / 255 & 0xff; if (newColor == destColor) { if ((layerColor & 0xff0000) > (destColor & 0xff0000)) newColor += 1 << 16; else if ((layerColor & 0xff0000) < (destColor & 0xff0000)) newColor -= 1 << 16; if ((layerColor & 0xff00) > (destColor & 0xff00)) newColor += 1 << 8; else if ((layerColor & 0xff00) < (destColor & 0xff00)) newColor -= 1 << 8; if ((layerColor & 0xff) > (destColor & 0xff)) newColor += 1; else if ((layerColor & 0xff) < (destColor & 0xff)) newColor -= 1; } buffer[srcOffset] = newColor; } } } if (srcRect.left > 0) { int fill = srcRect.left; for (int j = srcRect.top; j < srcRect.bottom; j++) { int offset = j * w; int fillColor = buffer[offset + srcRect.left]; for (int i = 0; i < fill; i++) buffer[offset++] = fillColor; } } if (srcRect.right < w) { int fill = w - srcRect.right; for (int j = srcRect.top; j < srcRect.bottom; j++) { int offset = j * w + srcRect.right; int fillColor = buffer[offset - 1]; for (int i = 0; i < fill; i++) buffer[offset++] = fillColor; } } for (int j = 0; j < srcRect.top; j++) System.arraycopy(buffer, srcRect.top * w, buffer, j * w, w); for (int j = srcRect.bottom; j < w; j++) System.arraycopy(buffer, (srcRect.bottom - 1) * w, buffer, j * w, w); } private void smudgePasteBuffer(CPRect srcRect, CPRect dstRect, int[] buffer, byte[] brush, int w, int alpha) { int[] undoData = undoBuffer.data; int by = srcRect.top; for (int j = dstRect.top; j < dstRect.bottom; j++, by++) { int srcOffset = srcRect.left + by * w; int dstOffset = dstRect.left + j * width; for (int i = dstRect.left; i < dstRect.right; i++, srcOffset++, dstOffset++) { int bufferColor = buffer[srcOffset]; int opacityAlpha = (bufferColor >>> 24) * (brush[srcOffset] & 0xff) / 255; if (opacityAlpha > 0) { int destColor = undoData[dstOffset]; int realAlpha = 255; int invAlpha = 255 - realAlpha; int newColor = ((bufferColor >>> 24 & 0xff) * realAlpha + (destColor >>> 24 & 0xff) * invAlpha) / 255 << 24 & 0xff000000 | ((bufferColor >>> 16 & 0xff) * realAlpha + (destColor >>> 16 & 0xff) * invAlpha) / 255 << 16 & 0xff0000 | ((bufferColor >>> 8 & 0xff) * realAlpha + (destColor >>> 8 & 0xff) * invAlpha) / 255 << 8 & 0xff00 | ((bufferColor & 0xff) * realAlpha + (destColor & 0xff) * invAlpha) / 255 & 0xff; curLayer.data[dstOffset] = newColor; } } } } } // /////////////////////////////////////////////////////////////////////////////////// // Layer methods // /////////////////////////////////////////////////////////////////////////////////// public void setActiveLayer(int i) { if (i < 0 || i >= layers.size()) return; activeLayer = i; curLayer = layers.get(i); callListenersLayerChange(); } public int getActiveLayerNb() { return activeLayer; } public CPLayer getActiveLayer() { return curLayer; } public CPLayer getLayer(int i) { if (i < 0 || i >= layers.size()) return null; return layers.get(i); } // // Undo / Redo // public void undo() { if (!canUndo()) return; CPUndo undo = undoList.removeFirst(); undo.undo(); redoList.addFirst(undo); } public void redo() { if (!canRedo()) return; CPUndo redo = redoList.removeFirst(); redo.redo(); undoList.addFirst(redo); } public boolean canUndo() { return !undoList.isEmpty(); } public boolean canRedo() { return !redoList.isEmpty(); } private void addUndo(CPUndo undo) { if (undoList.isEmpty() || !undoList.getFirst().merge(undo)) { if (undoList.size() >= maxUndo) undoList.removeLast(); undoList.addFirst(undo); } else // Two merged changes can mean no change at all // don't leave a useless undo in the list if (undoList.getFirst().noChange()) undoList.removeFirst(); if (!redoList.isEmpty()) redoList = new LinkedList<CPUndo>(); } public void clearHistory() { undoList = new LinkedList<CPUndo>(); redoList = new LinkedList<CPUndo>(); Runtime r = Runtime.getRuntime(); r.gc(); } // // // public int colorPicker(float x, float y) { // not really necessary and could potentially the repaint // of the canvas to miss that area // fusionLayers(); return fusion.getPixel((int) x, (int) y) & 0xffffff; } public boolean isPointWithin(float x, float y) { return x >= 0 && y >= 0 && (int) x < width && (int) y < height; } // FIXME: 2007-01-13 I'm moving this to the CPRect class // find where this version is used and change the // code to use the CPRect version public void clipSourceDest(CPRect srcRect, CPRect dstRect) { // FIXME: // /!\ dstRect bottom and right are ignored and instead we clip // against the width, height of the layer. :/ // // this version would be enough in most cases (when we don't need // srcRect bottom and right to be clipped) // it's left here in case it's needed to make a faster version // of this function // dstRect.right = Math.min(width, dstRect.left + srcRect.getWidth()); // dstRect.bottom = Math.min(height, dstRect.top + srcRect.getHeight()); // new dest bottom/right dstRect.right = dstRect.left + srcRect.getWidth(); if (dstRect.right > width) { srcRect.right -= dstRect.right - width; dstRect.right = width; } dstRect.bottom = dstRect.top + srcRect.getHeight(); if (dstRect.bottom > height) { srcRect.bottom -= dstRect.bottom - height; dstRect.bottom = height; } // new src top/left if (dstRect.left < 0) { srcRect.left -= dstRect.left; dstRect.left = 0; } if (dstRect.top < 0) { srcRect.top -= dstRect.top; dstRect.top = 0; } } public Object[] getLayers() { return layers.toArray(); } public int getLayersNb() { return layers.size(); } public CPRect getSize() { return new CPRect(width, height); } // // Selection methods // // Gets the current selection rect or a rectangle covering the whole canvas if // there are no selections public CPRect getSelectionAutoSelect() { CPRect r; if (!curSelection.isEmpty()) r = (CPRect) curSelection.clone(); else r = getSize(); return r; } // Gets the current selection rect public CPRect getSelection() { return (CPRect) curSelection.clone(); } void setSelection(CPRect r) { curSelection.set(r); curSelection.clip(getSize()); } void emptySelection() { curSelection.makeEmpty(); } // // // public void invalidateFusion(CPRect r) { fusionArea.union(r); callListenersUpdateRegion(r); } public void invalidateFusion() { invalidateFusion(new CPRect(0, 0, width, height)); } public void setLayerVisibility(int layer, boolean visible) { addUndo(new CPUndoLayerVisible(layer, getLayer(layer).visible, visible)); getLayer(layer).visible = visible; invalidateFusion(); callListenersLayerChange(); } public void addLayer() { addUndo(new CPUndoAddLayer(activeLayer)); CPLayer newLayer = new CPLayer(width, height); newLayer.name = getDefaultLayerName(); layers.add(activeLayer + 1, newLayer); setActiveLayer(activeLayer + 1); invalidateFusion(); callListenersLayerChange(); } public void removeLayer() { if (layers.size() > 1) { addUndo(new CPUndoRemoveLayer(activeLayer, curLayer)); layers.remove(activeLayer); setActiveLayer(activeLayer < layers.size() ? activeLayer : activeLayer - 1); invalidateFusion(); callListenersLayerChange(); } } public void duplicateLayer() { String copySuffix = " Copy"; addUndo(new CPUndoDuplicateLayer(activeLayer)); CPLayer newLayer = new CPLayer(width, height); newLayer.copyFrom(layers.elementAt(activeLayer)); if (!newLayer.name.endsWith(copySuffix)) newLayer.name += copySuffix; layers.add(activeLayer + 1, newLayer); setActiveLayer(activeLayer + 1); invalidateFusion(); callListenersLayerChange(); } public void mergeDown(boolean createUndo) { if (layers.size() > 0 && activeLayer > 0) { if (createUndo) addUndo(new CPUndoMergeDownLayer(activeLayer)); layers.elementAt(activeLayer).fusionWithFullAlpha( layers.elementAt(activeLayer - 1), new CPRect(width, height)); layers.remove(activeLayer); setActiveLayer(activeLayer - 1); invalidateFusion(); callListenersLayerChange(); } } public void mergeAllLayers(boolean createUndo) { if (layers.size() > 1) { if (createUndo) addUndo(new CPUndoMergeAllLayers()); fusionLayers(); layers.clear(); CPLayer layer = new CPLayer(width, height); layer.name = getDefaultLayerName(); layer.copyDataFrom(fusion); layers.add(layer); setActiveLayer(0); invalidateFusion(); callListenersLayerChange(); } } public void moveLayer(int from, int to) { if (from < 0 || from >= getLayersNb() || to < 0 || to > getLayersNb() || from == to) return; addUndo(new CPUndoMoveLayer(from, to)); moveLayerReal(from, to); } private void moveLayerReal(int from, int to) { CPLayer layer = layers.remove(from); if (to <= from) { layers.add(to, layer); setActiveLayer(to); } else { layers.add(to - 1, layer); setActiveLayer(to - 1); } invalidateFusion(); callListenersLayerChange(); } public void setLayerAlpha(int layer, int alpha) { if (getLayer(layer).getAlpha() != alpha) { addUndo(new CPUndoLayerAlpha(layer, alpha)); getLayer(layer).setAlpha(alpha); invalidateFusion(); callListenersLayerChange(); } } public void setBlendMode(int layer, int blendMode) { if (getLayer(layer).getBlendMode() != blendMode) { addUndo(new CPUndoLayerMode(layer, blendMode)); getLayer(layer).setBlendMode(blendMode); invalidateFusion(); callListenersLayerChange(); } } public void setLayerName(int layer, String name) { if (getLayer(layer).name != name) { addUndo(new CPUndoLayerRename(layer, name)); getLayer(layer).name = name; callListenersLayerChange(); } } public void floodFill(float x, float y) { undoBuffer.copyFrom(curLayer); undoArea = new CPRect(width, height); curLayer.floodFill((int) x, (int) y, curColor | 0xff000000); addUndo(new CPUndoPaint()); invalidateFusion(); } public void fill(int color) { CPRect r = getSelectionAutoSelect(); undoBuffer.copyFrom(curLayer); undoArea = r; curLayer.clear(r, color); addUndo(new CPUndoPaint()); invalidateFusion(); } public void clear() { fill(0xffffff); } public void hFlip() { CPRect r = getSelectionAutoSelect(); undoBuffer.copyFrom(curLayer); undoArea = r; curLayer.copyRegionHFlip(r, undoBuffer); addUndo(new CPUndoPaint()); invalidateFusion(); } public void vFlip() { CPRect r = getSelectionAutoSelect(); undoBuffer.copyFrom(curLayer); undoArea = r; curLayer.copyRegionVFlip(r, undoBuffer); addUndo(new CPUndoPaint()); invalidateFusion(); } public void monochromaticNoise() { CPRect r = getSelectionAutoSelect(); undoBuffer.copyFrom(curLayer); undoArea = r; curLayer.fillWithNoise(r); addUndo(new CPUndoPaint()); invalidateFusion(); } public void colorNoise() { CPRect r = getSelectionAutoSelect(); undoBuffer.copyFrom(curLayer); undoArea = r; curLayer.fillWithColorNoise(r); addUndo(new CPUndoPaint()); invalidateFusion(); } public void boxBlur(int radiusX, int radiusY, int iterations) { CPRect r = getSelectionAutoSelect(); undoBuffer.copyFrom(curLayer); undoArea = r; for (int c = 0; c < iterations; c++) curLayer.boxBlur(r, radiusX, radiusY); addUndo(new CPUndoPaint()); invalidateFusion(); } public void invert() { CPRect r = getSelectionAutoSelect(); undoBuffer.copyFrom(curLayer); undoArea = r; curLayer.invert(r); addUndo(new CPUndoPaint()); invalidateFusion(); } public void rectangleSelection(CPRect r) { CPRect newSelection = (CPRect) r.clone(); newSelection.clip(getSize()); addUndo(new CPUndoRectangleSelection(getSelection(), newSelection)); setSelection(newSelection); } public void beginPreviewMode(boolean copy) { // !!!! awful awful hack !!! will break as soon as CPMultiUndo is used for // other things // FIXME: ASAP! if (!copy && !undoList.isEmpty() && redoList.isEmpty() && undoList.getFirst() instanceof CPMultiUndo && ((CPMultiUndo) undoList.getFirst()).undoes[0] instanceof CPUndoPaint && ((CPUndoPaint) ((CPMultiUndo) undoList.getFirst()).undoes[0]).layer == getActiveLayerNb()) { undo(); copy = prevModeCopy; } else { movePrevX = 0; movePrevY = 0; undoBuffer.copyFrom(curLayer); undoArea.makeEmpty(); opacityBuffer.clear(); opacityArea.makeEmpty(); } moveInitSelect = null; moveModeCopy = copy; } public void endPreviewMode() { CPUndo undo = new CPUndoPaint(); if (moveInitSelect != null) { CPUndo[] undoArray = { undo, new CPUndoRectangleSelection(moveInitSelect, getSelection()) }; undo = new CPMultiUndo(undoArray); } else { // !!!!!! // FIXME: this is required just to make the awful move hack work CPUndo[] undoArray = { undo }; undo = new CPMultiUndo(undoArray); } addUndo(undo); moveInitSelect = null; movePrevX = movePrevX2; movePrevY = movePrevY2; prevModeCopy = moveModeCopy; } // temp awful hack CPRect moveInitSelect = null; int movePrevX, movePrevY, movePrevX2, movePrevY2; boolean moveModeCopy, prevModeCopy; public void move(int offsetX, int offsetY) { CPRect srcRect; offsetX += movePrevX; offsetY += movePrevY; if (moveInitSelect == null) { srcRect = getSelectionAutoSelect(); if (!getSelection().isEmpty()) moveInitSelect = getSelection(); } else srcRect = (CPRect) moveInitSelect.clone(); curLayer.copyFrom(undoBuffer); if (!moveModeCopy) curLayer.clear(srcRect, 0); curLayer.pasteAlphaRect(undoBuffer, srcRect, srcRect.left + offsetX, srcRect.top + offsetY); undoArea = new CPRect(); if (!moveModeCopy) undoArea.union(srcRect); srcRect.translate(offsetX, offsetY); undoArea.union(srcRect); invalidateFusion(); if (moveInitSelect != null) { CPRect sel = (CPRect) moveInitSelect.clone(); sel.translate(offsetX, offsetY); setSelection(sel); } // this is a really bad idea :D movePrevX2 = offsetX; movePrevY2 = offsetY; } // //// // Copy/Paste public void cutSelection(boolean createUndo) { CPRect sel = getSelection(); if (sel.isEmpty()) return; clipboard = new CPClip(new CPColorBmp(curLayer, sel), sel.left, sel.top); if (createUndo) addUndo(new CPUndoCut(clipboard.bmp, sel.left, sel.top, getActiveLayerNb(), sel)); curLayer.clear(sel, 0); invalidateFusion(); } public void copySelection() { CPRect sel = getSelection(); if (sel.isEmpty()) return; clipboard = new CPClip(new CPColorBmp(curLayer, sel), sel.left, sel.top); } public void copySelectionMerged() { CPRect sel = getSelection(); if (sel.isEmpty()) return; // make sure the fusioned picture is up to date fusionLayers(); clipboard = new CPClip(new CPColorBmp(fusion, sel), sel.left, sel.top); } public void pasteClipboard(boolean createUndo) { if (clipboard != null) pasteClip(createUndo, clipboard); } public void pasteClip(boolean createUndo, CPClip clip) { if (createUndo) addUndo(new CPUndoPaste(clip, getActiveLayerNb(), getSelection())); // FIXME: redundant code, should use AddLayer's code?? CPLayer newLayer = new CPLayer(width, height); newLayer.name = getDefaultLayerName(); layers.add(activeLayer + 1, newLayer); setActiveLayer(activeLayer + 1); CPRect r = clip.bmp.getSize(); int x, y; if (r.isInside(getSize())) { x = clip.x; y = clip.y; } else { x = (width - clip.bmp.width) / 2; y = (height - clip.bmp.height) / 2; } curLayer.pasteBitmap(clip.bmp, x, y); emptySelection(); invalidateFusion(); callListenersLayerChange(); } // //////////////////////////////////////////////////// // Miscellaneous functions public String getDefaultLayerName() { String prefix = context.getString(idv.jlchntoz.oekakimobile.R.string.layer); int highestLayerNb = 0; for (CPLayer l : layers) if (l.name.matches("^" + prefix + "[0-9]+$")) highestLayerNb = Math.max(highestLayerNb, Integer.parseInt(l.name.substring(prefix.length()))); return prefix + (highestLayerNb + 1); } public boolean hasAlpha() { return fusion.hasAlpha(); } // //////////////////////////////////////////////////// // Undo classes class CPUndoPaint extends CPUndo { int layer; CPRect rect; int[] data; public CPUndoPaint() { layer = getActiveLayerNb(); rect = new CPRect(undoArea); data = undoBuffer.copyRectXOR(curLayer, rect); undoArea.makeEmpty(); } @Override public void undo() { getLayer(layer).setRectXOR(data, rect); invalidateFusion(rect); } @Override public void redo() { getLayer(layer).setRectXOR(data, rect); invalidateFusion(rect); } @Override public long getMemoryUsed(boolean undone, Object param) { return data.length * 4; } } class CPUndoLayerVisible extends CPUndo { int layer; boolean oldVis, newVis; public CPUndoLayerVisible(int layer, boolean oldVis, boolean newVis) { this.layer = layer; this.oldVis = oldVis; this.newVis = newVis; } @Override public void redo() { getLayer(layer).visible = newVis; invalidateFusion(); callListenersLayerChange(); } @Override public void undo() { getLayer(layer).visible = oldVis; invalidateFusion(); callListenersLayerChange(); } @Override public boolean merge(CPUndo u) { if (u instanceof CPUndoLayerVisible && layer == ((CPUndoLayerVisible) u).layer) { newVis = ((CPUndoLayerVisible) u).newVis; return true; } return false; } @Override public boolean noChange() { return oldVis == newVis; } } class CPUndoAddLayer extends CPUndo { int layer; public CPUndoAddLayer(int layer) { this.layer = layer; } @Override public void undo() { layers.remove(layer + 1); setActiveLayer(layer); invalidateFusion(); callListenersLayerChange(); } @Override public void redo() { CPLayer newLayer = new CPLayer(width, height); newLayer.name = getDefaultLayerName(); layers.add(layer + 1, newLayer); setActiveLayer(layer + 1); invalidateFusion(); callListenersLayerChange(); } } class CPUndoDuplicateLayer extends CPUndo { int layer; public CPUndoDuplicateLayer(int layer) { this.layer = layer; } @Override public void undo() { layers.remove(layer + 1); setActiveLayer(layer); invalidateFusion(); callListenersLayerChange(); } @Override public void redo() { String copySuffix = " Copy"; CPLayer newLayer = new CPLayer(width, height); newLayer.copyFrom(layers.elementAt(layer)); if (!newLayer.name.endsWith(copySuffix)) newLayer.name += copySuffix; layers.add(layer + 1, newLayer); setActiveLayer(layer + 1); invalidateFusion(); callListenersLayerChange(); } } class CPUndoRemoveLayer extends CPUndo { int layer; CPLayer layerObj; public CPUndoRemoveLayer(int layer, CPLayer layerObj) { this.layer = layer; this.layerObj = layerObj; } @Override public void undo() { layers.add(layer, layerObj); setActiveLayer(layer); invalidateFusion(); callListenersLayerChange(); } @Override public void redo() { layers.remove(layer); setActiveLayer(layer < layers.size() ? layer : layer - 1); invalidateFusion(); callListenersLayerChange(); } @Override public long getMemoryUsed(boolean undone, Object param) { return undone ? 0 : width * height * 4; } } class CPUndoMergeDownLayer extends CPUndo { int layer; CPLayer layerBottom, layerTop; public CPUndoMergeDownLayer(int layer) { this.layer = layer; layerBottom = new CPLayer(width, height); layerBottom.copyFrom(layers.elementAt(layer - 1)); layerTop = layers.elementAt(layer); } @Override public void undo() { layers.elementAt(layer - 1).copyFrom(layerBottom); layers.add(layer, layerTop); setActiveLayer(layer); layerBottom = layerTop = null; invalidateFusion(); callListenersLayerChange(); } @Override public void redo() { layerBottom = new CPLayer(width, height); layerBottom.copyFrom(layers.elementAt(layer - 1)); layerTop = layers.elementAt(layer); setActiveLayer(layer); mergeDown(false); } @Override public long getMemoryUsed(boolean undone, Object param) { return undone ? 0 : width * height * 4 * 2; } } class CPUndoMergeAllLayers extends CPUndo { Vector<CPLayer> oldLayers; int oldActiveLayer; @SuppressWarnings("unchecked") public CPUndoMergeAllLayers() { oldLayers = (Vector<CPLayer>) layers.clone(); oldActiveLayer = getActiveLayerNb(); } @Override @SuppressWarnings("unchecked") public void undo() { layers = (Vector<CPLayer>) oldLayers.clone(); setActiveLayer(oldActiveLayer); invalidateFusion(); callListenersLayerChange(); } @Override public void redo() { mergeAllLayers(false); } @Override public long getMemoryUsed(boolean undone, Object param) { return undone ? 0 : oldLayers.size() * width * height * 4; } } class CPUndoMoveLayer extends CPUndo { int from, to; public CPUndoMoveLayer(int from, int to) { this.from = from; this.to = to; } @Override public void undo() { if (to <= from) moveLayerReal(to, from + 1); else moveLayerReal(to - 1, from); } @Override public void redo() { moveLayerReal(from, to); } } class CPUndoLayerAlpha extends CPUndo { int layer; int from, to; public CPUndoLayerAlpha(int layer, int alpha) { from = getLayer(layer).getAlpha(); to = alpha; this.layer = layer; } @Override public void undo() { getLayer(layer).setAlpha(from); invalidateFusion(); callListenersLayerChange(); } @Override public void redo() { getLayer(layer).setAlpha(to); invalidateFusion(); callListenersLayerChange(); } @Override public boolean merge(CPUndo u) { if (u instanceof CPUndoLayerAlpha && layer == ((CPUndoLayerAlpha) u).layer) { to = ((CPUndoLayerAlpha) u).to; return true; } return false; } @Override public boolean noChange() { return from == to; } } class CPUndoLayerMode extends CPUndo { int layer; int from, to; public CPUndoLayerMode(int layer, int mode) { from = getLayer(layer).getBlendMode(); to = mode; this.layer = layer; } @Override public void undo() { getLayer(layer).setBlendMode(from); invalidateFusion(); callListenersLayerChange(); } @Override public void redo() { getLayer(layer).setBlendMode(to); invalidateFusion(); callListenersLayerChange(); } @Override public boolean merge(CPUndo u) { if (u instanceof CPUndoLayerMode && layer == ((CPUndoLayerMode) u).layer) { to = ((CPUndoLayerMode) u).to; return true; } return false; } @Override public boolean noChange() { return from == to; } } class CPUndoLayerRename extends CPUndo { int layer; String from, to; public CPUndoLayerRename(int layer, String name) { from = getLayer(layer).name; to = name; this.layer = layer; } @Override public void undo() { getLayer(layer).name = from; callListenersLayerChange(); } @Override public void redo() { getLayer(layer).name = to; callListenersLayerChange(); } @Override public boolean merge(CPUndo u) { if (u instanceof CPUndoLayerRename && layer == ((CPUndoLayerRename) u).layer) { to = ((CPUndoLayerRename) u).to; return true; } return false; } @Override public boolean noChange() { return from.equals(to); } } class CPUndoRectangleSelection extends CPUndo { CPRect from, to; public CPUndoRectangleSelection(CPRect from, CPRect to) { this.from = (CPRect) from.clone(); this.to = (CPRect) to.clone(); } @Override public void undo() { setSelection(from); } @Override public void redo() { setSelection(to); } @Override public boolean merge(CPUndo u) { return false; } @Override public boolean noChange() { return from.equals(to); } } // used to encapsulate multiple undo operation as one class CPMultiUndo extends CPUndo { CPUndo[] undoes; public CPMultiUndo(CPUndo[] undoes) { this.undoes = undoes; } @Override public void undo() { for (int i = undoes.length - 1; i >= 0; i--) undoes[i].undo(); } @Override public void redo() { for (int i = 0; i < undoes.length; i++) undoes[i].redo(); } @Override public boolean merge(CPUndo u) { return false; } @Override public boolean noChange() { boolean noChange = true; for (int i = 0; i < undoes.length; i++) noChange = noChange && undoes[i].noChange(); return noChange; } @Override public long getMemoryUsed(boolean undone, Object param) { long total = 0; for (CPUndo undo : undoes) total += undo.getMemoryUsed(undone, param); return total; } } class CPUndoCut extends CPUndo { CPColorBmp bmp; int x, y, layer; CPRect selection; public CPUndoCut(CPColorBmp bmp, int x, int y, int layerNb, CPRect selection) { this.bmp = bmp; this.x = x; this.y = y; layer = layerNb; this.selection = (CPRect) selection.clone(); } @Override public void undo() { setActiveLayer(layer); curLayer.pasteBitmap(clipboard.bmp, x, y); setSelection(selection); invalidateFusion(); } @Override public void redo() { setActiveLayer(layer); CPRect r = bmp.getSize(); r.translate(x, y); curLayer.clear(r, 0); emptySelection(); invalidateFusion(); } @Override public long getMemoryUsed(boolean undone, Object param) { return bmp == param ? 0 : bmp.width * bmp.height * 4; } } class CPUndoPaste extends CPUndo { CPClip clip; int layer; CPRect selection; public CPUndoPaste(CPClip clip, int layerNb, CPRect selection) { this.clip = clip; layer = layerNb; this.selection = (CPRect) selection.clone(); } @Override public void undo() { layers.remove(layer + 1); setActiveLayer(layer); setSelection(selection); invalidateFusion(); callListenersLayerChange(); } @Override public void redo() { setActiveLayer(layer); pasteClip(false, clip); } @Override public long getMemoryUsed(boolean undone, Object param) { return clip.bmp == param ? 0 : clip.bmp.width * clip.bmp.height * 4; } } }