Sketcher2 source code
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1279 lines
43 KiB

4 years ago
package com.jotuntech.sketcher.client;
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.ImageObserver;
import java.net.URI;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.BoundedRangeModel;
import javax.swing.JComponent;
import javax.swing.JOptionPane;
import javax.swing.KeyStroke;
import javax.swing.Timer;
import cello.jtablet.TabletManager;
import cello.jtablet.event.TabletEvent;
import cello.jtablet.event.TabletListener;
import cello.jtablet.installer.JTabletExtension;
import cello.tablet.JTablet;
import com.jotuntech.sketcher.client.command.CursorCommand;
import com.jotuntech.sketcher.client.command.LineCommand;
import com.jotuntech.sketcher.client.command.MergeCommand;
import com.jotuntech.sketcher.client.command.MoveCommand;
import com.jotuntech.sketcher.client.command.SetColorCommand;
import com.jotuntech.sketcher.client.command.UndoCommand;
import com.jotuntech.sketcher.common.BitmapLayer;
import com.jotuntech.sketcher.common.Brush;
import com.jotuntech.sketcher.common.Canvas;
import com.jotuntech.sketcher.common.Input;
import com.jotuntech.sketcher.common.Layer;
import com.jotuntech.sketcher.common.Log;
import com.jotuntech.sketcher.common.User;
public class Editor extends JComponent implements MouseListener, MouseMotionListener, TabletListener {
private Client client;
private BufferedImage image;
private float zoom = 1;
private boolean smoothZoom = true;
private Dimension size;
private JTablet jTablet1;
private boolean jTablet2;
private Smoother smoother;
private Cursor currentNativeCursor, crosshair, blank, eyedropper, hand;
private Timer tagTimer, selectTimer, yieldTimer;
private Color canvasBackground = Color.WHITE;
private long lastCursorCommand;
private int dashPhase = 0;
private boolean tagsEnabled = true;
public enum State {
DISABLED,
DRAW_HOVER, DRAW_PRESS,
LINE_HOVER, LINE_PRESS,
RECT_HOVER, RECT_PRESS,
OVAL_HOVER, OVAL_PRESS,
BEZIER_HOVER, BEZIER_PRESS, BEZIER_HOVER2, BEZIER_PRESS_P2, BEZIER_PRESS_P3,
EYEDROP_PRESS,
SCROLL_HOVER, SCROLL_PRESS,
SELECT_HOVER, SELECT_PRESS, SELECT_INSIDE, SELECT_MOVE,
MOVE_HOVER, MOVE_INSIDE, MOVE_PRESS,
AD_HOVER, AD_PRESS;
};
private State _state, _oldState = State.DISABLED;
private int buttonDown = TabletEvent.NOBUTTON;
public enum CursorType {
NATIVE, SOFTWARE
};
private CursorType cursorType;
private boolean cursorVisible = true;
private boolean softwareCursorEnabled = false;
private BufferedImage ad;
private Point adPosition;
private int pressure = 0xFF;
private float scrollSpeedX, scrollSpeedY, originX, originY, slowLOSX, slowLOSY;
private Point los;
private Timer scrollTimer;
private float currentX, currentY;
private float x1, y1, x2, y2, x3, y3, x4, y4;
private int iOriginX, iOriginY;
/** Current select */
private Rectangle select;
public Editor(final Client client) {
super();
Toolkit tk = Toolkit.getDefaultToolkit();
crosshair = tk.createCustomCursor(tk.getImage(getClass().getResource("images/crosshair.gif")), new Point(16, 16), "crosshair");
blank = tk.createCustomCursor(new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB), new Point(0, 0), "blank");
eyedropper = tk.createCustomCursor(tk.getImage(getClass().getResource("images/eyedropper.gif")), new Point(16, 16), "eyedropper");
hand = tk.createCustomCursor(tk.getImage(getClass().getResource("images/hand.gif")), new Point(16, 16), "hand");
setState(State.DISABLED);
jTablet2 = false;
if(JTabletExtension.checkCompatibility(this, "1.2.0") && JTabletExtension.getInstallStatus("1.2.0").equals(JTabletExtension.InstallStatus.INSTALLED)) {
jTablet2 = true;
TabletManager.getDefaultManager().addTabletListener(this, this);
Log.info("JTablet 2.x installed.");
} else {
try {
jTablet1 = new JTablet();
Log.info("JTablet 0.9.x installed.");
} catch (Throwable t) {
Log.info("JTablet not installed.");
}
}
smoother = new Smoother();
if(!jTablet2) {
addMouseListener(this);
addMouseMotionListener(this);
}
this.client = client;
ActionListener tagListener = new ActionListener() {
public void actionPerformed(ActionEvent e) {
if(client.getConnection() == null || client.getConnection().getUser() == null) {
return;
}
if(lastCursorCommand + 500 < System.currentTimeMillis()) {
client.getCommandQueue().offer(new CommandEntry(0, new CursorCommand(smoother.getIndex(2))));
lastCursorCommand = System.currentTimeMillis();
}
if(!tagsEnabled) {
return;
}
for(User u : client.getUserArray()) {
if(u != client.getConnection().getUser()) {
Input c = u.getCursor();
int cursorX = (int) (c.x * zoom);
int cursorY = (int) (c.y * zoom);
Rectangle tag = u.getTag();
if(tag.x != cursorX || tag.y != cursorY) {
String name = u.getName();
Graphics2D g2 = (Graphics2D) getGraphics();
if(g2 == null) {
return;
}
Rectangle2D stringBounds = g2.getFontMetrics().getStringBounds(name, g2);
g2.dispose();
Rectangle newTag = new Rectangle(cursorX, cursorY, (int) stringBounds.getWidth() + 4, (int) stringBounds.getHeight() + 4);
u.setTag(newTag);
repaint(tag);
repaint(newTag);
}
}
}
}
};
tagTimer = new Timer(1000, tagListener);
yieldTimer = new Timer(100, new ActionListener() {
public void actionPerformed(ActionEvent e) {
client.getCanvas().setDrawing(false);
}
});
ActionListener selectListener = new ActionListener() {
public void actionPerformed(ActionEvent e) {
Connection connection = client.getConnection();
if(connection == null) {
return;
}
User user = connection.getUser();
if(user == null) {
return;
}
if(select == null) {
return;
}
repaint(Math.round(select.x * zoom) - 1, Math.round(select.y * zoom) - 1, Math.round(select.width * zoom) + 2, Math.round(select.height * zoom) + 2);
++dashPhase;
}
};
selectTimer = new Timer(250, selectListener);
selectTimer.setCoalesce(true);
selectTimer.start();
lastCursorCommand = System.currentTimeMillis();
getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke("SPACE"), "drag begin");
getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke("released SPACE"), "drag end");
getActionMap().put("drag begin", new AbstractAction() {
public void actionPerformed(ActionEvent e) {
if(buttonDown != TabletEvent.NOBUTTON || getState() == State.SCROLL_PRESS || getState() == State.SCROLL_HOVER) {
return;
}
if(storeState()) {
setState(State.SCROLL_HOVER);
}
}
});
getActionMap().put("drag end", new AbstractAction() {
public void actionPerformed(ActionEvent e) {
if(getState() == State.SCROLL_PRESS || getState() == State.SCROLL_HOVER) {
restoreState();
}
}
});
getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("ctrl Z"), "undo");
getActionMap().put("undo", new AbstractAction() {
public void actionPerformed(ActionEvent e) {
if(buttonDown == TabletEvent.NOBUTTON) {
client.getCommandQueue().offer(new CommandEntry(0, new UndoCommand()));
}
}
});
}
protected void adjust() {
if(image == null) {
return;
}
size = new Dimension((int)(image.getWidth() * zoom), (int)(image.getHeight() * zoom));
setPreferredSize(size);
setMaximumSize(size);
invalidate();
getParent().validate();
repaint();
}
public float getZoom() {
return zoom;
}
public void setZoom(float zoom) {
this.zoom = zoom;
adjust();
}
public void setState(State state) {
if(state == this._state) {
return;
}
if(state != State.MOVE_HOVER && state != State.MOVE_INSIDE && state != State.MOVE_PRESS && (this._state == State.MOVE_HOVER || this._state == State.MOVE_INSIDE)) {
// TODO: Fix this dirty hack somehow. We must do this here, or the Move tool leaves our phantom layer dirty.
client.getCommandQueue().offer(new CommandEntry(0, new MergeCommand()));
select = null;
repaint();
System.err.println("Move state hack!");
}
switch(state) {
case DISABLED:
setNativeCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
setCursorType(CursorType.NATIVE);
break;
case DRAW_HOVER:
case DRAW_PRESS:
setCursorType(CursorType.SOFTWARE);
break;
case LINE_HOVER:
case RECT_HOVER:
case OVAL_HOVER:
case BEZIER_HOVER:
case BEZIER_HOVER2:
setNativeCursor(crosshair);
setCursorType(CursorType.NATIVE);
break;
case EYEDROP_PRESS:
setNativeCursor(eyedropper);
setCursorType(CursorType.NATIVE);
break;
case AD_HOVER:
setNativeCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); // Don't set to 'hand', this is actually a finger cursor
setCursorType(CursorType.NATIVE);
break;
case SCROLL_HOVER:
setNativeCursor(hand);
setCursorType(CursorType.NATIVE);
break;
case SELECT_HOVER:
setNativeCursor(crosshair);
setCursorType(CursorType.NATIVE);
break;
case SELECT_INSIDE:
setNativeCursor(hand);
setCursorType(CursorType.NATIVE);
break;
case MOVE_HOVER:
setNativeCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
setCursorType(CursorType.NATIVE);
break;
case MOVE_INSIDE:
setNativeCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
setCursorType(CursorType.NATIVE);
break;
}
this._state = state;
System.err.println("Set state: " + state);
}
public State getState() {
return _state;
}
private boolean storeState() {
if(_oldState != State.DISABLED) {
return false;
}
_oldState = getState();
System.err.println("Stored state, stack: " + _oldState);
return true;
}
private void restoreState() {
if(_oldState == State.DISABLED) {
throw new RuntimeException("Stack is empty.");
}
setState(_oldState);
_oldState = State.DISABLED;
System.err.println("Restored state, stack: " + _oldState);
}
private void setNativeCursor(Cursor cursor) {
Cursor actualCursor = cursorVisible ? cursor : blank;
if(cursorType == CursorType.NATIVE && actualCursor != getCursor()) {
setCursor(actualCursor);
System.err.println("Changed native cursor (setNativeCursor): " + getCursor());
}
currentNativeCursor = cursor;
}
private void setCursorVisible(boolean cursorVisible) {
if(cursorVisible == this.cursorVisible) {
return;
}
this.cursorVisible = cursorVisible;
if(cursorType == CursorType.SOFTWARE || !cursorVisible) {
if(getCursor() != blank) {
setCursor(blank);
System.err.println("Changed native cursor: " + getCursor());
}
repaint();
} else if(currentNativeCursor != getCursor()) {
setCursor(currentNativeCursor);
}
}
public void setCursorType(CursorType cursorType) {
switch(cursorType) {
case NATIVE:
this.setCursor(cursorVisible ? currentNativeCursor : blank);
this.cursorType = CursorType.NATIVE;
System.err.println("Changed native cursor: " + getCursor());
repaint();
break;
case SOFTWARE:
if(softwareCursorEnabled) {
setCursor(blank);
this.cursorType = CursorType.SOFTWARE;
System.err.println("Changed native cursor: " + getCursor());
} else {
currentNativeCursor = crosshair;
this.setCursor(cursorVisible ? currentNativeCursor : blank);
this.cursorType = CursorType.NATIVE;
System.err.println("Changed native cursor: " + getCursor());
}
repaint();
break;
}
}
private float bezier(float t, float p0, float p1, float p2, float p3) {
return (float) (Math.pow(1 - t, 3) * p0 + 3 * Math.pow(1 - t, 2) * t * p1 + 3 * (1 - t) * Math.pow(t, 2) * p2 + Math.pow(t, 3) * p3);
}
public void paint(Graphics g) {
if(image == null) {
return;
}
// Ensure proper clip area since JScrollView is an asshole
Rectangle clip = g.getClipBounds();
Rectangle validRect = new Rectangle(new Point(0, 0), size);
if(clip == null) {
clip = validRect;
} else {
clip = clip.intersection(validRect);
}
g.setClip(clip);
Graphics2D g2 = (Graphics2D)g;
g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
if(smoothZoom && zoom != Math.floor(zoom)) {
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
}
int zoomWidth = (int) (image.getWidth() * zoom);
int zoomHeight = (int) (image.getHeight() * zoom);
synchronized(image) {
g2.setColor(canvasBackground);
g2.fillRect(clip.x, clip.y, Math.min(zoomWidth, clip.width), Math.max(zoomHeight, clip.height));
int sourceX = Math.max(0, (int) Math.floor(clip.x / zoom));
float destinationX = sourceX * zoom;
int sourceY = Math.max(0, (int) Math.floor(clip.y / zoom));
float destinationY = sourceY * zoom;
int sourceWidth = Math.min(image.getWidth() - sourceX, (int) Math.ceil(clip.width / zoom) + 1);
int sourceHeight = Math.min(image.getHeight() - sourceY, (int) Math.ceil(clip.height / zoom) + 1);
if(sourceWidth > 0 && sourceHeight > 0) {
BufferedImage subImage = image.getSubimage(sourceX, sourceY, sourceWidth, sourceHeight);
AffineTransform xform = new AffineTransform();
xform.scale(zoom, zoom);
AffineTransform oldXform = g2.getTransform();
g2.translate(destinationX, destinationY);
g2.drawImage(subImage, xform, this);
g2.setTransform(oldXform);
}
}
for(User u : client.getUserArray()) {
if(u != client.getConnection().getUser()) {
Rectangle tag = u.getTag();
String name = u.getName();
Rectangle2D stringBounds = g2.getFontMetrics().getStringBounds(name, g2);
g2.setColor(Color.YELLOW);
g2.fillRect(tag.x, tag.y, tag.width, tag.height);
g2.setColor(Color.BLACK);
g2.drawRect(tag.x, tag.y, tag.width - 1, tag.height - 1);
g2.drawString(name, tag.x + 2, tag.y + (int) stringBounds.getHeight());
}
}
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
User user = client.getConnection().getUser();
if(cursorVisible && cursorType == CursorType.SOFTWARE) {
Brush brush = user.getBrush();
float hardAdjust = 0.5f + ((brush.getHardness() - 1f) / 12f);
Input input = smoother.getIndex(2);
float finalRadius = brush.isPressureToRadius() && input.pressure > 0 ? (brush.getRadius() * input.pressure * hardAdjust * zoom) / 255f : brush.getRadius() * hardAdjust * zoom;
float finalX = input.x * zoom;
float finalY = input.y * zoom;
Shape s = new Ellipse2D.Float(finalX - finalRadius, finalY - finalRadius, finalRadius * 2, finalRadius * 2);
g2.setColor(Color.BLACK);
g2.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10, new float[] { 2, 2 }, 0));
g2.draw(s);
g2.setColor(Color.WHITE);
g2.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10, new float[] { 2, 2 }, 2));
g2.draw(s);
}
g2.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10));
g2.setColor(Color.BLACK);
State myState = getState();
if(myState == State.LINE_PRESS || myState == State.RECT_PRESS || myState == State.OVAL_PRESS) {
switch(myState) {
case LINE_PRESS:
g2.draw(new Line2D.Float(new Point2D.Float(originX * zoom, originY * zoom), new Point2D.Float(currentX * zoom, currentY * zoom)));
break;
case RECT_PRESS:
g2.draw(new Rectangle2D.Float(originX * zoom, originY * zoom, currentX * zoom - originX * zoom, currentY * zoom - originY * zoom));
break;
case OVAL_PRESS:
float radiusX = Math.abs(currentX * zoom - originX * zoom);
float radiusY = Math.abs(currentY * zoom - originY * zoom);
float minX = originX * zoom - radiusX;
float minY = originY * zoom - radiusY;
g2.draw(new Ellipse2D.Float(minX, minY, radiusX * 2, radiusY * 2));
break;
}
} else if(myState == State.BEZIER_PRESS || myState == State.BEZIER_HOVER2 || myState == State.BEZIER_PRESS_P2 || myState == State.BEZIER_PRESS_P3) {
float t = 0, lx = x1 * zoom, ly = y1 * zoom;
for(int i = 0; i <= 64; i++, t += 1f / 64f) {
float x = bezier(t, x1 * zoom, x2 * zoom, x3 * zoom, x4 * zoom);
float y = bezier(t, y1 * zoom, y2 * zoom, y3 * zoom, y4 * zoom);
g2.draw(new Line2D.Float(lx, ly, x, y));
lx = x;
ly = y;
}
g2.setColor(Color.GRAY);
g2.draw(new Line2D.Float(x1 * zoom, y1 * zoom, x2 * zoom, y2 * zoom));
g2.draw(new Line2D.Float(x4 * zoom, y4 * zoom, x3 * zoom, y3 * zoom));
g2.draw(new Rectangle2D.Float(x2 * zoom - 3, y2 * zoom - 3, 6, 6));
g2.draw(new Rectangle2D.Float(x3 * zoom - 3, y3 * zoom - 3, 6, 6));
}
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_DEFAULT);
if(ad != null) {
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.8f));
g2.drawImage(ad, adPosition.x, adPosition.y, null);
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f));
}
if(select != null) {
g2.setColor(Color.BLACK);
g2.setXORMode(Color.WHITE);
g2.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10, new float[] { 4, 4 }, dashPhase));
g2.drawRect(Math.round(select.x * zoom), Math.round(select.y * zoom), Math.round(select.width * zoom), Math.round(select.height * zoom));
}
}
public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
if(img == null) {
if(infoflags == ImageObserver.ALLBITS) {
x = 0;
y = 0;
width = client.getCanvas().getWidth();
height = client.getCanvas().getHeight();
}
synchronized(image) {
Graphics2D g2 = (Graphics2D)image.getGraphics();
g2.setClip(x, y, width, height);
AlphaComposite ac = AlphaComposite.getInstance(AlphaComposite.DST_OUT, 1);
g2.setComposite(ac);
g2.fillRect(x, y, width, height);
/** Create a map of layers, showing which phantom layers they have */
IdentityHashMap<Layer, Set<Layer>> selectedLayers = new IdentityHashMap<Layer, Set<Layer>>();
for(User u : client.getUserMap().values()) {
Layer l = u.getLayer();
if(l == null) {
continue;
}
Set<Layer> us = selectedLayers.get(l);
if(us == null) {
us = new HashSet<Layer>();
selectedLayers.put(l, us);
}
us.add(u.getPhantomLayer());
}
for(Layer l : client.getCanvas().getLayerMap().values()) {
Set<Layer> ls = selectedLayers.get(l);
if(l instanceof BitmapLayer && ls != null) {
((BitmapLayer) l).draw(g2, selectedLayers.get(l));
} else {
l.draw(g2);
}
}
g2.dispose();
}
repaint((int) Math.floor(x * zoom - zoom), (int) Math.floor(y * zoom - zoom), (int) Math.ceil(width * zoom + zoom), (int) Math.ceil(height * zoom + zoom));
return true;
} else {
return super.imageUpdate(img, infoflags, x, y, width, height);
}
}
public void mouseDragged(MouseEvent e) {
cursorDragged(new TabletEvent(e, null, null));
}
public void mouseMoved(MouseEvent e) {
cursorMoved(new TabletEvent(e, null, null));
}
public void mouseClicked(MouseEvent e) {
}
public void mousePressed(MouseEvent e) {
cursorPressed(new TabletEvent(e, null, null));
}
public void mouseReleased(MouseEvent e) {
cursorReleased(new TabletEvent(e, null, null));
}
public void mouseEntered(MouseEvent e) {
cursorEntered(new TabletEvent(e, null, null));
}
public void mouseExited(MouseEvent e) {
cursorExited(new TabletEvent(e, null, null));
}
void eyedrop(MouseEvent e) {
if(client.getConnection() == null || client.getConnection().getUser() == null || client.getConnection().getUser().getLayer() == null) {
return;
}
Input input = new Input(e.getX() / zoom, e.getY() / zoom, 0);
int color = client.getConnection().getUser().getLayer().getColor(input);
client.getCommandQueue().add(new CommandEntry(0, new SetColorCommand(color)));
client.getUserInterface().setColorSliders(color);
}
public void setTagTimer(Timer tagTimer) {
this.tagTimer = tagTimer;
}
public Timer getTagTimer() {
return tagTimer;
}
public void setCanvas(Canvas canvas) {
image = new BufferedImage(canvas.getWidth(), canvas.getHeight(), BufferedImage.TYPE_INT_ARGB);
image.setAccelerationPriority(1);
canvas.addImageObserver(this);
adjust();
tagTimer.start();
yieldTimer.start();
}
public void setCanvasBackground(Color canvasBackground) {
this.canvasBackground = canvasBackground;
}
public Color getCanvasBackground() {
return canvasBackground;
}
public boolean hasJTablet() {
return (jTablet1 != null) || jTablet2;
}
void paintMouseCursor() {
if(cursorVisible && cursorType == CursorType.SOFTWARE) {
Brush brush = client.getConnection().getUser().getBrush();
float hardAdjust = 0.5f + ((brush.getHardness() - 1f) / 12f);
Input oldInput = smoother.getIndex(1);
float oldRadius = brush.isPressureToRadius() && oldInput.pressure > 0 ? (brush.getRadius() * oldInput.pressure * hardAdjust * zoom) / 255f : brush.getRadius() * hardAdjust * zoom;
float oldX = oldInput.x * zoom;
float oldY = oldInput.y * zoom;
repaint((int) Math.floor(oldX - oldRadius) - 2, (int) Math.floor(oldY - oldRadius) - 2, (int) Math.ceil(oldRadius * 2) + 4, (int) Math.ceil(oldRadius * 2) + 4);
Input input = smoother.getIndex(2);
float radius = brush.isPressureToRadius() && input.pressure > 0 ? (brush.getRadius() * input.pressure * hardAdjust * zoom) / 255f : brush.getRadius() * hardAdjust * zoom;
float x = input.x * zoom;
float y = input.y * zoom;
paintImmediately((int) Math.floor(x - oldRadius) - 2, (int) Math.floor(y - oldRadius) - 2, (int) Math.ceil(radius * 2) + 4, (int) Math.ceil(radius * 2) + 4);
}
}
private void yieldForPaint() {
if(client.getCanvas() != null) {
client.getCanvas().setDrawing(true);
yieldTimer.restart();
}
}
public void cursorEntered(TabletEvent e) {
setCursorVisible(true);
}
public void cursorExited(TabletEvent e) {
setCursorVisible(false);
}
public void cursorGestured(TabletEvent e) {
}
public void cursorMoved(TabletEvent e) {
if(client.getConnection() == null || client.getConnection().getUser() == null) {
return;
}
Input input = new Input(e.getFloatX() / zoom, e.getFloatY() / zoom, 0);
smoother.add(input);
paintMouseCursor();
if(lastCursorCommand + 500 < System.currentTimeMillis()) {
client.getCommandQueue().offer(new CommandEntry(0, new CursorCommand(smoother.getIndex(2))));
lastCursorCommand = System.currentTimeMillis();
}
State myState = getState();
boolean inSelect = select != null && select.contains(e.getX() / zoom, e.getY() / zoom);
boolean inAd = ad != null && e.getX() >= adPosition.x && e.getY() >= adPosition.y && e.getX() < adPosition.x + ad.getWidth() && e.getY() < adPosition.y + ad.getHeight();
switch(myState) {
case SCROLL_HOVER:
case SCROLL_PRESS:
break;
case SELECT_HOVER:
if(inSelect) {
setState(State.SELECT_INSIDE);
}
break;
case SELECT_INSIDE:
if(!inSelect) {
setState(State.SELECT_HOVER);
}
break;
case MOVE_HOVER:
if(inSelect) {
setState(State.MOVE_INSIDE);
}
break;
case MOVE_INSIDE:
if(!inSelect) {
setState(State.MOVE_HOVER);
}
break;
case AD_HOVER:
if(!inAd) {
restoreState();
}
break;
default:
if(inAd && storeState()) {
setState(State.AD_HOVER);
}
break;
}
}
public void cursorPressed(TabletEvent e) {
if(buttonDown != TabletEvent.NOBUTTON) {
return;
}
buttonDown = e.getButton();
yieldForPaint();
requestFocusInWindow();
User user = client.getConnection().getUser();
State myState = getState();
int button = e.getButton();
switch(button) {
case MouseEvent.BUTTON1:
switch(myState) {
case DISABLED:
return;
case DRAW_HOVER:
if(user != null && user.isViewer()) {
client.getUserInterface().println("Drawing is not permitted in viewer mode. Please sign in to participate.");
break;
}
setState(State.DRAW_PRESS);
if(jTablet2) {
pressure = Math.round(e.getPressure() * 255f);
} else if(jTablet1 != null) {
try {
if(jTablet1.poll() && jTablet1.hasCursor()) {
pressure = (jTablet1.getPressure() * 255) / jTablet1.getPressureExtent();
}
} catch(Throwable jte) { }
}
Input input = new Input(e.getFloatX() / zoom, e.getFloatY() / zoom, pressure);
smoother.add(input);
smoother.setPressure(pressure);
client.getCommandQueue().offer(new CommandEntry(0, new CursorCommand(smoother.getIndex(2))));
break;
case LINE_HOVER:
case RECT_HOVER:
case OVAL_HOVER:
originX = e.getFloatX() / zoom;
originY = e.getFloatY() / zoom;
currentX = e.getFloatX() / zoom;
currentY = e.getFloatY() / zoom;
switch(myState) {
case LINE_HOVER:
setState(State.LINE_PRESS);
break;
case RECT_HOVER:
setState(State.RECT_PRESS);
break;
case OVAL_HOVER:
setState(State.OVAL_PRESS);
break;
}
break;
case BEZIER_HOVER:
x4 = x3 = x2 = x1 = e.getFloatX() / zoom;
y4 = y3 = y2 = y1 = e.getFloatY() / zoom;
setState(State.BEZIER_PRESS);
break;
case BEZIER_HOVER2:
float x = e.getFloatX() / zoom;
float y = e.getFloatY() / zoom;
float pad = 3 / zoom;
if(x >= x2 - pad && x <= x2 + pad && y >= y2 - pad && y <= y2 + pad) {
setState(State.BEZIER_PRESS_P2);
} else if(x >= x3 - pad && x <= x3 + pad && y >= y3 - pad && y <= y3 + pad) {
setState(State.BEZIER_PRESS_P3);
}
break;
case SELECT_INSIDE:
setState(State.SELECT_MOVE);
originX = e.getX() / zoom;
originY = e.getY() / zoom;
break;
case SELECT_HOVER:
setState(State.SELECT_PRESS);
if(select != null) {
Rectangle r = new Rectangle(select);
r.grow(1, 1);
repaint(Math.round(r.x * zoom), Math.round(r.y * zoom), Math.round(r.width * zoom), Math.round(r.height * zoom));
}
select = new Rectangle(e.getX(), e.getY(), 0, 0);
iOriginX = Math.round(e.getX() / zoom);
iOriginY = Math.round(e.getY() / zoom);
break;
case MOVE_INSIDE:
if(user == null || user.isViewer()) {
client.getUserInterface().println("Moving is not permitted in viewer mode. Please sign in to participate.");
break;
}
if(user.getLayer() == null) {
break;
}
client.getCommandQueue().offer(new CommandEntry(0, new MoveCommand(select.x, select.y, select.x, select.y, select.width, select.height)));
setState(State.MOVE_PRESS);
originX = e.getX() / zoom;
originY = e.getY() / zoom;
break;
case SCROLL_HOVER:
los = e.getLocationOnScreen();
originX = los.x;
originY = los.y;
slowLOSX = los.x;
slowLOSY = los.y;
scrollSpeedX = 0;
scrollSpeedY = 0;
setState(State.SCROLL_PRESS);
BoundedRangeModel hm = client.getUserInterface().getEditorPane().getHorizontalScrollBar().getModel();
BoundedRangeModel vm = client.getUserInterface().getEditorPane().getVerticalScrollBar().getModel();
hm.setValueIsAdjusting(true);
vm.setValueIsAdjusting(true);
if(scrollTimer != null) {
scrollTimer.stop();
scrollTimer = null;
}
scrollTimer = new Timer(20, new ActionListener() {
public void actionPerformed(ActionEvent e) {
BoundedRangeModel hm = client.getUserInterface().getEditorPane().getHorizontalScrollBar().getModel();
BoundedRangeModel vm = client.getUserInterface().getEditorPane().getVerticalScrollBar().getModel();
if(getState() == State.SCROLL_PRESS) {
slowLOSX += (los.x - slowLOSX) * 0.125f;
slowLOSY += (los.y - slowLOSY) * 0.125f;
scrollSpeedX = -(slowLOSX - originX) - Math.signum(slowLOSX - originX) * (slowLOSX - originX) * (slowLOSX - originX) / 12f;
scrollSpeedY = -(slowLOSY - originY) - Math.signum(slowLOSY - originY) * (slowLOSY - originY) * (slowLOSY - originY) / 12f;
originX = slowLOSX;
originY = slowLOSY;
hm.setValue(hm.getValue() + Math.round(scrollSpeedX));
vm.setValue(vm.getValue() + Math.round(scrollSpeedY));
} else {
hm.setValue(hm.getValue() + Math.round(scrollSpeedX));
vm.setValue(vm.getValue() + Math.round(scrollSpeedY));
scrollSpeedX *= 0.95f;
scrollSpeedY *= 0.95f;
if(Math.abs(scrollSpeedX) < 0.5f && Math.abs(scrollSpeedY) < 0.5f) {
client.getUserInterface().getEditorPane().getHorizontalScrollBar().getModel().setValueIsAdjusting(false);
client.getUserInterface().getEditorPane().getVerticalScrollBar().getModel().setValueIsAdjusting(false);
scrollTimer.stop();
scrollTimer = null;
}
}
}
});
scrollTimer.setCoalesce(false);
scrollTimer.start();
break;
case AD_HOVER:
setState(State.AD_PRESS);
break;
}
break;
case MouseEvent.BUTTON3:
if(storeState()) {
setState(State.EYEDROP_PRESS);
}
break;
}
}
public void cursorDragged(TabletEvent e) {
if(client.getConnection() == null || client.getConnection().getUser() == null || client.getConnection().getUser().getLayer() == null) {
return;
}
if(jTablet2) {
pressure = Math.round(e.getPressure() * 255f);
} else if(jTablet1 != null) {
try {
if(jTablet1.poll() && jTablet1.hasCursor()) {
pressure = (jTablet1.getPressure() * 255) / jTablet1.getPressureExtent();
}
} catch(Throwable jte) { }
}
Input input = new Input(e.getFloatX() / zoom, e.getFloatY() / zoom, pressure);
smoother.add(input);
paintMouseCursor();
State myState = getState();
switch(myState) {
case DRAW_PRESS:
yieldForPaint();
if(pressure > 0) {
List<Input> inputList = smoother.get();
for(Iterator<Input> i = inputList.iterator(); i.hasNext();) {
Input in = i.next();
client.getCommandQueue().offer(new CommandEntry(0, new LineCommand(in)));
}
}
break;
case LINE_PRESS:
case RECT_PRESS:
case OVAL_PRESS:
yieldForPaint();
// Calculate where shape is right now
float radiusX = Math.abs(currentX * zoom - originX * zoom);
float radiusY = Math.abs(currentY * zoom - originY * zoom);
int minX = (int) Math.floor(originX * zoom - radiusX) - 1;
int minY = (int) Math.floor(originY * zoom - radiusY) - 1;
int width = (int) Math.ceil(radiusX * 2) + 2;
int height = (int) Math.ceil(radiusY * 2) + 2;
// Repaint this area, clearing it out
repaint(minX, minY, width, height);
currentX = e.getFloatX() / zoom;
currentY = e.getFloatY() / zoom;
// Calculate new position of shape
radiusX = Math.abs(currentX * zoom - originX * zoom);
radiusY = Math.abs(currentY * zoom - originY * zoom);
minX = (int) Math.floor(originX * zoom - radiusX) - 1;
minY = (int) Math.floor(originY * zoom - radiusY) - 1;
width = (int) Math.ceil(radiusX * 2) + 2;
height = (int) Math.ceil(radiusY * 2) + 2;
repaint(minX, minY, width, height);
break;
case BEZIER_PRESS:
yieldForPaint();
// Initialize curve to straight line
x4 = e.getFloatX() / zoom;
y4 = e.getFloatY() / zoom;
x2 = (x1 * 2 + x4) / 3;
y2 = (y1 * 2 + y4) / 3;
x3 = (x1 + x4 * 2) / 3;
y3 = (y1 + y4 * 2) / 3;
// Calculate extreme corners
minX = (int) Math.floor(Math.min(x1, x4) * zoom) - 4;
minY = (int) Math.floor(Math.min(y1, y4) * zoom) - 4;
int maxX = (int) Math.ceil(Math.max(x1, x4) * zoom) + 4;
int maxY = (int) Math.ceil(Math.max(y1, y4) * zoom) + 4;
repaint(minX, minY, maxX - minX, maxY - minY);
break;
case BEZIER_PRESS_P2:
yieldForPaint();
minX = (int) Math.floor(Math.min(x1, Math.min(x2, Math.min(x3, x4))) * zoom) - 4;
minY = (int) Math.floor(Math.min(y1, Math.min(y2, Math.min(y3, y4))) * zoom) - 4;
maxX = (int) Math.ceil(Math.max(x1, Math.max(x2, Math.max(x3, x4))) * zoom) + 4;
maxY = (int) Math.ceil(Math.max(y1, Math.max(y2, Math.max(y3, y4))) * zoom) + 4;
repaint(minX, minY, maxX - minX, maxY - minY);
x2 = e.getFloatX() / zoom;
y2 = e.getFloatY() / zoom;
minX = (int) Math.floor(Math.min(x1, Math.min(x2, Math.min(x3, x4))) * zoom) - 4;
minY = (int) Math.floor(Math.min(y1, Math.min(y2, Math.min(y3, y4))) * zoom) - 4;
maxX = (int) Math.ceil(Math.max(x1, Math.max(x2, Math.max(x3, x4))) * zoom) + 4;
maxY = (int) Math.ceil(Math.max(y1, Math.max(y2, Math.max(y3, y4))) * zoom) + 4;
repaint(minX, minY, maxX - minX, maxY - minY);
break;
case BEZIER_PRESS_P3:
yieldForPaint();
minX = (int) Math.floor(Math.min(x1, Math.min(x2, Math.min(x3, x4))) * zoom) - 4;
minY = (int) Math.floor(Math.min(y1, Math.min(y2, Math.min(y3, y4))) * zoom) - 4;
maxX = (int) Math.ceil(Math.max(x1, Math.max(x2, Math.max(x3, x4))) * zoom) + 4;
maxY = (int) Math.ceil(Math.max(y1, Math.max(y2, Math.max(y3, y4))) * zoom) + 4;
repaint(minX, minY, maxX - minX, maxY - minY);
x3 = e.getFloatX() / zoom;
y3 = e.getFloatY() / zoom;
minX = (int) Math.floor(Math.min(x1, Math.min(x2, Math.min(x3, x4))) * zoom) - 4;
minY = (int) Math.floor(Math.min(y1, Math.min(y2, Math.min(y3, y4))) * zoom) - 4;
maxX = (int) Math.ceil(Math.max(x1, Math.max(x2, Math.max(x3, x4))) * zoom) + 4;
maxY = (int) Math.ceil(Math.max(y1, Math.max(y2, Math.max(y3, y4))) * zoom) + 4;
repaint(minX, minY, maxX - minX, maxY - minY);
break;
case SELECT_MOVE:
yieldForPaint();
repaint(Math.round(select.x * zoom) - 1, Math.round(select.y * zoom) - 1, Math.round(select.width * zoom) + 2, Math.round(select.height * zoom) + 2);
select.translate((int) (e.getX() / zoom - originX), (int) (e.getY() / zoom - originY));
originX = e.getX() / zoom;
originY = e.getY() / zoom;
++dashPhase;
repaint(Math.round(select.x * zoom) - 1, Math.round(select.y * zoom) - 1, Math.round(select.width * zoom) + 2, Math.round(select.height * zoom) + 2);
break;
case SELECT_PRESS:
yieldForPaint();
minX = Math.min(iOriginX, Math.round(e.getX() / zoom));
minY = Math.min(iOriginY, Math.round(e.getY() / zoom));
maxX = Math.max(iOriginX, Math.round(e.getX() / zoom));
maxY = Math.max(iOriginY, Math.round(e.getY() / zoom));
width = maxX - minX;
height = maxY - minY;
repaint(Math.round(select.x * zoom) - 1, Math.round(select.y * zoom) - 1, Math.round(select.width * zoom) + 2, Math.round(select.height * zoom) + 2);
select.setBounds(minX, minY, maxX - minX, maxY - minY);
repaint(Math.round(select.x * zoom) - 1, Math.round(select.y * zoom) - 1, Math.round(select.width * zoom) + 2, Math.round(select.height * zoom) + 2);
break;
case MOVE_PRESS:
break;
case SCROLL_PRESS:
los = e.getLocationOnScreen();
break;
case EYEDROP_PRESS:
yieldForPaint();
eyedrop(e);
break;
}
}
public void cursorReleased(TabletEvent e) {
int button = e.getButton();
if(buttonDown == button) {
buttonDown = TabletEvent.NOBUTTON;
} else {
return;
}
State myState = getState();
switch(button) {
case TabletEvent.BUTTON1:
switch(myState) {
case DRAW_PRESS:
client.getCommandQueue().offer(new CommandEntry(0, new MergeCommand()));
setState(State.DRAW_HOVER);
break;
case LINE_PRESS:
client.getCommandQueue().offer(new CommandEntry(0, new CursorCommand(new Input(originX, originY, 0xFF))));
client.getCommandQueue().offer(new CommandEntry(0, new LineCommand(new Input(e.getFloatX() / zoom, e.getFloatY() / zoom, 0xFF))));
client.getCommandQueue().offer(new CommandEntry(0, new MergeCommand()));
setState(State.LINE_HOVER);
break;
case RECT_PRESS:
client.getCommandQueue().offer(new CommandEntry(0, new CursorCommand(new Input(originX, originY, 0xFF))));
client.getCommandQueue().offer(new CommandEntry(0, new LineCommand(new Input(e.getFloatX() / zoom, originY, 0xFF))));
client.getCommandQueue().offer(new CommandEntry(0, new CursorCommand(new Input(e.getFloatX() / zoom, originY, 0xFF))));
client.getCommandQueue().offer(new CommandEntry(0, new LineCommand(new Input(e.getFloatX() / zoom, e.getFloatY() / zoom, 0xFF))));
client.getCommandQueue().offer(new CommandEntry(0, new CursorCommand(new Input(e.getFloatX() / zoom, e.getFloatY() / zoom, 0xFF))));
client.getCommandQueue().offer(new CommandEntry(0, new LineCommand(new Input(originX, e.getFloatY() / zoom, 0xFF))));
client.getCommandQueue().offer(new CommandEntry(0, new CursorCommand(new Input(originX, e.getFloatY() / zoom, 0xFF))));
client.getCommandQueue().offer(new CommandEntry(0, new LineCommand(new Input(originX, originY, 0xFF))));
client.getCommandQueue().offer(new CommandEntry(0, new MergeCommand()));
setState(State.RECT_HOVER);
break;
case BEZIER_PRESS:
setState(State.BEZIER_HOVER2);
break;
case BEZIER_HOVER2:
client.getCommandQueue().offer(new CommandEntry(0, new CursorCommand(new Input(x1, y1, 64))));
float t = 1f / 64f;
for(int i = 1; i <= 64; i++, t += 1f / 64f) {
float x = bezier(t, x1, x2, x3, x4);
float y = bezier(t, y1, y2, y3, y4);
client.getCommandQueue().offer(new CommandEntry(0, new LineCommand(new Input(x, y, 64 + (int) (191 * Math.sin(t * Math.PI))))));
}
client.getCommandQueue().offer(new CommandEntry(0, new MergeCommand()));
setState(State.BEZIER_HOVER);
break;
case BEZIER_PRESS_P2:
case BEZIER_PRESS_P3:
setState(State.BEZIER_HOVER2);
break;
case OVAL_PRESS:
float centerX = originX;
float centerY = originY;
float radiusX = Math.abs(e.getFloatX() / zoom - centerX);
float radiusY = Math.abs(e.getFloatY() / zoom - centerY);
int divisions = 64;//(int) Math.round(Math.max(radiusX, radiusY) * Math.PI);
if(divisions == 0) {
break;
}
client.getCommandQueue().offer(new CommandEntry(0, new CursorCommand(new Input(centerX + radiusX, centerY, 0xFF))));
float alpha = 0;
for(int i = 0; i <= divisions; i++, alpha += Math.PI * 2 / divisions) {
client.getCommandQueue().offer(new CommandEntry(0, new LineCommand(new Input(centerX + (float) Math.cos(alpha) * radiusX, centerY + (float) Math.sin(alpha) * radiusY, 0xFF))));
}
client.getCommandQueue().offer(new CommandEntry(0, new MergeCommand()));
setState(State.OVAL_HOVER);
break;
case AD_PRESS:
if(e.getX() >= adPosition.x && e.getY() >= adPosition.y && e.getX() < adPosition.x + ad.getWidth() && e.getY() < adPosition.y + ad.getHeight()) {
try {
Class<?> desktopClass = Class.forName("java.awt.Desktop");
if((Boolean) desktopClass.getMethod("isDesktopSupported").invoke(null)) { // Desktop.isDesktopSupported()
Object desktop = desktopClass.getMethod("getDesktop").invoke(null); // Desktop.getDesktop()
Class<?> actionClass = Class.forName("java.awt.Desktop$Action");
if((Boolean) desktopClass.getMethod("isSupported", actionClass).invoke(desktop, actionClass.getField("BROWSE").get(null))) { // desktop.isSupported(java.awt.Desktop.Action.BROWSE)
desktopClass.getMethod("browse", URI.class).invoke(desktop, new URI("http://www.jotuntech.com/sketcher/")); // desktop.browse(uri);
}
}
} catch(Throwable t2) {
JOptionPane.showMessageDialog(client.getUserInterface(), "Visit www.jotuntech.com/sketcher/ to rent a Sketcher room!");
}
}
setState(State.AD_HOVER);
case SELECT_PRESS:
if(select.isEmpty()) {
System.err.println("Empty select!");
select = null;
}
setState(State.SELECT_HOVER);
repaint();
break;
case SELECT_MOVE:
setState(State.SELECT_INSIDE);
break;
case MOVE_HOVER:
client.getCommandQueue().offer(new CommandEntry(0, new MergeCommand()));
select = null;
repaint();
break;
case MOVE_PRESS:
repaint(Math.round(select.x * zoom) - 1, Math.round(select.y * zoom) - 1, Math.round(select.width * zoom) + 2, Math.round(select.height * zoom) + 2);
int transX = (int) (e.getX() / zoom - originX);
int transY = (int) (e.getY() / zoom - originY);
client.getCommandQueue().offer(new CommandEntry(0, new MoveCommand(select.x, select.y, select.x + transX, select.y + transY, select.width, select.height)));
select.translate(transX, transY);
++dashPhase;
repaint(Math.round(select.x * zoom) - 1, Math.round(select.y * zoom) - 1, Math.round(select.width * zoom) + 2, Math.round(select.height * zoom) + 2);
setState(State.MOVE_INSIDE);
break;
case SCROLL_PRESS:
restoreState();
break;
}
break;
case TabletEvent.BUTTON3:
switch(myState) {
case EYEDROP_PRESS:
eyedrop(e);
restoreState();
break;
}
break;
}
}
public void cursorScrolled(TabletEvent e) {
}
public void levelChanged(TabletEvent e) {
}
public BufferedImage getImage() {
return image;
}
public void setSmoothZoom(boolean enabled) {
this.smoothZoom = enabled;
}
public boolean isSmoothZoom() {
return smoothZoom;
}
public void setTagsEnabled(boolean tagsEnabled) {
this.tagsEnabled = tagsEnabled;
}
public boolean isTagsEnabled() {
return tagsEnabled;
}
public void setAd(BufferedImage ad) {
this.ad = ad;
}
public BufferedImage getAd() {
return ad;
}
public void setAdPosition(Point adPosition) {
this.adPosition = adPosition;
}
public void setAdPosition(int x, int y) {
this.adPosition = new Point(x, y);
}
public Point getAdPosition() {
return adPosition;
}
public Rectangle getSelect() {
return select;
}
public boolean isSoftwareCursorEnabled() {
return softwareCursorEnabled;
}
public void setSoftwareCursorEnabled(boolean softwareCursorEnabled) {
this.softwareCursorEnabled = softwareCursorEnabled;
}
}