Skip to content

Instantly share code, notes, and snippets.

@BurntPizza
Last active October 29, 2019 01:17
Show Gist options
  • Save BurntPizza/f0d2d31f843c47ea55f4 to your computer and use it in GitHub Desktop.
Save BurntPizza/f0d2d31f843c47ea55f4 to your computer and use it in GitHub Desktop.
package com.burntpizza.swift2d;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.util.Arrays;
import javax.swing.JComponent;
import javax.swing.JFrame;
import com.burntpizza.swift2d.utils.*;
/**
* * insert description here *
*
* @author BurntPizza
*
*/
public class BufferedContext {
@SuppressWarnings("serial")
private static class TestPanel extends JComponent implements MouseMotionListener {
BufferedContext bc;
LagMeter m = new LagMeter("Context + paintImmediately()", new Point(1920 / 2 - 200, 1080 / 6 * 4));
int mx, my, lx, ly;
{
setSize(1920 * 3 / 4, 1080 * 3 / 4);
setPreferredSize(getSize());
addMouseMotionListener(this);
bc = new BufferedContext(getWidth(), getHeight());
bc.setBackground(0, 0, 0);
bc.clear();
}
@Override
public void mouseDragged(MouseEvent e) {
mx = e.getX();
my = e.getY();
if (lx == 0 && ly == 0) {
lx = mx;
ly = my;
}
m.tick();
//bc.clear();
bc.setColor(128, 128, 128);
//bc.drawThickLine(mx, my, lx, ly, 200);
bc.fillCircle(mx, my, 100);
//bc.fillEllipse(mx - 100, my - 150, 200, 300);
m.tick();
//g.dispose();
lx = mx;
ly = my;
//bc.setColor(255, 0, 0);
//bc.drawRect(bc.abx, bc.aby, bc.abw, bc.abh);
paintImmediately(/*0, 0, getWidth(), getHeight()*/bc.getActiveBounds());
bc.resetActiveBounds();
}
@Override
public void mouseMoved(MouseEvent e) {
}
@Override
public void paint(Graphics g) {
g.drawImage(bc.getImage(), 0, 0, null);
}
}
public static void main(String[] a) {
JFrame j = new JFrame();
j.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
TestPanel p = new TestPanel();
j.add(p);
j.pack();
j.setVisible(true);
j.setLocationRelativeTo(null);
}
protected int abx, aby, abw, abh; // active bounds
/**
* Background color
*/
protected int bgr, bgg, bgb, bga, bgc;
protected BlendMode blendmode = new Overwrite();
protected int[] buffer; // using INT_ARGB
/**
* Current painting color
*/
// note: they are premultiplied and half-clamped, except alpha
protected int cr, cg, cb, ca, cc;
protected int crp, cgp, cbp;
protected BufferedImage image;
protected int sclipx, sclipy, sclipw, scliph;
protected boolean stencil, opStencil;
protected boolean[] stencilBuffer, opStencilBuffer;
protected int w, h;
/**
* Creates a new context wrapping image
*/
public BufferedContext(BufferedImage image) {
if (image.getType() != BufferedImage.TYPE_INT_ARGB)
throw new IllegalArgumentException("Images to be wrapped must be of type INT_ARGB");
this.image = image;
w = image.getWidth();
h = image.getHeight();
buffer = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
stencilBuffer = new boolean[buffer.length];
opStencilBuffer = new boolean[buffer.length];
abx = w;
aby = h;
}
/**
* Creates a blank context of width and height
*/
public BufferedContext(int width, int height) {
image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
w = width;
h = height;
buffer = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
stencilBuffer = new boolean[buffer.length];
opStencilBuffer = new boolean[buffer.length];
abx = w;
aby = h;
}
public void beginStencil() {
// no point in stencil if we don't need blending, so ignore
stencil = !(blendmode instanceof Overwrite);
sclipx = w;
sclipy = h;
sclipw = scliph = 0;
}
/**
* Clears the entire image to it's background color.
*/
public void clear() {
Arrays.fill(buffer, bgc);
abx = 0;
aby = 0;
abw = w;
abh = h;
}
/**
* Clears the specified region to the current background color.
* Clips automatically.
*/
public void clearRect(int x, int y, int w, int h) {
// clip to edges
final int nx = MathUtils.clamp(x, this.w, 0);
final int len = MathUtils.clamp(w, this.w - (nx - x), 0);
final int mh = MathUtils.clamp(y + h, this.h, 0);
final int bg = ColorUtils.pack(bgr, bgg, bgb, bga);
for (; y < mh; y++) {
final int np = this.w * y + nx;
Arrays.fill(buffer, np, np + len, bg); // might replace this with tmpBuffer + arraycopy
}
}
/**
* Copies the specified region of source to the same region of this image.
* Clips automatically.
* Requires that source be of type INT_ARGB.
*/
// needs testing to verify the clipping is correct
public void copyRect(BufferedImage source, int x, int y, int w, int h) {
if (source.getType() != BufferedImage.TYPE_INT_ARGB)
throw new IllegalArgumentException("Images to be copied must be of type INT_ARGB");
final int[] srcBuffer = ((DataBufferInt) source.getRaster().getDataBuffer()).getData();
// clip to edges
final int nx = MathUtils.clamp(x, Math.min(this.w, source.getWidth()), 0);
final int len = MathUtils.clamp(w, Math.min(this.w - (nx - x), source.getWidth() - (nx - x)), 0);
final int mh = MathUtils.clamp(y + h, Math.min(this.h, source.getHeight()), 0);
for (; y < mh; y++) {
System.arraycopy(srcBuffer, source.getWidth() * y + nx, buffer, this.w * y + nx, len);
}
}
public void drawCircle(int x, int y, int radius) {
//TODO: clipping
int xOff = radius, yOff = 0;
int radiusError = 1 - xOff;
while (xOff >= yOff) {
drawPixel(-yOff + x, -xOff + y); // north left top
drawPixel(yOff + x, -xOff + y); // north right top
drawPixel(-xOff + x, -yOff + y); // north left bottom
drawPixel(xOff + x, -yOff + y); // north right bottom
drawPixel(-xOff + x, yOff + y); // south left top
drawPixel(xOff + x, yOff + y); // south right top
drawPixel(-yOff + x, xOff + y); // south left bottom
drawPixel(yOff + x, xOff + y); // south right bottom
yOff++;
if (radiusError < 0) {
radiusError += 2 * yOff + 1;
} else {
xOff--;
radiusError += 2 * (yOff - xOff + 1);
}
}
}
public void drawLine(int x1, int y1, int x2, int y2) {
int dx = Math.abs(x2 - x1);
int dy = Math.abs(y2 - y1);
int sx = -1;
int sy = -1;
if (x1 < x2)
sx = 1;
if (y1 < y2)
sy = 1;
int err = dx - dy;
do {
drawPixel(x1, y1);
int e2 = 2 * err;
if (e2 > -dy) {
err = err - dy;
x1 = x1 + sx;
}
if (e2 < dx) {
err = err + dx;
y1 = y1 + sy;
}
} while (!(x1 == x2 && y1 == y2));
}
public void drawPixel(int x, int y) {
if (x >= 0 && x < w && y >= 0 && y < h)
drawPixel(x + y * w);
}
public void drawRect(int x, int y, int width, int height) {
//* do clipping *
int ox = x, oy = y;
if (x < 0) {
if (x <= -width)
return;
width += x;
x = 0;
} else if (x + width >= w) {
if (x >= w)
return;
width = Math.min(width + x, w);
width -= x;
}
if (y < 0) {
if (y <= -height)
return;
height += y;
y = 0;
} else if (y + height >= h) {
if (y >= h)
return;
height = Math.min(height + y, h);
height -= y;
}
expandActiveBounds(x, y, width, height);
final int wm1 = width - 1;
int start = x + y * w;
int end = start + width;
// top
if (oy > 0)
for (int i = start; i < end; i++) {
drawPixel(i);
}
end = y + height - 1;
// sides
if (ox > 0)
for (int yi = y + 1; yi < end; yi++) {
start += w;
drawPixel(start);
}
start = x + y * w + wm1;
if (ox + width < w)
for (int yi = y + 1; yi < end; yi++) {
start += w;
drawPixel(start);
}
start = x + (y + height - 1) * w;
end = start + width;
// bottom
if (oy + height < h)
for (int i = start; i < end; i++) {
drawPixel(i);
}
/*
drawLine(x, y, x + w, y); // top side
drawLine(x, y + h, x + w, y + h); // bottom side
drawLine(x, y + 1, x, y + h - 1); // left side
drawLine(x + w, y + 1, x + w, y + h - 1); // right side
*/
}
public void drawThickLine(int x1, int y1, int x2, int y2, int radius) {
// plot a circle at each end, connect with lines | DOESN'T WORK (perfectly) :(
// instead plot line, filling a circle at each point (crappy I know)
int dx = Math.abs(x2 - x1);
int dy = Math.abs(y2 - y1);
int sx = -1;
int sy = -1;
if (x1 < x2)
sx = 1;
if (y1 < y2)
sy = 1;
int err = dx - dy;
int clipx = Math.min(x1, x2) - radius;
int clipy = Math.min(y1, y2) - radius;
int clipw = dx + radius + radius;
int cliph = dy + radius + radius;
if (stencil)
expandStencilClip(clipx, clipy, clipw, cliph);
expandActiveBounds(clipx, clipy, clipw, cliph);
// only use opStencil if not using regular stencil and we need blending
opStencil = !stencil && !(blendmode instanceof Overwrite);
do {
fillCircle(x1, y1, radius);
int e2 = 2 * err;
if (e2 > -dy) {
err = err - dy;
x1 = x1 + sx;
}
if (e2 < dx) {
err = err + dx;
y1 = y1 + sy;
}
} while (!(x1 == x2 && y1 == y2));
if (opStencil)
flushBuffer(opStencilBuffer, clipx, clipy, clipw, cliph);
opStencil = false;
}
public void endStencil() {
boolean wasActive = stencil;
stencil = false;
if (wasActive)
flushBuffer(stencilBuffer, sclipx, sclipy, sclipw, scliph);
}
/**
*
*/
// clipping done, also unrolled for cache
public void fillCircle(int x, int y, int radius) {
int clipx = x - radius;
int clipy = y - radius;
int clipw = radius * 2;
int cliph = clipw;
if (stencil)
expandStencilClip(clipx, clipy, clipw, cliph);
expandActiveBounds(clipx, clipy, clipw, cliph);
opStencil = !stencil && !(blendmode instanceof Overwrite);
int xOff = radius, yOff = 0;
int radiusError = 1 - xOff;
while (xOff >= yOff) {
//drawPixel(-yOff + x, -xOff + y); // north left top
//drawPixel(yOff + x, -xOff + y); // north right top
int xi = x - yOff;
int yi = (y - xOff) * w;
int start = Math.max(xi + yi, Math.max(yi, 0));
int end = Math.min(xi + yi + yOff + yOff, Math.min(yi + w, buffer.length));
for (int i = start; i < end; i++)
if (opStencil)
opStencilBuffer[i] = true;
else
drawPixel(i);
yOff++;
if (radiusError < 0) {
radiusError += 2 * yOff + 1;
} else {
xOff--;
radiusError += 2 * (yOff - xOff + 1);
}
}
xOff = radius;
yOff = 0;
radiusError = 1 - xOff;
while (xOff >= yOff) {
//drawPixel(-xOff + x, -yOff + y); // north left bottom
//drawPixel(xOff + x, -yOff + y); // north right bottom
int xi = x - xOff;
int yi = (y - yOff) * w;
int start = Math.max(xi + yi, Math.max(yi, 0));
int end = Math.min(xi + yi + xOff + xOff, Math.min(yi + w, buffer.length));
for (int i = start; i < end; i++)
if (opStencil)
opStencilBuffer[i] = true;
else
drawPixel(i);
yOff++;
if (radiusError < 0) {
radiusError += 2 * yOff + 1;
} else {
xOff--;
radiusError += 2 * (yOff - xOff + 1);
}
}
xOff = radius;
yOff = 0;
radiusError = 1 - xOff;
while (xOff >= yOff) {
//drawPixel(-xOff + x, yOff + y); // south left top
//drawPixel(xOff + x, yOff + y); // south right top
int xi = x - xOff;
int yi = (y + yOff - 1) * w;
int start = Math.max(xi + yi, Math.max(yi, 0));
int end = Math.min(xi + yi + xOff + xOff, Math.min(yi + w, buffer.length));
for (int i = start; i < end; i++)
if (opStencil)
opStencilBuffer[i] = true;
else
drawPixel(i);
yOff++;
if (radiusError < 0) {
radiusError += 2 * yOff + 1;
} else {
xOff--;
radiusError += 2 * (yOff - xOff + 1);
}
}
xOff = radius;
yOff = 0;
radiusError = 1 - xOff;
while (xOff >= yOff) {
//drawPixel(-yOff + x, xOff + y); // south left bottom
//drawPixel(yOff + x, xOff + y); // south right bottom
int xi = x - yOff;
int yi = (y + xOff - 1) * w;
int start = Math.max(xi + yi, Math.max(yi, 0));
int end = Math.min(xi + yi + yOff + yOff, Math.min(yi + w, buffer.length));
for (int i = start; i < end; i++)
if (opStencil)
opStencilBuffer[i] = true;
else
drawPixel(i);
yOff++;
if (radiusError < 0) {
radiusError += 2 * yOff + 1;
} else {
xOff--;
radiusError += 2 * (yOff - xOff + 1);
}
}
if (opStencil)
flushBuffer(opStencilBuffer, clipx, clipy, clipw, cliph);
opStencil = false;
}
public void fillEllipse(int x, int y, int width, int height) {
int yOff = 0;
int maxy = y + height;
double a = width / (double) height;
int ywOff = y * w;
x += width >> 1;
if (y < 0) {
if (y < -height)
return;
yOff -= y;
maxy -= y;
ywOff = 0;
}
maxy = Math.min(maxy, h) - y;
for (; yOff < maxy; yOff++, ywOff += w) {
int xOff = (int) (Math.sqrt(yOff * (height - yOff)) * a);
int start = Math.max(x - xOff, 0) + ywOff;
int end = Math.min(x + xOff, w) + ywOff;
for (int i = start; i < end; i++)
drawPixel(i);
}
}
public void fillRect(int x, int y, int width, int height) {
//TODO:* do clipping *
// single loop method:
/*
int start = x + y * w;
int end = start + (height - 1) * w + width;
int inc = w - width;
for (int i = start, t = 0; i < end; i++) {
drawPixel(i);
if (++t % width == 0)
i += inc;
}
*/
// nested loop method, honestly the jit can probably optimize this better than single loop
int my = y + height;
int mx = x + width;
for (int yi = y; yi < my; yi++) {
int ii = yi * w;
for (int xi = x; xi < mx; xi++) {
drawPixel(ii + xi);
}
}
/*
for (; y < y + h; y++) {
drawLine(x, y, x + w, y);
}
*/
}
public Rectangle getActiveBounds() {
return new Rectangle(abx, aby, abw, abh);
}
public Rectangle getActiveStencilBounds() {
return new Rectangle(sclipx, sclipy, sclipw, scliph);
}
public Color getColor() {
return MutableColor.getColor(cc);
}
public BufferedImage getImage() {
return image;
}
public void resetActiveBounds() {
abx = w;
aby = h;
abw = abh = 0;
}
public void setBackground(Color color) {
setBackground(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha());
}
public void setBackground(int argb) {
int a = argb >> 24 & 0xFF;
int r = argb >> 16 & 0xFF;
int g = argb >> 8 & 0xFF;
int b = argb & 0xFF;
setBackground(r, g, b, a);
}
public void setBackground(int r, int g, int b) {
setBackground(r, g, b, 255);
}
public void setBackground(int r, int g, int b, int a) {
bgr = mult(r, a);
bgg = mult(g, a);
bgb = mult(b, a);
bga = a;
bgc = ColorUtils.pack(bgr, bgg, bgc, bga);
}
public void setBlendMode(BlendMode mode) {
blendmode = mode;
}
public void setColor(Color color) { // MutableColor works here
setColor(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha());
}
public void setColor(int argb) {
int a = argb >> 24 & 0xFF;
int r = argb >> 16 & 0xFF;
int g = argb >> 8 & 0xFF;
int b = argb & 0xFF;
setColor(r, g, b, a);
}
public void setColor(int r, int g, int b) {
setColor(r, g, b, 255);
}
public void setColor(int r, int g, int b, int a) {
cr = mult(r, a);
cg = mult(g, a);
cb = mult(b, a);
crp = (cr << 16) - 0xFF0000;
cgp = (cg << 8) - 0x00FF00;
cbp = cb - 0x0000FF;
ca = a;
cc = ColorUtils.pack(cr, cg, cb, ca);
}
/**
* Overwrites the pixel at (x, y) to the current color.
*/
public void setPixel(int x, int y) {
setPixel(x, y, cc);
}
public void setPixel(int x, int y, int value) {
setPixelAtIndex(x + y * w, value);
}
// clamps to 255
private int clamp(int x) {
x -= 0xFF;
x &= x >> 8;
x += 0xFF;
return x;
}
/**
* interesting unsigned-byte-ish multiply
* integer equivalent of
*
* C = A * B where A, B are both in the range [0, 1]
*
* but instead it's in the range [0, 255]
*
* Examples:
* mult(255, 255) = 255 // 1.0 * 1.0 = 1.0
* mult(255, 128) = 128 // 1.0 * 0.5 = 0.5
* mult(128, 128) = 64 // 0.5 * 0.5 = 0.25
*/
private int mult(int a, int b) {
a = b * a + 0x80;
return (a >> 8) + a >> 8;
}
/**
* Clears the stencil buffer.
*/
protected void clearStencilBuffer() {
Arrays.fill(stencilBuffer, false);
}
protected void drawPixel(int i) {
if (stencil)
stencilBuffer[i] = true;
else {
final int src = buffer[i];
/*// get channels
int a = src & 0xFF000000;
int r = src & 0x00FF0000;
int g = src & 0x0000FF00;
int b = src & 0x000000FF;
// add src and current color (premultiplied with alpha), then clamp
r += crp;
g += cgp;
b += cbp;
r &= r >> 8;
g &= g >> 8;
b &= b >> 8;
r += 0xFF0000;
g += 0x00FF00;
b += 0x0000FF;
// store
buffer[i] = a | r | g | b;
*/
buffer[i] = blendmode.blend(src, cc);
}
}
/**
* Flushes entire stencil to main buffer.
*/
protected void drawStencilBuffer() {
if (!stencil)
return;
stencil = false;
for (int i = 0; i < stencilBuffer.length; i++) {
if (stencilBuffer[i]) {
drawPixel(i);
}
}
stencil = true;
}
protected void expandActiveBounds(int x, int y, int w, int h) {
abx = Math.min(abx, x);
aby = Math.min(aby, y);
abw = Math.max(abw, w);
abh = Math.max(abh, h);
}
protected void expandStencilClip(int x, int y, int w, int h) {
sclipx = Math.min(sclipx, x);
sclipy = Math.min(sclipy, y);
sclipw = Math.max(sclipw, w);
scliph = Math.max(scliph, h);
}
protected void flushBuffer(final boolean[] buffer, int x, int y, int width, int height) {
if (x < 0) {
if (x <= -width)
return;
width += x;
x = 0;
} else if (x >= w - width) {
if (x >= w)
return;
width = Math.min(width + x, w);
width -= x;
}
if (y < 0) {
if (y <= -height)
return;
height += y;
y = 0;
} else if (y >= h - height) {
if (y >= h)
return;
height = Math.min(height + y, h);
height -= y;
}
width += x;
height += y;
width = Math.min(width, w);
height = Math.min(height, h);
for (; y < height; y++) {
for (int xi = x; xi < width; xi++) {
int i = xi + y * w;
if (buffer[i]) {
drawPixel(i);
buffer[i] = false;
}
}
}
}
/**
* Flushes entire stencil to main buffer and clears the stencil buffer.
*/
protected void flushStencilBuffer() {
if (!stencil)
return;
stencil = false;
for (int i = 0; i < stencilBuffer.length; i++) {
if (stencilBuffer[i]) {
drawPixel(i);
stencilBuffer[i] = false;
}
}
stencil = true;
}
protected void setPixelAtIndex(int i, int value) {
buffer[i] = value;
}
protected void stencilPixel(int i) {
(opStencil ? opStencilBuffer : stencilBuffer)[i] = true;
}
}
public interface BlendMode {
public int blend(int argb1, int argb2);
}
public class Overwrite implements BlendMode {
@Override
public int blend(int argb1, int argb2) {
return argb2;
}
}
public enum MathUtils {
;
public static int clamp(int i, int max, int min) {
return Math.min(Math.max(i, min), max);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment