2018-03-18 20:13:42 +01:00

845 lines
19 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package inf101.v18.gfx.textmode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.BiFunction;
import inf101.v18.gfx.IPaintLayer;
import inf101.v18.gfx.Screen;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.effect.BlendMode;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
public class Printer implements IPaintLayer {
private static class Char {
public int mode;
public String s;
public Color fill;
public Color stroke;
public Paint bg;
public Char(String s, Color fill, Color stroke, Paint bg, int mode) {
this.s = s;
this.fill = fill;
this.stroke = stroke;
this.bg = bg;
this.mode = mode;
}
}
public static final TextFont FONT_MONOSPACED = new TextFont("Monospaced", 27.00, TextMode.CHAR_BOX_SIZE, 3.4000,
-6.7000, 1.5000, 1.0000, true);
public static final TextFont FONT_LMMONO = new TextFont("lmmono10-regular.otf", 30.00, TextMode.CHAR_BOX_SIZE,
4.0000, -8.5000, 1.5000, 1.0000, true);
public static final TextFont FONT_ZXSPECTRUM7 = new TextFont("ZXSpectrum-7.otf", 22.00, TextMode.CHAR_BOX_SIZE,
3.1000, -3.8000, 1.0000, 1.0000, true);
/**
* TTF file can be found here: http://users.teilar.gr/~g1951d/ in this ZIP file:
* http://users.teilar.gr/~g1951d/Symbola.zip
* <p>
* (Put the extracted Symbola.ttf in src/inf101/v18/gfx/fonts/)
*/
public static final TextFont FONT_SYMBOLA = new TextFont("Symbola.ttf", 26.70, TextMode.CHAR_BOX_SIZE, -0.4000,
-7.6000, 1.35000, 1.0000, true);
/**
* TTF file can be found here:
* http://www.kreativekorp.com/software/fonts/c64.shtml
*/
public static final TextFont FONT_GIANA = new TextFont("Giana.ttf", 25.00, TextMode.CHAR_BOX_SIZE, 4.6000, -5.0000,
1.0000, 1.0000, true);
/**
* TTF file can be found here:
* http://www.kreativekorp.com/software/fonts/c64.shtml
*/
public static final TextFont FONT_C64 = new TextFont("PetMe64.ttf", 31.50, TextMode.CHAR_BOX_SIZE, 0.0000, -4.000,
1.0000, 1.0000, true);
private static final Paint DEFAULT_BACKGROUND = Color.TRANSPARENT;
private static final TextMode DEFAULT_MODE = TextMode.MODE_40X22;
private static final boolean DEBUG_REDRAW = false;
public static String center(String s, int width) {
for (; s.length() < width; s = " " + s + " ")
;
return s;
}
public static String repeat(String s, int width) {
String r = s;
for (; r.length() < width; r += s)
;
return r;
}
Color DEFAULT_FILL = Color.WHITE;
Color DEFAULT_STROKE = Color.TRANSPARENT;
private TextMode textMode;
private Color fill;
private Color stroke;
private Paint background;
private Screen screen;
private List<Char[]> lineBuffer = new ArrayList<>();
private boolean autoscroll = true;
private final Canvas textPage;
private int x = 1, y = 1, savedX = 1, savedY = 1;
// private int pageWidth = LINE_WIDTHS[resMode], pageHeight =
// PAGE_HEIGHTS[resMode];
private int leftMargin = 1, topMargin = 1;
private TextFont font = FONT_SYMBOLA;
private int videoAttrs = 0;
private String csiSeq = null;
private boolean csiEnabled = true;
private int csiMode = 0;
private final double width;
private final double height;
private int dirtyX0 = Integer.MAX_VALUE;
private int dirtyX1 = Integer.MIN_VALUE;
private int dirtyY0 = Integer.MAX_VALUE;
private int dirtyY1 = Integer.MIN_VALUE;
private boolean useBuffer = true;
public Printer(double width, double height) {
this.screen = null;
this.textPage = null;
this.width = width;
this.height = height;
for (int i = 0; i < TextMode.PAGE_HEIGHT_MAX; i++) {
lineBuffer.add(new Char[TextMode.LINE_WIDTH_MAX]);
}
resetFull();
}
public Printer(Screen screen, Canvas page) {
this.screen = screen;
this.textPage = page;
this.width = page.getWidth();
this.height = page.getHeight();
for (int i = 0; i < TextMode.PAGE_HEIGHT_MAX; i++) {
lineBuffer.add(new Char[TextMode.LINE_WIDTH_MAX]);
}
resetFull();
}
public void addToCharBuffer(String string) {
string.codePoints().mapToObj((int i) -> String.valueOf(Character.toChars(i))).forEach((String s) -> {
if (csiMode != 0) {
s = addToCsiBuffer(s);
}
switch (s) {
case "\r":
moveTo(leftMargin, y);
break;
case "\n":
moveTo(leftMargin, y + 1);
break;
case "\f":
moveTo(leftMargin, topMargin);
for (Char[] line : lineBuffer)
Arrays.fill(line, null);
if(textPage != null) {
GraphicsContext context = textPage.getGraphicsContext2D();
if (background != null && background != Color.TRANSPARENT) {
context.setFill(background);
context.fillRect(0.0, 0.0, textPage.getWidth(), textPage.getHeight());
} else
context.clearRect(0.0, 0.0, textPage.getWidth(), textPage.getHeight());
}
break;
case "\b":
moveHoriz(-1);
break;
case "\t":
moveTo((x + 8) % 8, y);
break;
case "\u001b":
if (csiEnabled) {
csiSeq = s;
csiMode = 1;
}
break;
default:
if (s.length() > 0 && s.codePointAt(0) >= 0x20) {
drawChar(x, y, setChar(x, y, s));
moveHoriz(1);
}
break;
}
});
}
private String addToCsiBuffer(String s) {
if (csiMode == 1) {
switch (s) {
case "[":
csiMode = 2;
csiSeq += s;
break;
case "c":
csiMode = 0;
resetFull();
break;
default:
csiReset();
return s;
}
} else if (csiMode == 2) {
int c = s.codePointAt(0);
if (c >= 0x30 && c <= 0x3f) {
csiSeq += s;
} else if (c >= 0x20 && c <= 0x2f) {
csiMode = 3;
csiSeq += s;
} else if (c >= 0x40 && c <= 0x7e) {
csiSeq += s;
csiFinish();
} else {
csiReset();
return s;
}
} else if (csiMode == 3) {
int c = s.codePointAt(0);
if (c >= 0x20 && c <= 0x2f) {
csiSeq += s;
} else if (c >= 0x40 && c <= 0x7e) {
csiSeq += s;
csiFinish();
} else {
csiReset();
return s;
}
}
return "";
}
public void beginningOfLine() {
x = leftMargin;
}
public void beginningOfPage() {
x = leftMargin;
y = topMargin;
}
@Override
public void clear() {
print("\f");
}
public void clearAt(int x, int y) {
printAt(x, y, " ");
}
public void clearLine(int y) {
y = constrainY(y);
if (y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) {
Arrays.fill(lineBuffer.get(y - 1), null);
dirty(1, y);
dirty(getLineWidth(), y);
if (!useBuffer)
redrawDirty();
}
}
public void clearRegion(int x, int y, int width, int height) {
if (x > getLineWidth() || y > getPageHeight())
return;
int x2 = Math.min(x + width - 1, getLineWidth());
int y2 = Math.min(y + height - 1, getPageHeight());
if (x2 < 1 || y2 < 1)
return;
int x1 = Math.max(1, x);
int y1 = Math.max(1, y);
// Char fillWith = new Char("*", Color.BLACK, Color.GREEN, Color.TRANSPARENT,
// 0);
for (int i = y1; i <= y2; i++) {
Arrays.fill(lineBuffer.get(i - 1), x1 - 1, x2, null);
}
dirty(x1, y1);
dirty(x2, y2);
if (!useBuffer)
redrawDirty();
}
private int constrainX(int x) {
return x; // Math.min(LINE_WIDTH_HIRES, Math.max(1, x));
}
public int constrainY(int y) {
return y; // Math.min(pageHeight, Math.max(1, y));
}
public int constrainYOrScroll(int y) {
if (autoscroll) {
if (y < 1) {
scroll(y - 1);
return 1;
} else if (y > getPageHeight()) {
scroll(y - getPageHeight());
return getPageHeight();
}
}
return y;// Math.min(pageHeight, Math.max(1, y));
}
private void csiFinish() {
ControlSequences.applyCsi(this, csiSeq);
csiReset();
}
private void csiReset() {
csiMode = 0;
csiSeq = null;
}
public void cycleMode(boolean adjustDisplayAspect) {
textMode = textMode.nextMode();
if (adjustDisplayAspect && screen != null)
screen.setAspect(textMode.getAspect());
dirty(1, 1);
dirty(getLineWidth(), getPageHeight());
if (!useBuffer)
redrawDirty();
}
private void drawChar(int x, int y, Char c) {
if (useBuffer) {
dirty(x, y);
} else if (c != null && textPage != null) {
GraphicsContext context = textPage.getGraphicsContext2D();
context.setFill(c.fill);
context.setStroke(c.stroke);
font.drawTextAt(context, (x - 1) * getCharWidth(), y * getCharHeight(), c.s,
textMode.getCharWidth() / textMode.getCharBoxSize(), c.mode, c.bg);
}
}
public void drawCharCells() {
if (screen != null) {
GraphicsContext context = screen.getBackgroundContext();
screen.clearBackground();
double w = getCharWidth();
double h = getCharHeight();
context.save();
context.setGlobalBlendMode(BlendMode.EXCLUSION);
context.setFill(Color.WHITE.deriveColor(0.0, 1.0, 1.0, 0.1));
for (int x = 0; x < getLineWidth(); x++) {
for (int y = 0; y < getPageHeight(); y++) {
if ((x + y) % 2 == 0)
context.fillRect(x * w, y * h, w, h);
}
}
context.restore();
}
}
public Color getBackground(int x, int y) {
Char c = null;
if (x > 0 && x <= TextMode.LINE_WIDTH_MAX && y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) {
c = lineBuffer.get(y - 1)[x - 1];
}
Color bg = Color.TRANSPARENT;
if (c != null && c.bg instanceof Color)
bg = (Color) c.bg;
else if (background instanceof Color)
bg = (Color) background;
return bg;
}
public boolean getBold() {
return (videoAttrs & TextFont.ATTR_BRIGHT) != 0;
}
public String getChar(int x, int y) {
Char c = null;
if (x > 0 && x <= TextMode.LINE_WIDTH_MAX && y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) {
c = lineBuffer.get(y - 1)[x - 1];
}
if (c != null)
return c.s;
else
return " ";
}
public double getCharHeight() {
return textMode.getCharHeight();
}
public double getCharWidth() {
return textMode.getCharWidth();
}
public Color getColor(int x, int y) {
Char c = null;
if (x > 0 && x <= TextMode.LINE_WIDTH_MAX && y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) {
c = lineBuffer.get(y - 1)[x - 1];
}
if (c != null)
return c.fill;
else
return fill;
}
public TextFont getFont() {
return font;
}
public boolean getItalics() {
return (videoAttrs & TextFont.ATTR_ITALIC) != 0;
}
/**
* @return the leftMargin
*/
public int getLeftMargin() {
return leftMargin;
}
public int getLineWidth() {
return textMode.getLineWidth();
}
public int getPageHeight() {
return textMode.getPageHeight();
}
public boolean getReverseVideo() {
return (videoAttrs & TextFont.ATTR_INVERSE) != 0;
}
public TextMode getTextMode() {
return textMode;
}
/**
* @return the topMargin
*/
public int getTopMargin() {
return topMargin;
}
public int getVideoMode() {
return videoAttrs;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public boolean isFilled(int x, int y) {
return !getChar(x, y).equals(" ");
}
@Override
public void layerToBack() {
if (screen != null) {
screen.moveToBack(this);
}
}
@Override
public void layerToFront() {
if (screen != null) {
screen.moveToFront(this);
}
}
public void move(int deltaX, int deltaY) {
x = constrainX(x + deltaX);
y = constrainYOrScroll(y + deltaY);
}
public void moveHoriz(int dist) {
x = constrainX(x + dist);
}
public void moveTo(int newX, int newY) {
x = constrainX(newX);
y = constrainYOrScroll(newY);
}
public void moveVert(int dist) {
y = constrainYOrScroll(y + dist);
}
public void plot(int x, int y) {
plot(x, y, (a, b) -> a | b);
}
public void plot(int x, int y, BiFunction<Integer, Integer, Integer> op) {
int textX = (x) / 2 + 1;
int textY = (y) / 2 + 1;
int bitPos = (x + 1) % 2 + ((y + 1) % 2) * 2;
String blockChar = BlocksAndBoxes.unicodeBlocks[1 << bitPos];
// System.out.println(blockChar + ", " + bitPos + ", ("+ (x) + ", " + (y) + ")"+
// ", (" + (textX) + ", " + (textY) + ")");
String s = BlocksAndBoxes.blockComposeOrOverwrite(getChar(textX, textY), blockChar, op);
// System.out.println("Merge '" + getChar(textX, textY) + "' + '" + blockChar +
// "' = '" + s + "'");
printAt(textX, textY, s);
}
public void print(String s) {
addToCharBuffer(s);
}
public void print(String s, Color paint) {
Color tmp = fill;
fill = paint;
addToCharBuffer(s);
fill = tmp;
}
public void printAt(int atX, int atY, String s) {
moveTo(atX, atY);
print(s);
}
public void printAt(int atX, int atY, String s, Color ink) {
moveTo(atX, atY);
print(s, ink);
}
public void println() {
print("\n");
}
public void println(String s) {
print(s);
print("\n");
}
public void redrawTextPage() {
redrawTextPage(1, 1, getLineWidth(), getPageHeight());
clean();
}
private void redrawTextPage(int x0, int y0, int x1, int y1) {
/*
* System.out.printf("redrawTextPage benchmark");
* System.out.printf(" %5s %5s %7s %4s %5s %5s %5s%n", "ms", "chars",
* "ms/char", "mode", "indir", "inv", "fake"); for (int m = -1; m < 8; m++) {
* long t0 = System.currentTimeMillis(); int n = 0;
*/
if(textPage == null)
return;
GraphicsContext context = textPage.getGraphicsContext2D();
double px0 = (x0 - 1) * getCharWidth(), py0 = (y0 - 1) * getCharHeight();
double px1 = x1 * getCharWidth(), py1 = y1 * getCharHeight();
if (DEBUG_REDRAW)
System.out.printf("redrawTextPage(): Area to clear: (%2f,%2f)(%2f,%2f)%n", px0, py0, px1, py1);
if (background != null && background != Color.TRANSPARENT) {
context.setFill(background);
context.fillRect(px0, py0, px1 - px0, py1 - py0);
} else {
context.clearRect(px0, py0, px1 - px0, py1 - py0);
}
for (int tmpY = y0; tmpY <= y1; tmpY++) {
Char[] line = lineBuffer.get(tmpY - 1);
for (int tmpX = x0; tmpX <= x1; tmpX++) {
Char c = line[tmpX - 1];
if (c != null) {
context.save();
context.setFill(c.fill);
context.setStroke(c.stroke);
Paint bg = c.bg == background ? null : c.bg;
font.drawTextNoClearAt(context, (tmpX - 1) * getCharWidth(), tmpY * getCharHeight(), c.s,
textMode.getCharWidth() / textMode.getCharBoxSize(), c.mode/* m */, bg);
context.restore();
// n++;
}
}
}
/*
* long t = System.currentTimeMillis() - t0; if (m >= 0)
* System.out.printf(" %5d %5d %7.4f %4d %5b %5b %5b%n", t, n, ((double) t) /
* n, m, (m & 3) != 0, (m & 1) != 0, (m & 4) != 0); } System.out.println();
*/
}
public void resetAttrs() {
this.fill = DEFAULT_FILL;
this.stroke = DEFAULT_STROKE;
this.background = DEFAULT_BACKGROUND;
this.videoAttrs = 0;
this.csiSeq = null;
this.csiMode = 0;
}
public void resetFull() {
resetAttrs();
beginningOfPage();
this.autoscroll = true;
this.textMode = DEFAULT_MODE;
redrawTextPage();
}
public void restoreCursor() {
x = savedX;
y = savedY;
}
public void saveCursor() {
savedX = x;
savedY = y;
}
void scroll(int i) {
while (i < 0) {
scrollDown();
i++;
}
while (i > 0) {
scrollUp();
i--;
}
}
public void scrollDown() {
Char[] remove = lineBuffer.remove(lineBuffer.size() - 1);
Arrays.fill(remove, null);
lineBuffer.add(0, remove);
dirty(1, 1);
dirty(getLineWidth(), getPageHeight());
if (!useBuffer)
redrawDirty();
}
public void scrollUp() {
Char[] remove = lineBuffer.remove(0);
Arrays.fill(remove, null);
lineBuffer.add(remove);
dirty(1, 1);
dirty(getLineWidth(), getPageHeight());
if (!useBuffer)
redrawDirty();
}
public boolean setAutoScroll(boolean autoScroll) {
boolean old = autoscroll;
autoscroll = autoScroll;
return old;
}
public void setBackground(int x, int y, Paint bg) {
Char c = null;
if (x > 0 && x <= TextMode.LINE_WIDTH_MAX && y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) {
c = lineBuffer.get(y - 1)[x - 1];
}
if (c != null) {
c.bg = bg;
drawChar(x, y, c);
}
}
public void setBackground(Paint bgColor) {
this.background = bgColor != null ? bgColor : DEFAULT_BACKGROUND;
}
public void setBold(boolean enabled) {
if (enabled)
videoAttrs |= TextFont.ATTR_BRIGHT;
else
videoAttrs &= ~TextFont.ATTR_BRIGHT;
}
public Char setChar(int x, int y, String s) {
if (x > 0 && x <= TextMode.LINE_WIDTH_MAX && y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) {
Char c = new Char(s, fill, stroke, background, videoAttrs);
lineBuffer.get(y - 1)[x - 1] = c;
return c;
}
return null;
}
public void setColor(int x, int y, Color fill) {
Char c = null;
if (x > 0 && x <= TextMode.LINE_WIDTH_MAX && y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) {
c = lineBuffer.get(y - 1)[x - 1];
}
if (c != null) {
c.fill = fill;
drawChar(x, y, c);
}
}
public void setFill(Color fill) {
this.fill = fill != null ? fill : DEFAULT_FILL;
}
public void setFont(TextFont font) {
this.font = font;
}
public void setInk(Color ink) {
fill = ink != null ? ink : DEFAULT_FILL;
stroke = ink != null ? ink : DEFAULT_STROKE;
}
public void setItalics(boolean enabled) {
if (enabled)
videoAttrs |= TextFont.ATTR_ITALIC;
else
videoAttrs &= ~TextFont.ATTR_ITALIC;
}
/**
*/
public void setLeftMargin() {
this.leftMargin = x;
}
/**
* @param leftMargin
* the leftMargin to set
*/
public void setLeftMargin(int leftMargin) {
this.leftMargin = constrainX(leftMargin);
}
public void setReverseVideo(boolean enabled) {
if (enabled)
videoAttrs |= TextFont.ATTR_INVERSE;
else
videoAttrs &= ~TextFont.ATTR_INVERSE;
}
public void setStroke(Color stroke) {
this.stroke = stroke != null ? stroke : DEFAULT_STROKE;
}
public void setTextMode(TextMode mode) {
setTextMode(mode, false);
}
public void setTextMode(TextMode mode, boolean adjustDisplayAspect) {
if (mode == null)
throw new IllegalArgumentException();
textMode = mode;
if (adjustDisplayAspect && screen != null)
screen.setAspect(textMode.getAspect());
dirty(1, 1);
dirty(getLineWidth(), getPageHeight());
if (!useBuffer)
redrawDirty();
}
public void setTopMargin() {
this.topMargin = y;
}
/**
* @param topMargin
* the topMargin to set
*/
public void setTopMargin(int topMargin) {
this.topMargin = constrainY(topMargin);
}
public void setVideoAttrDisabled(int attr) {
videoAttrs &= ~attr;
}
public void setVideoAttrEnabled(int attr) {
videoAttrs |= attr;
}
public void setVideoAttrs(int attr) {
videoAttrs = attr;
}
public void unplot(int x, int y) {
plot(x, y, (a, b) -> a & ~b);
}
@Override
public double getWidth() {
return width;
}
@Override
public double getHeight() {
return height;
}
private boolean isDirty() {
return dirtyX0 <= dirtyX1 || dirtyY0 <= dirtyY1;
}
/**
* Expand the dirty region (area that should be redrawn) to include the given
* position
*
* @param x
* @param y
*/
private void dirty(int x, int y) {
dirtyX0 = Math.max(Math.min(x, dirtyX0), 1);
dirtyX1 = Math.min(Math.max(x, dirtyX1), getLineWidth());
dirtyY0 = Math.max(Math.min(y, dirtyY0), 1);
dirtyY1 = Math.min(Math.max(y, dirtyY1), getPageHeight());
}
/**
* Redraw the part of the page that has changed since last redraw.
*/
public void redrawDirty() {
if (isDirty()) {
if (DEBUG_REDRAW)
System.out.printf("redrawDirty(): Dirty region is (%d,%d)(%d,%d)%n", dirtyX0, dirtyY0, dirtyX1,
dirtyY1);
redrawTextPage(dirtyX0, dirtyY0, dirtyX1, dirtyY1);
clean();
}
}
/**
* Mark the entire page as clean
*/
private void clean() {
dirtyX0 = Integer.MAX_VALUE;
dirtyX1 = Integer.MIN_VALUE;
dirtyY0 = Integer.MAX_VALUE;
dirtyY1 = Integer.MIN_VALUE;
}
/**
* With buffered printing, nothing is actually drawn until
* {@link #redrawDirty()} or {@link #redrawTextPage()} is called.
*
* @param buffering
* Whether to use buffering
*/
public void setBuffering(boolean buffering) {
useBuffer = buffering;
}
/**
* @return True if buffering is enabled
* @see #setBuffering(boolean)
*/
public boolean getBuffering() {
return useBuffer;
}
}