package com.jotuntech.sketcher.common; import java.awt.AlphaComposite; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.image.ImageObserver; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import com.jotuntech.sketcher.common.filter.Filter; /** * @author Thor Harald Johansen * */ public class BitmapLayer implements Layer, Copyable, Serializable { static final long serialVersionUID = 3583655360668336368L; private transient final static int HASHMAP_CAPACITY = 768; private transient final static float SUBPIXEL_THRESHOLD = 16f; private transient final static float MINIMUM_SPACING = 0.04f; private transient final static float MINIMUM_RADIUS = 1.5f; private transient final static int DATA_TYPE_PIXELS = 1; private transient final static int DATA_TYPE_CLEAN = 2; private String name; private float opacity; private transient int blendMode; private Map tiles; private transient Iterator> tileIterator = null; private transient ImageObserver observer; private transient BitmapTile currentTile; private Point currentTilePoint; private transient static PixelPacker packer; private transient static PixelUnpacker unpacker; private transient static BitmapTile tempTile = new BitmapTile(); public BitmapLayer() { currentTile = null; currentTilePoint = new Point(Integer.MAX_VALUE, Integer.MAX_VALUE); blendMode = AlphaComposite.SRC_OVER; } /** * Creates a bitmap layer */ public BitmapLayer(String name) { this.name = name; this.opacity = 1; this.tiles = new LinkedHashMap(HASHMAP_CAPACITY); this.currentTile = null; this.currentTilePoint = new Point(Integer.MAX_VALUE, Integer.MAX_VALUE); this.blendMode = AlphaComposite.SRC_OVER; } public BitmapLayer(BitmapLayer layer) { /** Create new tile map */ this.tiles = new LinkedHashMap(); /** Iterate through old tile map */ for(Iterator> i = layer.tiles.entrySet().iterator(); i.hasNext();) { /** Fetch next tile entry */ Entry e = i.next(); /** Make copy of tile and put in new map */ this.tiles.put(e.getKey(), new BitmapTile(e.getValue())); } /** Copy other parameters */ this.name = layer.name; this.opacity = layer.opacity; this.blendMode = layer.blendMode; this.currentTile = layer.currentTile; this.currentTilePoint = layer.currentTilePoint; } public void draw(Graphics2D g) { AlphaComposite ac = AlphaComposite.getInstance(blendMode, opacity); g.setComposite(ac); g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED); Rectangle clip = g.getClipBounds(); for(Entry e : tiles.entrySet()) { Point k = e.getKey(); int xp = k.x << BitmapTile.SIZE_2; int yp = k.y << BitmapTile.SIZE_2; if(xp + BitmapTile.SIZE < clip.x || xp > clip.x + clip.width || yp + BitmapTile.SIZE < clip.y || yp > clip.y + clip.height) { /* Ignore tile */ } else { BitmapTile v = e.getValue(); g.drawImage(v.getImage(), xp, yp, null); } } } public void draw(Graphics2D g, Set phantomLayers) { AlphaComposite ac = AlphaComposite.getInstance(blendMode, opacity); g.setComposite(ac); g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED); Rectangle clip = g.getClipBounds(); Set ps = new HashSet(); ps.addAll(tiles.keySet()); for(Layer phl : phantomLayers) { BitmapLayer bphl = (BitmapLayer) phl; ps.addAll(bphl.tiles.keySet()); } for(Point k : ps) { int xp = k.x << BitmapTile.SIZE_2; int yp = k.y << BitmapTile.SIZE_2; if(xp + BitmapTile.SIZE < clip.x || xp > clip.x + clip.width || yp + BitmapTile.SIZE < clip.y || yp > clip.y + clip.height) { /* Ignore tile */ } else { if(!phantomLayers.isEmpty()) { Graphics2D tg = (Graphics2D) tempTile.getImage().getGraphics(); /** Clear temporary tile */ ac = AlphaComposite.getInstance(AlphaComposite.DST_OUT, 1); tg.setComposite(ac); tg.fillRect(0, 0, BitmapTile.SIZE, BitmapTile.SIZE); /** Draw destination on tile */ BitmapTile dt = tiles.get(k); if(dt != null) { ac = AlphaComposite.getInstance(AlphaComposite.SRC, 1); tg.setComposite(ac); tg.drawImage(dt.getImage(), 0, 0, null); } /** Draw all phantom layers on tile */ for(Layer phl : phantomLayers) { BitmapLayer bphl = (BitmapLayer) phl; BitmapTile pht = bphl.getTiles().get(k); if(pht != null) { AlphaComposite dac = AlphaComposite.getInstance(bphl.blendMode, bphl.opacity); tg.setComposite(dac); tg.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED); tg.drawImage(pht.getImage(), 0, 0, null); } } g.drawImage(tempTile.getImage(), xp, yp, null); } else { BitmapTile dt = tiles.get(k); if(dt != null) { g.drawImage(dt.getImage(), xp, yp, null); } } } } } public float getOpacity() { return opacity; } public Layer copy() { return new BitmapLayer(this); } public void clear() { tiles = new LinkedHashMap(HASHMAP_CAPACITY); currentTile = null; currentTilePoint = new Point(Integer.MAX_VALUE, Integer.MAX_VALUE); if(observer != null) { observer.imageUpdate(null, ImageObserver.ALLBITS, 0, 0, 0, 0); } } public Input line(Input start, Input end, int color, Brush brush, Layer originalLayer) { /* Calculate final radius */ float radius = Math.max(MINIMUM_RADIUS, brush.getRadius() * (brush.isPressureToRadius() ? start.pressure : 0x100) / 256f); if(end == null) { /** Paint daub */ daub(start, brush, color, (BitmapLayer) originalLayer); /** Return point */ return start; } else { /** Find line length. */ float length = (float)Math.hypot(end.x - start.x, end.y - start.y); /** Calculate delta */ float delta = Math.max(1, radius * 2 * Math.max(MINIMUM_SPACING, brush.getSpacing())); /** Set starting point. */ Input input = start; /** Set paint status. */ boolean hasDrawn = false; /** Store area for repaint */ Rectangle dirty = null; for(float t = delta; t <= length; t += delta) { /** Calculate new input point. */ input = new Input( start.x + (end.x - start.x) * t / length, start.y + (end.y - start.y) * t / length, (int)(start.pressure + (end.pressure - start.pressure) * t / length)); /** Calculate final radius */ radius = Math.max(MINIMUM_RADIUS, brush.getRadius() * (brush.isPressureToRadius() ? input.pressure : 0x100) / 256f); /** Calculate delta */ delta = Math.max(1, radius * 2 * Math.max(MINIMUM_SPACING, brush.getSpacing())); /** Paint daub */ Rectangle dirtyDaub = daub(input, brush, color, (BitmapLayer) originalLayer); if(dirty == null) { dirty = dirtyDaub; } else { dirty = dirty.union(dirtyDaub); } hasDrawn = true; } if(hasDrawn && observer != null) { observer.imageUpdate(null, ImageObserver.SOMEBITS, dirty.x, dirty.y, dirty.width, dirty.height); } return hasDrawn ? input : null; } } public int getColor(Input input) { int pixel = getPixel(Math.round(input.x), Math.round(input.y)); return Pixels.lerp(0xFFFFFF, pixel & 0xFFFFFF, Pixels.getChannel0(pixel)); } public int getColor(int x, int y) { int pixel = getPixel(x, y); return Pixels.lerp(0xFFFFFF, pixel & 0xFFFFFF, Pixels.getChannel0(pixel)); } public int getColor(int x, int y, int background) { int pixel = getPixel(x, y); return Pixels.lerp(background, pixel & 0xFFFFFF, Pixels.getChannel0(pixel)); } int getPixel(int x, int y) { BitmapTile tile = getBitmapTile(x, y, false); if(tile == null) { return 0x00000000; } else { int tileX = x % BitmapTile.SIZE; int tileY = y % BitmapTile.SIZE; return tile.getPixels()[tileY * BitmapTile.SIZE + tileX]; } } public void addImageObserver(ImageObserver observer) { this.observer = observer; } public ImageObserver getImageObserver() { return this.observer; } public int getType() { return LayerType.BITMAP; } public void setName(String name) { this.name = name; } public String getName() { return this.name; } private BitmapTile getBitmapTile(int x, int y, boolean writable) { int tileX = x >> BitmapTile.SIZE_2; int tileY = y >> BitmapTile.SIZE_2; if(tileX != currentTilePoint.x || tileY != currentTilePoint.y) { currentTilePoint = new Point(tileX, tileY); currentTile = tiles.get(currentTilePoint); } if(writable) { if(currentTile == null) { currentTile = new BitmapTile(); tiles.put(currentTilePoint, currentTile); } else { currentTile.setEncoded(null); } } return currentTile; } private void blendPixel(int x, int y, int sourceColor, int sourceAlpha, int maxAlpha) { if(sourceAlpha != 0 && x >= 0 && y >= 0) { BitmapTile tile = getBitmapTile(x, y, true); if(tile != null) { tile.blendPixel(x & BitmapTile.SIZE_MASK, y & BitmapTile.SIZE_MASK, sourceColor, sourceAlpha, maxAlpha); } } } public Rectangle daub(Input input, Brush brush, int color, BitmapLayer originalLayer) { float radius = Math.max(MINIMUM_RADIUS, brush.isPressureToRadius() ? (brush.getRadius() * input.pressure) / 255f : brush.getRadius()); float x = input.x + (float)(Math.random() - 0.5) * radius * brush.getJitter(); float y = input.y + (float)(Math.random() - 0.5) * radius * brush.getJitter(); /** X & Y, integer */ int floorX = (int) Math.floor(x); int floorY = (int) Math.floor(y); if(brush.isLockTransparency() && brush.getOpacity() <= 0) { return new Rectangle(floorX, floorY, 0, 0); } int maxAlpha = brush.isPressureToOpacity() ? input.pressure : 255; int flow = Math.min(255, brush.isPressureToFlow() ? (brush.getFlow() * maxAlpha * input.pressure) / 65025 : (brush.getFlow() * maxAlpha) / 255); int noise = brush.getNoise(); /** Radius, rounded up */ int ceilRadius = (int)Math.ceil(radius) + 1; int ceilRadius255 = ceilRadius * 255; /** Radius, squared and premultiplied */ int radiusSquared255 = (int)(radius * radius * 255f); int radiusSquared65025 = (int)(radius * radius * 65025f); /** X & Y, remainders */ int xMod256 = (int)(x * 255f) % 255; int yMod256 = (int)(y * 255f) % 255; /** Watercolor */ if(brush.getWater() > 0) { int rim = (int) Math.ceil(radius * brush.getWaterArea()); int diagRim = (int) Math.ceil(radius * brush.getWaterArea() * 0.70710678118654752440084436210485); int center = originalLayer.getPixel(floorX, floorY); int top = originalLayer.getPixel(floorX, floorY - rim); int topRight = originalLayer.getPixel(floorX + diagRim, floorY - diagRim); int right = originalLayer.getPixel(floorX + rim, floorY); int bottomRight = originalLayer.getPixel(floorX + diagRim, floorY + diagRim); int bottom = originalLayer.getPixel(floorX, floorY + rim); int bottomLeft = originalLayer.getPixel(floorX - diagRim, floorY + diagRim); int left = originalLayer.getPixel(floorX - rim, floorY); int topLeft = originalLayer.getPixel(floorX - diagRim, floorY - diagRim); float sampleRed = 0, sampleGreen = 0, sampleBlue = 0, sampleAlpha = 0; if(Pixels.getChannel0(center) > 0) { float alpha = Pixels.gammaDecode(Pixels.getChannel0(center)); sampleAlpha += alpha; sampleRed += Pixels.gammaDecode(Pixels.getChannel1(center)) * alpha; sampleGreen += Pixels.gammaDecode(Pixels.getChannel2(center)) * alpha; sampleBlue += Pixels.gammaDecode(Pixels.getChannel3(center)) * alpha; } if(Pixels.getChannel0(top) > 0) { float alpha = Pixels.gammaDecode(Pixels.getChannel0(top)); sampleAlpha += alpha; sampleRed += Pixels.gammaDecode(Pixels.getChannel1(top)) * alpha; sampleGreen += Pixels.gammaDecode(Pixels.getChannel2(top)) * alpha; sampleBlue += Pixels.gammaDecode(Pixels.getChannel3(top)) * alpha; } if(Pixels.getChannel0(topRight) > 0) { float alpha = Pixels.gammaDecode(Pixels.getChannel0(topRight)); sampleAlpha += alpha; sampleRed += Pixels.gammaDecode(Pixels.getChannel1(topRight)) * alpha; sampleGreen += Pixels.gammaDecode(Pixels.getChannel2(topRight)) * alpha; sampleBlue += Pixels.gammaDecode(Pixels.getChannel3(topRight)) * alpha; } if(Pixels.getChannel0(right) > 0) { float alpha = Pixels.gammaDecode(Pixels.getChannel0(right)); sampleAlpha += alpha; sampleRed += Pixels.gammaDecode(Pixels.getChannel1(right)) * alpha; sampleGreen += Pixels.gammaDecode(Pixels.getChannel2(right)) * alpha; sampleBlue += Pixels.gammaDecode(Pixels.getChannel3(right)) * alpha; } if(Pixels.getChannel0(bottomRight) > 0) { float alpha = Pixels.gammaDecode(Pixels.getChannel0(bottomRight)); sampleAlpha += alpha; sampleRed += Pixels.gammaDecode(Pixels.getChannel1(bottomRight)) * alpha; sampleGreen += Pixels.gammaDecode(Pixels.getChannel2(bottomRight)) * alpha; sampleBlue += Pixels.gammaDecode(Pixels.getChannel3(bottomRight)) * alpha; } if(Pixels.getChannel0(bottom) > 0) { float alpha = Pixels.gammaDecode(Pixels.getChannel0(bottom)); sampleAlpha += alpha; sampleRed += Pixels.gammaDecode(Pixels.getChannel1(bottom)) * alpha; sampleGreen += Pixels.gammaDecode(Pixels.getChannel2(bottom)) * alpha; sampleBlue += Pixels.gammaDecode(Pixels.getChannel3(bottom)) * alpha; } if(Pixels.getChannel0(bottomLeft) > 0) { float alpha = Pixels.gammaDecode(Pixels.getChannel0(bottomLeft)); sampleAlpha += alpha; sampleRed += Pixels.gammaDecode(Pixels.getChannel1(bottomLeft)) * alpha; sampleGreen += Pixels.gammaDecode(Pixels.getChannel2(bottomLeft)) * alpha; sampleBlue += Pixels.gammaDecode(Pixels.getChannel3(bottomLeft)) * alpha; } if(Pixels.getChannel0(left) > 0) { float alpha = Pixels.gammaDecode(Pixels.getChannel0(left)); sampleAlpha += alpha; sampleRed += Pixels.gammaDecode(Pixels.getChannel1(left)) * alpha; sampleGreen += Pixels.gammaDecode(Pixels.getChannel2(left)) * alpha; sampleBlue += Pixels.gammaDecode(Pixels.getChannel3(left)) * alpha; } if(Pixels.getChannel0(topLeft) > 0) { float alpha = Pixels.gammaDecode(Pixels.getChannel0(topLeft)); sampleAlpha += alpha; sampleRed += Pixels.gammaDecode(Pixels.getChannel1(topLeft)) * alpha; sampleGreen += Pixels.gammaDecode(Pixels.getChannel2(topLeft)) * alpha; sampleBlue += Pixels.gammaDecode(Pixels.getChannel3(topLeft)) * alpha; } float brushRed = Pixels.gammaDecode(Pixels.getChannel1(color)); float brushGreen = Pixels.gammaDecode(Pixels.getChannel2(color)); float brushBlue = Pixels.gammaDecode(Pixels.getChannel3(color)); if(sampleAlpha > 0f) { sampleRed = sampleRed / sampleAlpha; sampleGreen = sampleGreen / sampleAlpha; sampleBlue = sampleBlue / sampleAlpha; sampleAlpha /= 9f; } else { sampleRed = brushRed; sampleGreen = brushGreen; sampleBlue = brushBlue; } float resultRed = brushRed + ((sampleRed - brushRed) * brush.getWater()) / 255f; float resultGreen = brushGreen + ((sampleGreen - brushGreen) * brush.getWater()) / 255f; float resultBlue = brushBlue + ((sampleBlue - brushBlue) * brush.getWater()) / 255f; color = Pixels.pack(Pixels.gammaEncode(resultRed), Pixels.gammaEncode(resultGreen), Pixels.gammaEncode(resultBlue)); flow = ((65025 + ((Pixels.gammaEncode(sampleAlpha) - 255) * brush.getWater())) * flow) / 65025; maxAlpha = flow; } if(radius > SUBPIXEL_THRESHOLD) { /** top of Y cathetus */ int cathetusY = 0; /** Y loop */ for(int pixelY = 0; pixelY < ceilRadius; pixelY++) { /** left side of X cathetus */ int cathetusX = 0; /** Y cathetus squared */ int cathetusYSquared = cathetusY * cathetusY; /** X loop */ for(int pixelX = 0; pixelX < ceilRadius; pixelX++) { int alpha = cathetusX * cathetusX + cathetusYSquared; if(alpha < radiusSquared65025) { alpha /= radiusSquared255; alpha = 255 - alpha; switch(brush.getHardness()) { case 1: alpha = (alpha * alpha * alpha) / 255; break; case 2: alpha = alpha * alpha; break; case 3: alpha = alpha * 255; break; default: alpha <<= 4 + brush.getHardness(); if(alpha > 65025) { alpha = 65025; } break; } /** Apply noise */ alpha -= noise > 0 ? (Pixels.random() % noise) << 8 : 0; if(alpha > 0) { /** Apply flow */ alpha = (alpha * flow) / 255; /* Blend mirrored pixels. */ if(pixelX == 0 && pixelY == 0) { /* Blend center pixel */ blendPixel(floorX, floorY, color, alpha, maxAlpha); } else if(pixelX == 0) { /* Blend vertical center line */ blendPixel(floorX, floorY - pixelY, color, alpha, maxAlpha); blendPixel(floorX, floorY + pixelY, color, alpha, maxAlpha); } else if(pixelY == 0) { /* Blend horizontal center line */ blendPixel(floorX - pixelX, floorY, color, alpha, maxAlpha); blendPixel(floorX + pixelX, floorY, color, alpha, maxAlpha); } else { /* Blend other pixels */ blendPixel(floorX - pixelX, floorY - pixelY, color, alpha, maxAlpha); blendPixel(floorX + pixelX, floorY - pixelY, color, alpha, maxAlpha); blendPixel(floorX - pixelX, floorY + pixelY, color, alpha, maxAlpha); blendPixel(floorX + pixelX, floorY + pixelY, color, alpha, maxAlpha); } } } /** Increment X cathetus */ cathetusX += 255; } /** increment Y cathetus */ cathetusY += 255; } } else { /** top of Y cathetus */ int cathetusY = -ceilRadius255 - yMod256; /** Y loop */ for(int pixelY = floorY - ceilRadius; pixelY < floorY + ceilRadius; pixelY++) { /** left side of X cathetus */ int cathetusX = -ceilRadius255 - xMod256; /** Y cathetus squared */ int cathetusYSquared = cathetusY * cathetusY; /** X loop */ for(int pixelX = floorX - ceilRadius; pixelX < floorX + ceilRadius; pixelX++) { int alpha = cathetusX * cathetusX + cathetusYSquared; if(alpha < radiusSquared65025) { alpha /= radiusSquared255; alpha = 255 - alpha; switch(brush.getHardness()) { case 1: alpha = (alpha * alpha * alpha) / 255; break; case 2: alpha = alpha * alpha; break; case 3: alpha = alpha * 255; break; default: alpha <<= 4 + brush.getHardness(); if(alpha > 65025) { alpha = 65025; } break; } /** Apply noise */ alpha -= noise > 0 ? (Pixels.random() % noise) << 8 : 0; if(alpha > 0) { /** Apply flow */ alpha = (alpha * flow) / 255; /** Blend as pixel */ blendPixel(pixelX, pixelY, color, alpha, maxAlpha); } } /** Increment X cathetus */ cathetusX += 255; } /** increment Y cathetus */ cathetusY += 255; } } return new Rectangle(floorX - ceilRadius, floorY - ceilRadius, ceilRadius << 1, ceilRadius << 1); } public void clean() { List removePoints = new ArrayList(); for(Map.Entry e : tiles.entrySet()) { Point p = e.getKey(); BitmapTile t = e.getValue(); int emptyPixels = 0; int[] pixels = t.getPixels(); for(int j = pixels.length - 1; j >= 0; j--) { int alpha = Pixels.getChannel0(pixels[j]); if(alpha < 2) { ++emptyPixels; } } if(emptyPixels == pixels.length) { removePoints.add(p); } } for(Iterator i = removePoints.iterator(); i.hasNext();) { Point p = i.next(); tiles.remove(p); } } public void reset() { tileIterator = null; } public void decode(byte[] data) { int offset = 0; int dataType = data[offset++]; if(dataType == DATA_TYPE_PIXELS) { int x = data[offset++] & 0xFF; int y = data[offset++] & 0xFF; Point p = new Point(x, y); BitmapTile t = new BitmapTile(); if(unpacker == null) { unpacker = new PixelUnpacker(); } unpacker.unpack(data, offset, data.length - offset, t.getPixels()); tiles.put(p, t); if(observer == null) { Log.warn("We are supposed to have an observer when loading layers."); } else { observer.imageUpdate(null, ImageObserver.SOMEBITS, x << BitmapTile.SIZE_2, y << BitmapTile.SIZE_2, BitmapTile.SIZE, BitmapTile.SIZE); } } else if(dataType == DATA_TYPE_CLEAN) { /** Cleanup now happens internally and separately on client and server */ } else { throw new RuntimeException("Invalid tile data type " + dataType); } } private static byte[] encodedTile; public byte[] encode() { if(tileIterator == null) { tileIterator = tiles.entrySet().iterator(); } if(tileIterator.hasNext()) { Entry e = tileIterator.next(); Point p = e.getKey(); BitmapTile t = e.getValue(); byte[] encodedBytes = t.getEncoded(); if(encodedBytes == null) { if(encodedTile == null) { encodedTile = new byte[BitmapTile.SIZE * BitmapTile.SIZE * 10]; } int offset = 0; encodedTile[offset++] = DATA_TYPE_PIXELS; encodedTile[offset++] = (byte) p.x; encodedTile[offset++] = (byte) p.y; if(packer == null) { packer = new PixelPacker(); } int compressedLength = packer.pack(t.getPixels(), encodedTile, offset, encodedTile.length - offset); offset += compressedLength; encodedBytes = Arrays.copyOf(encodedTile, offset); t.setEncoded(encodedBytes); } return encodedBytes; } else { tileIterator = null; return null; } } public void setOpacity(float opacity) { this.opacity = opacity; } public boolean isEmpty() { return tiles.isEmpty(); } public Map getTiles() { return tiles; } public UndoData merge(Layer fromLayer) { if(fromLayer instanceof BitmapLayer) { BitmapLayer bl = (BitmapLayer) fromLayer; BitmapUndoData bud = new BitmapUndoData(); /** Transfer tiles */ for(Map.Entry e : bl.tiles.entrySet()) { Point p = e.getKey(); BitmapTile st = e.getValue(); // Source Tile BitmapTile dt = tiles.get(p), tt; // Destination Tile, Temporary Tile if(dt == null) { tt = new BitmapTile(); } else { tt = new BitmapTile(dt); } Graphics2D g2 = (Graphics2D) tt.getImage().getGraphics(); AlphaComposite ac = AlphaComposite.getInstance(bl.blendMode, bl.opacity); g2.setComposite(ac); g2.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED); g2.drawImage(st.getImage(), 0, 0, null); g2.dispose(); tiles.put(p, tt); BitmapTileDiff btd = new BitmapTileDiff(); int[] tp = tt.getPixels(); if(dt == null) { for(int i = 0; i < BitmapTile.SIZE * BitmapTile.SIZE; i++) { int tpi = tp[i]; btd.diffAlpha[i] = (short) Pixels.getChannel0(tpi); btd.diffRed[i] = (short) Pixels.getChannel1(tpi); btd.diffGreen[i] = (short) Pixels.getChannel2(tpi); btd.diffBlue[i] = (short) Pixels.getChannel3(tpi); } } else { int[] dp = dt.getPixels(); for(int i = 0; i < BitmapTile.SIZE * BitmapTile.SIZE; i++) { int tpi = tp[i]; int dpi = dp[i]; btd.diffAlpha[i] = (short) (Pixels.getChannel0(tpi) - Pixels.getChannel0(dpi)); btd.diffRed[i] = (short) (Pixels.getChannel1(tpi) - Pixels.getChannel1(dpi)); btd.diffGreen[i] = (short) (Pixels.getChannel2(tpi) - Pixels.getChannel2(dpi)); btd.diffBlue[i] = (short) (Pixels.getChannel3(tpi) - Pixels.getChannel3(dpi)); } } bud.put(p, btd); } bl.tiles.clear(); bl.currentTile = null; bl.currentTilePoint = new Point(Integer.MAX_VALUE, Integer.MAX_VALUE); return bud; } else { throw new RuntimeException("Cannot merge layers of different types"); } } public int getAlphaRule() { return blendMode; } public void setAlphaRule(int alphaRule) { blendMode = alphaRule; } public void undo(UndoData undoData) { if(undoData instanceof BitmapUndoData) { BitmapUndoData bud = (BitmapUndoData) undoData; Rectangle dirty = null; for(Map.Entry e : bud.entrySet()) { Point p = e.getKey(); BitmapTileDiff btd = e.getValue(); BitmapTile dt = tiles.get(p); if(dt == null) { dt = new BitmapTile(); int[] dtp = dt.getPixels(); for(int i = 0; i < BitmapTile.SIZE * BitmapTile.SIZE; i++) { dtp[i] = Pixels.pack(btd.diffAlpha[i], btd.diffRed[i], btd.diffGreen[i], btd.diffBlue[i]); } } else { int[] dtp = dt.getPixels(); for(int i = 0; i < BitmapTile.SIZE * BitmapTile.SIZE; i++) { int dtpi = dtp[i]; int alpha = Math.max(0, Math.min(255, Pixels.getChannel0(dtpi) - btd.diffAlpha[i])); if(alpha == 0) { dtp[i] = 0; } else { int red = Math.max(0, Math.min(255, Pixels.getChannel1(dtpi) - btd.diffRed[i])); int green = Math.max(0, Math.min(255, Pixels.getChannel2(dtpi) - btd.diffGreen[i])); int blue = Math.max(0, Math.min(255, Pixels.getChannel3(dtpi) - btd.diffBlue[i])); dtp[i] = Pixels.pack(alpha, red, green, blue); } } } Rectangle tileRect = new Rectangle(p.x << BitmapTile.SIZE_2, p.y << BitmapTile.SIZE_2, BitmapTile.SIZE, BitmapTile.SIZE); dirty = dirty == null ? tileRect : dirty.union(tileRect); } if(dirty != null) { observer.imageUpdate(null, ImageObserver.SOMEBITS, dirty.x, dirty.y, dirty.width, dirty.height); } } else { throw new RuntimeException("Cannot undo with data from different layer type"); } } public UndoData copyTo(Layer destination, ImageObserver obs, boolean isMove, float sx, float sy, float dx, float dy, float width, float height) { //if(destination instanceof BitmapLayer) { // throw new RuntimeException("Destination of move operation must be a bitmap layer!"); //} BitmapLayer bld = (BitmapLayer) destination; if(width == 0 || height == 0) { return null; } if(this == destination && sx == dx && sy == dy) { return null; } int startX, dStartX, stopX, dirX, startY, dStartY, stopY, dirY; if(sx > dx) { startX = Math.round(sx); dStartX = Math.round(dx); stopX = Math.round(sx + width); dirX = 1; } else { startX = Math.round(sx + width - 1); dStartX = Math.round(dx + width - 1); stopX = Math.round(sx) - 1; dirX = -1; } if(sy > dy) { startY = Math.round(sy); dStartY = Math.round(dy); stopY = Math.round(sy + height); dirY = 1; } else { startY = Math.round(sy + height - 1); dStartY = Math.round(dy + height - 1); stopY = Math.round(sy) - 1; dirY = -1; } for(int syy = startY, dyy = dStartY; syy != stopY; syy += dirY, dyy += dirY) { for(int sxx = startX, dxx = dStartX; sxx != stopX; sxx += dirX, dxx += dirX) { BitmapTile st = getBitmapTile(sxx, syy, false); if(st == null) { continue; } int[] sp = st.getPixels(); int offset = ((syy & BitmapTile.SIZE_MASK) << BitmapTile.SIZE_2) + (sxx & BitmapTile.SIZE_MASK); int sourcePixel = sp[offset]; if(isMove) { sp[offset] = 0; } BitmapTile dt = bld.getBitmapTile(dxx, dyy, true); int[] dp = dt.getPixels(); offset = ((dyy & BitmapTile.SIZE_MASK) << BitmapTile.SIZE_2) + (dxx & BitmapTile.SIZE_MASK); dp[offset] = sourcePixel; } } if(obs != null) { Rectangle r = new Rectangle(Math.round(sx), Math.round(sy), Math.round(width), Math.round(height)).union(new Rectangle(Math.round(dx), Math.round(dy), Math.round(width), Math.round(height))); obs.imageUpdate(null, ImageObserver.SOMEBITS, r.x, r.y, r.width, r.height); } return null; } public void applyFilter(Filter filter, ImageObserver obs, int x, int y, int w, int h) { filter.startPass1(); if(filter.isPass1ReadOnly()) { for(int yy = y; yy < y + h; yy++) { for(int xx = x; xx < x + w; xx++) { BitmapTile t = getBitmapTile(xx, yy, false); if(t == null) { filter.processPass1Pixel(0); } else { filter.processPass1Pixel(t.getPixels()[((yy & BitmapTile.SIZE_MASK) << BitmapTile.SIZE_2) + (xx & BitmapTile.SIZE_MASK)]); } } } } else { for(int yy = y; yy < y + h; yy++) { for(int xx = x; xx < x + w; xx++) { BitmapTile t = getBitmapTile(xx, yy, true); int[] tp = t.getPixels(); int offset = ((yy & BitmapTile.SIZE_MASK) << BitmapTile.SIZE_2) + (xx & BitmapTile.SIZE_MASK); int pixel = tp[offset]; pixel = filter.processPass1Pixel(pixel); tp[offset] = pixel; } } } if(filter.hasPass2()) { filter.startPass2(); if(filter.isPass2Reversed()) { for(int yy = y + h - 1; yy >= y; yy--) { for(int xx = x + w - 1; xx >= x; xx--) { BitmapTile t = getBitmapTile(xx, yy, true); int[] tp = t.getPixels(); int offset = ((yy & BitmapTile.SIZE_MASK) << BitmapTile.SIZE_2) + (xx & BitmapTile.SIZE_MASK); int pixel = tp[offset]; pixel = filter.processPass2Pixel(pixel); tp[offset] = pixel; } } } else { for(int yy = y; yy < y + h; yy++) { for(int xx = x; xx < x + w; xx++) { BitmapTile t = getBitmapTile(xx, yy, true); int[] tp = t.getPixels(); int offset = ((yy & BitmapTile.SIZE_MASK) << BitmapTile.SIZE_2) + (xx & BitmapTile.SIZE_MASK); int pixel = tp[offset]; pixel = filter.processPass2Pixel(pixel); tp[offset] = pixel; } } } } if(obs != null) { obs.imageUpdate(null, ImageObserver.SOMEBITS, x, y, w, h); } } }