commit 74627c9c7a53a486f90f259e251ad575a52d8ad8 Author: Thor Harald Johansen Date: Mon Sep 14 07:52:25 2020 +0200 Intial commit diff --git a/.classpath b/.classpath new file mode 100755 index 0000000..5fb44ea --- /dev/null +++ b/.classpath @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..52f8a89 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +**/.DS_Store +**/*.p12 +.settings +.project + +bin/ + +obfuscate-sketcher1.xml diff --git a/build.xml b/build.xml new file mode 100755 index 0000000..8f8e4ab --- /dev/null +++ b/build.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + -libraryjars "${java.home}/lib/rt.jar" + -libraryjars lib/ + -injars sketcher2-client-internal-${DSTAMP}-${TSTAMP}.jar + -outjars sketcher2-client-${DSTAMP}-${TSTAMP}.jar + -printmapping sketcher2-client-${DSTAMP}-${TSTAMP}.map + -keep public class * extends java.applet.Applet + -keep public class * extends com.jotuntech.sketcher.client.Command + -keep public class * extends com.jotuntech.sketcher.common.filter.Filter + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/TableLayout-bin-jdk1.5-2009-08-26.jar b/lib/TableLayout-bin-jdk1.5-2009-08-26.jar new file mode 100755 index 0000000..b2e2b3d Binary files /dev/null and b/lib/TableLayout-bin-jdk1.5-2009-08-26.jar differ diff --git a/lib/jspeex.jar b/lib/jspeex.jar new file mode 100755 index 0000000..c77e0df Binary files /dev/null and b/lib/jspeex.jar differ diff --git a/lib/jtablet-2010-04-13-1.jar b/lib/jtablet-2010-04-13-1.jar new file mode 100755 index 0000000..a8cf53f Binary files /dev/null and b/lib/jtablet-2010-04-13-1.jar differ diff --git a/lib/jtablet2-thin-2010-04-13-1.jar b/lib/jtablet2-thin-2010-04-13-1.jar new file mode 100755 index 0000000..c55b65e Binary files /dev/null and b/lib/jtablet2-thin-2010-04-13-1.jar differ diff --git a/lib/proguard.jar b/lib/proguard.jar new file mode 100755 index 0000000..dce7fdb Binary files /dev/null and b/lib/proguard.jar differ diff --git a/lib/proguard_task.properties b/lib/proguard_task.properties new file mode 100755 index 0000000..b676db7 --- /dev/null +++ b/lib/proguard_task.properties @@ -0,0 +1,2 @@ +proguard = proguard.ant.ProGuardTask +proguardconfiguration = proguard.ant.ConfigurationTask diff --git a/sketcher2-client-20120527-1225.jar b/sketcher2-client-20120527-1225.jar new file mode 100755 index 0000000..4f61c4c Binary files /dev/null and b/sketcher2-client-20120527-1225.jar differ diff --git a/sketcher2-client-20120527-1225.map b/sketcher2-client-20120527-1225.map new file mode 100755 index 0000000..422c644 --- /dev/null +++ b/sketcher2-client-20120527-1225.map @@ -0,0 +1,1215 @@ +com.jotuntech.sketcher.client.Client -> com.jotuntech.sketcher.client.a: + com.jotuntech.sketcher.client.UserInterface userInterface -> b + com.jotuntech.sketcher.common.TwoWayHashMap userMap -> c + com.jotuntech.sketcher.common.User[] userArray -> d + com.jotuntech.sketcher.common.Canvas canvas -> e + long lastLayerClean -> f + com.jotuntech.sketcher.client.Connection connection -> g + java.util.concurrent.BlockingQueue commandQueue -> h + java.nio.ByteBuffer commandBuffer -> i + java.lang.String login -> j + java.lang.String password -> k + java.lang.String hostname -> l + int port -> m + com.jotuntech.sketcher.client.voice.VoiceClient voiceClient -> n + boolean soundEnabled -> o + boolean ads -> p + java.util.Properties props -> q + java.io.File propsFile -> r + java.util.Timer propsTimer -> s + com.jotuntech.sketcher.common.Brush[] defaultBrushes -> t + com.jotuntech.sketcher.common.Brush[] brushes -> a + void resetProps() -> a + void loadProps() -> m + void saveProps() -> n + void close(java.lang.String) -> a + com.jotuntech.sketcher.common.TwoWayHashMap getUserMap() -> b + com.jotuntech.sketcher.common.User[] getUserArray() -> c + com.jotuntech.sketcher.common.Canvas getCanvas() -> d + void setCanvas(com.jotuntech.sketcher.common.Canvas) -> a + com.jotuntech.sketcher.client.UserInterface getUserInterface() -> e + com.jotuntech.sketcher.client.Connection getConnection() -> f + void run() -> run + java.util.concurrent.BlockingQueue getCommandQueue() -> g + boolean isVoiceEnabled() -> h + void setVoiceEnabled(boolean) -> a + void setSoundEnabled(boolean) -> b + boolean isSoundEnabled() -> i + boolean isAds() -> j + java.lang.String getLogin() -> k + java.lang.String getPassword() -> l + com.jotuntech.sketcher.common.TwoWayHashMap access$0(com.jotuntech.sketcher.client.Client) -> a + void access$1(com.jotuntech.sketcher.client.Client,com.jotuntech.sketcher.common.User[]) -> a + void access$2(com.jotuntech.sketcher.client.Client) -> b + com.jotuntech.sketcher.client.UserInterface access$3(com.jotuntech.sketcher.client.Client) -> c +com.jotuntech.sketcher.client.Client$1 -> com.jotuntech.sketcher.client.b: + com.jotuntech.sketcher.client.Client this$0 -> a + void stateChanged(javax.swing.event.ChangeEvent) -> stateChanged +com.jotuntech.sketcher.client.Client$2 -> com.jotuntech.sketcher.client.c: + com.jotuntech.sketcher.client.Client this$0 -> a + void run() -> run +com.jotuntech.sketcher.client.Client$3 -> com.jotuntech.sketcher.client.d: + com.jotuntech.sketcher.client.Client this$0 -> a + java.lang.Integer val$peerKey -> b + void voiceEvent(com.jotuntech.sketcher.client.voice.VoiceEvent) -> a +com.jotuntech.sketcher.client.ClientApplet -> com.jotuntech.sketcher.client.ClientApplet: + com.jotuntech.sketcher.client.Client client -> a + java.lang.String login -> b + java.lang.String password -> c + java.lang.String hostname -> d + int port -> e + boolean ads -> f + void init() -> init + void start() -> start + void stop() -> stop + void destroy() -> destroy +com.jotuntech.sketcher.client.Command -> com.jotuntech.sketcher.client.e: + int perform(com.jotuntech.sketcher.client.Client,com.jotuntech.sketcher.common.User) -> a +com.jotuntech.sketcher.client.CommandEntry -> com.jotuntech.sketcher.client.f: + java.lang.Integer sourceKey -> a + com.jotuntech.sketcher.client.Command command -> b + java.lang.Integer getSourceKey() -> a + com.jotuntech.sketcher.client.Command getCommand() -> b +com.jotuntech.sketcher.client.Connection -> com.jotuntech.sketcher.client.g: + java.nio.channels.SocketChannel channel -> a + com.jotuntech.sketcher.common.User user -> b + java.nio.ByteBuffer inputBuffer -> c + java.nio.ByteBuffer outputBuffer -> d + java.util.concurrent.ArrayBlockingQueue sendQueue -> e + long timeOfDeath -> f + long lastPing -> g + void setUser(com.jotuntech.sketcher.common.User) -> a + com.jotuntech.sketcher.common.User getUser() -> a + void setChannel(java.nio.channels.SocketChannel) -> a + java.nio.channels.SocketChannel getChannel() -> b + java.nio.ByteBuffer getInputBuffer() -> c + java.nio.ByteBuffer getOutputBuffer() -> d + java.util.concurrent.ArrayBlockingQueue getSendQueue() -> e + void setTimeOfDeath(long) -> a + boolean isTimeOfDeath() -> f + void setLastPing(long) -> b + long getLastPing() -> g +com.jotuntech.sketcher.client.Editor -> com.jotuntech.sketcher.client.h: + com.jotuntech.sketcher.client.Client client -> a + java.awt.image.BufferedImage image -> b + float zoom -> c + boolean smoothZoom -> d + java.awt.Dimension size -> e + cello.tablet.JTablet jTablet1 -> f + boolean jTablet2 -> g + com.jotuntech.sketcher.client.Smoother smoother -> h + java.awt.Cursor currentNativeCursor -> i + java.awt.Cursor crosshair -> j + java.awt.Cursor blank -> k + java.awt.Cursor eyedropper -> l + java.awt.Cursor hand -> m + javax.swing.Timer tagTimer -> n + javax.swing.Timer selectTimer -> o + javax.swing.Timer yieldTimer -> p + java.awt.Color canvasBackground -> q + long lastCursorCommand -> r + int dashPhase -> s + boolean tagsEnabled -> t + com.jotuntech.sketcher.client.Editor$State _state -> u + com.jotuntech.sketcher.client.Editor$State _oldState -> v + int buttonDown -> w + com.jotuntech.sketcher.client.Editor$CursorType cursorType -> x + boolean cursorVisible -> y + boolean softwareCursorEnabled -> z + java.awt.image.BufferedImage ad -> A + java.awt.Point adPosition -> B + int pressure -> C + float scrollSpeedX -> D + float scrollSpeedY -> E + float originX -> F + float originY -> G + float slowLOSX -> H + float slowLOSY -> I + java.awt.Point los -> J + javax.swing.Timer scrollTimer -> K + float currentX -> L + float currentY -> M + float x1 -> N + float y1 -> O + float x2 -> P + float y2 -> Q + float x3 -> R + float y3 -> S + float x4 -> T + float y4 -> U + int iOriginX -> V + int iOriginY -> W + java.awt.Rectangle select -> X + int[] $SWITCH_TABLE$com$jotuntech$sketcher$client$Editor$State -> Y + int[] $SWITCH_TABLE$com$jotuntech$sketcher$client$Editor$CursorType -> Z + void adjust() -> a + void setZoom(float) -> a + void setState(com.jotuntech.sketcher.client.Editor$State) -> a + com.jotuntech.sketcher.client.Editor$State getState() -> b + boolean storeState() -> i + void restoreState() -> j + void setNativeCursor(java.awt.Cursor) -> a + void setCursorVisible(boolean) -> d + void setCursorType(com.jotuntech.sketcher.client.Editor$CursorType) -> a + float bezier(float,float,float,float,float) -> a + void paint(java.awt.Graphics) -> paint + boolean imageUpdate(java.awt.Image,int,int,int,int,int) -> imageUpdate + void mouseDragged(java.awt.event.MouseEvent) -> mouseDragged + void mouseMoved(java.awt.event.MouseEvent) -> mouseMoved + void mouseClicked(java.awt.event.MouseEvent) -> mouseClicked + void mousePressed(java.awt.event.MouseEvent) -> mousePressed + void mouseReleased(java.awt.event.MouseEvent) -> mouseReleased + void mouseEntered(java.awt.event.MouseEvent) -> mouseEntered + void mouseExited(java.awt.event.MouseEvent) -> mouseExited + void eyedrop(java.awt.event.MouseEvent) -> a + void setCanvas(com.jotuntech.sketcher.common.Canvas) -> a + void setCanvasBackground(java.awt.Color) -> a + java.awt.Color getCanvasBackground() -> c + boolean hasJTablet() -> d + void paintMouseCursor() -> k + void yieldForPaint() -> l + void cursorEntered(cello.jtablet.event.TabletEvent) -> cursorEntered + void cursorExited(cello.jtablet.event.TabletEvent) -> cursorExited + void cursorGestured(cello.jtablet.event.TabletEvent) -> cursorGestured + void cursorMoved(cello.jtablet.event.TabletEvent) -> cursorMoved + void cursorPressed(cello.jtablet.event.TabletEvent) -> cursorPressed + void cursorDragged(cello.jtablet.event.TabletEvent) -> cursorDragged + void cursorReleased(cello.jtablet.event.TabletEvent) -> cursorReleased + void cursorScrolled(cello.jtablet.event.TabletEvent) -> cursorScrolled + void levelChanged(cello.jtablet.event.TabletEvent) -> levelChanged + java.awt.image.BufferedImage getImage() -> e + void setSmoothZoom(boolean) -> a + void setTagsEnabled(boolean) -> b + boolean isTagsEnabled() -> f + void setAd(java.awt.image.BufferedImage) -> a + void setAdPosition(int,int) -> a + java.awt.Rectangle getSelect() -> g + boolean isSoftwareCursorEnabled() -> h + void setSoftwareCursorEnabled(boolean) -> c + long access$0(com.jotuntech.sketcher.client.Editor) -> a + com.jotuntech.sketcher.client.Smoother access$1(com.jotuntech.sketcher.client.Editor) -> b + void access$2(com.jotuntech.sketcher.client.Editor,long) -> a + boolean access$3(com.jotuntech.sketcher.client.Editor) -> c + float access$4(com.jotuntech.sketcher.client.Editor) -> d + java.awt.Rectangle access$5(com.jotuntech.sketcher.client.Editor) -> e + int access$6(com.jotuntech.sketcher.client.Editor) -> f + void access$7(com.jotuntech.sketcher.client.Editor,int) -> a + int access$8(com.jotuntech.sketcher.client.Editor) -> g + boolean access$9(com.jotuntech.sketcher.client.Editor) -> h + void access$10(com.jotuntech.sketcher.client.Editor) -> i + int[] $SWITCH_TABLE$com$jotuntech$sketcher$client$Editor$State() -> m + int[] $SWITCH_TABLE$com$jotuntech$sketcher$client$Editor$CursorType() -> n + com.jotuntech.sketcher.client.Client access$13(com.jotuntech.sketcher.client.Editor) -> j + float access$14(com.jotuntech.sketcher.client.Editor) -> k + java.awt.Point access$15(com.jotuntech.sketcher.client.Editor) -> l + void access$16(com.jotuntech.sketcher.client.Editor,float) -> a + float access$17(com.jotuntech.sketcher.client.Editor) -> m + void access$18(com.jotuntech.sketcher.client.Editor,float) -> b + float access$19(com.jotuntech.sketcher.client.Editor) -> n + void access$20(com.jotuntech.sketcher.client.Editor,float) -> c + float access$21(com.jotuntech.sketcher.client.Editor) -> o + void access$22(com.jotuntech.sketcher.client.Editor,float) -> d + void access$23(com.jotuntech.sketcher.client.Editor,float) -> e + void access$24(com.jotuntech.sketcher.client.Editor,float) -> f + float access$25(com.jotuntech.sketcher.client.Editor) -> p + float access$26(com.jotuntech.sketcher.client.Editor) -> q + javax.swing.Timer access$27(com.jotuntech.sketcher.client.Editor) -> r + void access$28(com.jotuntech.sketcher.client.Editor,javax.swing.Timer) -> a +com.jotuntech.sketcher.client.Editor$1 -> com.jotuntech.sketcher.client.i: + com.jotuntech.sketcher.client.Editor this$0 -> a + com.jotuntech.sketcher.client.Client val$client -> b + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.Editor$2 -> com.jotuntech.sketcher.client.j: + com.jotuntech.sketcher.client.Client val$client -> a + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.Editor$3 -> com.jotuntech.sketcher.client.k: + com.jotuntech.sketcher.client.Editor this$0 -> a + com.jotuntech.sketcher.client.Client val$client -> b + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.Editor$4 -> com.jotuntech.sketcher.client.l: + com.jotuntech.sketcher.client.Editor this$0 -> a + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.Editor$5 -> com.jotuntech.sketcher.client.m: + com.jotuntech.sketcher.client.Editor this$0 -> a + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.Editor$6 -> com.jotuntech.sketcher.client.n: + com.jotuntech.sketcher.client.Editor this$0 -> a + com.jotuntech.sketcher.client.Client val$client -> b + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.Editor$7 -> com.jotuntech.sketcher.client.o: + com.jotuntech.sketcher.client.Editor this$0 -> a + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.Editor$CursorType -> com.jotuntech.sketcher.client.p: + com.jotuntech.sketcher.client.Editor$CursorType NATIVE -> a + com.jotuntech.sketcher.client.Editor$CursorType SOFTWARE -> b + com.jotuntech.sketcher.client.Editor$CursorType[] ENUM$VALUES -> c + com.jotuntech.sketcher.client.Editor$CursorType[] values() -> a +com.jotuntech.sketcher.client.Editor$State -> com.jotuntech.sketcher.client.q: + com.jotuntech.sketcher.client.Editor$State DISABLED -> a + com.jotuntech.sketcher.client.Editor$State DRAW_HOVER -> b + com.jotuntech.sketcher.client.Editor$State DRAW_PRESS -> c + com.jotuntech.sketcher.client.Editor$State LINE_HOVER -> d + com.jotuntech.sketcher.client.Editor$State LINE_PRESS -> e + com.jotuntech.sketcher.client.Editor$State RECT_HOVER -> f + com.jotuntech.sketcher.client.Editor$State RECT_PRESS -> g + com.jotuntech.sketcher.client.Editor$State OVAL_HOVER -> h + com.jotuntech.sketcher.client.Editor$State OVAL_PRESS -> i + com.jotuntech.sketcher.client.Editor$State BEZIER_HOVER -> j + com.jotuntech.sketcher.client.Editor$State BEZIER_PRESS -> k + com.jotuntech.sketcher.client.Editor$State BEZIER_HOVER2 -> l + com.jotuntech.sketcher.client.Editor$State BEZIER_PRESS_P2 -> m + com.jotuntech.sketcher.client.Editor$State BEZIER_PRESS_P3 -> n + com.jotuntech.sketcher.client.Editor$State EYEDROP_PRESS -> o + com.jotuntech.sketcher.client.Editor$State SCROLL_HOVER -> p + com.jotuntech.sketcher.client.Editor$State SCROLL_PRESS -> q + com.jotuntech.sketcher.client.Editor$State SELECT_HOVER -> r + com.jotuntech.sketcher.client.Editor$State SELECT_PRESS -> s + com.jotuntech.sketcher.client.Editor$State SELECT_INSIDE -> t + com.jotuntech.sketcher.client.Editor$State SELECT_MOVE -> u + com.jotuntech.sketcher.client.Editor$State MOVE_HOVER -> v + com.jotuntech.sketcher.client.Editor$State MOVE_INSIDE -> w + com.jotuntech.sketcher.client.Editor$State MOVE_PRESS -> x + com.jotuntech.sketcher.client.Editor$State AD_HOVER -> y + com.jotuntech.sketcher.client.Editor$State AD_PRESS -> z + com.jotuntech.sketcher.client.Editor$State[] ENUM$VALUES -> A + com.jotuntech.sketcher.client.Editor$State[] values() -> a +com.jotuntech.sketcher.client.JCollapsiblePanel -> com.jotuntech.sketcher.client.r: + javax.swing.Icon expandedIcon -> a + javax.swing.Icon collapsedIcon -> b + javax.swing.Icon pinNormalIcon -> c + javax.swing.Icon pinStuckIcon -> d + javax.swing.JLabel label -> e + javax.swing.JLabel pin -> f + javax.swing.JPanel contentPane -> g + com.jotuntech.sketcher.client.JCollapsiblePanelGroup group -> h + boolean stuck -> i + int id -> j + boolean expandedByDefault -> k + boolean stuckByDefault -> l + javax.swing.JPanel getContentPane() -> a + void setExpanded(boolean,boolean) -> a + boolean isExpanded() -> b + void setGroup(com.jotuntech.sketcher.client.JCollapsiblePanelGroup) -> a + void setStuck(boolean) -> a + boolean isStuck() -> c + int getId() -> d + boolean isExpandedByDefault() -> e + boolean isStuckByDefault() -> f + boolean access$0(com.jotuntech.sketcher.client.JCollapsiblePanel) -> a + javax.swing.JPanel access$1(com.jotuntech.sketcher.client.JCollapsiblePanel) -> b +com.jotuntech.sketcher.client.JCollapsiblePanel$1 -> com.jotuntech.sketcher.client.s: + com.jotuntech.sketcher.client.JCollapsiblePanel this$0 -> a + void mouseClicked(java.awt.event.MouseEvent) -> mouseClicked + void mouseEntered(java.awt.event.MouseEvent) -> mouseEntered + void mouseExited(java.awt.event.MouseEvent) -> mouseExited + void mousePressed(java.awt.event.MouseEvent) -> mousePressed + void mouseReleased(java.awt.event.MouseEvent) -> mouseReleased +com.jotuntech.sketcher.client.JCollapsiblePanel$2 -> com.jotuntech.sketcher.client.t: + com.jotuntech.sketcher.client.JCollapsiblePanel this$0 -> a + void mouseClicked(java.awt.event.MouseEvent) -> mouseClicked + void mouseEntered(java.awt.event.MouseEvent) -> mouseEntered + void mouseExited(java.awt.event.MouseEvent) -> mouseExited + void mousePressed(java.awt.event.MouseEvent) -> mousePressed + void mouseReleased(java.awt.event.MouseEvent) -> mouseReleased +com.jotuntech.sketcher.client.JCollapsiblePanelGroup -> com.jotuntech.sketcher.client.u: +com.jotuntech.sketcher.client.JUserEntry -> com.jotuntech.sketcher.client.v: + javax.swing.JLabel nameLabel -> a + javax.swing.JPanel infoPanel -> b + javax.swing.JProgressBar volumeBar -> c + double slowVolume -> d + int[] delayLine -> e + void setVolume(int,int) -> a + void clearVoice() -> a +com.jotuntech.sketcher.client.JUserList -> com.jotuntech.sketcher.client.w: + com.jotuntech.sketcher.common.TwoWayHashMap userMap -> a + java.util.Map entryMap -> b + void redo() -> b + com.jotuntech.sketcher.client.JUserEntry getEntry(java.lang.Integer) -> a + void clearVoice() -> a + void access$0(com.jotuntech.sketcher.client.JUserList) -> a +com.jotuntech.sketcher.client.JUserList$1 -> com.jotuntech.sketcher.client.x: + com.jotuntech.sketcher.client.JUserList this$0 -> a + void stateChanged(javax.swing.event.ChangeEvent) -> stateChanged +com.jotuntech.sketcher.client.LookAndFeel -> com.jotuntech.sketcher.client.y: + com.jotuntech.sketcher.client.LookAndFeel NIMBUS -> a + com.jotuntech.sketcher.client.LookAndFeel SYSTEM -> b + java.lang.String getName() -> a + boolean activate() -> b +com.jotuntech.sketcher.client.LookAndFeel$1 -> com.jotuntech.sketcher.client.z: + java.lang.String name -> c + java.lang.String getName() -> a +com.jotuntech.sketcher.client.LookAndFeel$2 -> com.jotuntech.sketcher.client.A: + java.lang.String getName() -> a +com.jotuntech.sketcher.client.PSDEncoder -> com.jotuntech.sketcher.client.B: + java.io.RandomAccessFile raf -> a + java.awt.image.BufferedImage image -> b + com.jotuntech.sketcher.common.BitmapLayer[] layers -> c + long[][] channelLengthPositions -> d + void encode() -> a + void writeAllLayersPixelData(java.io.RandomAccessFile) -> a + void writeImagePixelData(java.io.RandomAccessFile) -> b +com.jotuntech.sketcher.client.PackBitsOutputStream -> com.jotuntech.sketcher.client.C: + java.io.DataOutput mdo -> a + int[] inBuffer -> b + int inBufferWritePos -> c + int inBufferReadPos -> d + int inBufferMark -> e + byte[] outBuffer -> f + int outBufferWritePos -> g + void write(int) -> write + void write(byte[]) -> write + void write(byte[],int,int) -> write + void flush() -> flush + void output(int) -> a + void pack() -> a + void close() -> close + int available() -> b + int peek() -> c + int read() -> d + void reset() -> e +com.jotuntech.sketcher.client.Smoother -> com.jotuntech.sketcher.client.D: + float threshold -> a + float smoothness -> b + boolean enabled -> c + com.jotuntech.sketcher.common.Input[] cursor -> d + void add(com.jotuntech.sketcher.common.Input) -> a + java.util.List get() -> a + com.jotuntech.sketcher.common.Input getIndex(int) -> a + void segment(java.util.List,com.jotuntech.sketcher.common.Input,com.jotuntech.sketcher.common.Input,com.jotuntech.sketcher.common.Input) -> a + void setSmoothness(float) -> a + void setEnabled(boolean) -> a +com.jotuntech.sketcher.client.UserInterface -> com.jotuntech.sketcher.client.E: + java.awt.Insets NO_INSETS -> a + com.jotuntech.sketcher.client.Client client -> b + javax.swing.JSplitPane splitPane -> k + javax.swing.JScrollPane editorPane -> l + com.jotuntech.sketcher.client.Editor editor -> m + javax.swing.JPanel rightPanel -> n + javax.swing.JMenuBar menuBar -> o + javax.swing.JMenu filterMenu -> p + javax.swing.JCheckBoxMenuItem soundItem -> q + javax.swing.JCheckBoxMenuItem tagsItem -> r + com.jotuntech.sketcher.client.JCollapsiblePanelGroup mainGroup -> s + javax.swing.JScrollPane chatPane -> t + javax.swing.JTextArea chatLog -> u + javax.swing.JTextField chatField -> v + com.jotuntech.sketcher.client.UserInterface$BrushButton[] brushButtons -> w + javax.swing.JLabel zoomLabel -> x + javax.swing.JSpinner zoomSpin -> y + javax.swing.JCheckBox smoothBox -> c + javax.swing.JCheckBox opacityBox -> z + javax.swing.JCheckBox flowBox -> A + javax.swing.JCheckBox radiusBox -> B + javax.swing.JCheckBox lockTransBox -> C + com.jotuntech.sketcher.client.JUserList userList -> D + javax.swing.JList layerList -> E + javax.swing.JSlider hardnessSlider -> F + javax.swing.JSlider sizeSlider -> G + javax.swing.JSlider opacitySlider -> H + javax.swing.JSlider flowSlider -> I + javax.swing.JSlider spacingSlider -> J + javax.swing.JSlider jitterSlider -> K + javax.swing.JSlider noiseSlider -> L + javax.swing.JSlider waterSlider -> M + javax.swing.JSlider waterAreaSlider -> N + javax.swing.JSlider redSlider -> O + javax.swing.JSlider greenSlider -> P + javax.swing.JSlider blueSlider -> Q + javax.swing.JPanel rgbColor -> R + javax.swing.JSlider hueSlider -> S + javax.swing.JSlider satSlider -> T + javax.swing.JSlider brightSlider -> U + javax.swing.JPanel hsbColor -> V + javax.swing.JSlider cyanSlider -> W + javax.swing.JSlider magentaSlider -> X + javax.swing.JSlider yellowSlider -> Y + javax.swing.JPanel cmyColor -> Z + com.jotuntech.sketcher.client.JCollapsiblePanel brushPanel -> aa + com.jotuntech.sketcher.client.JCollapsiblePanel brushSettingsPanel -> ab + com.jotuntech.sketcher.client.JCollapsiblePanel colorPanel -> ac + com.jotuntech.sketcher.client.JCollapsiblePanel inputPanel -> ad + javax.swing.JToggleButton selectButton -> ae + javax.swing.JToggleButton moveButton -> af + javax.swing.JToggleButton freehandButton -> ag + javax.swing.JToggleButton lineButton -> ah + javax.swing.JToggleButton bezierButton -> ai + javax.swing.JToggleButton rectangleButton -> aj + javax.swing.JToggleButton ovalButton -> ak + boolean smoothZoom -> al + boolean colorAdjusting -> am + boolean brushAdjusting -> d + boolean layerAdjusting -> an + java.applet.AudioClip AUDIO_INTRO -> e + java.applet.AudioClip AUDIO_SIGN_IN -> f + java.applet.AudioClip AUDIO_SIGN_OUT -> g + java.applet.AudioClip AUDIO_KICK -> h + java.applet.AudioClip AUDIO_CHAT -> i + java.applet.AudioClip AUDIO_OUTTRO -> j + javax.swing.Icon[] brushIcons -> ao + javax.swing.ImageIcon loadImageIcon(java.lang.String) -> c + void updateBrushSliders() -> a + void setColorSliders(int) -> a + void setRGBSliders(int) -> b + void setHSBSliders(int) -> c + void setCMYSliders(int) -> d + void println(java.lang.String) -> a + void print(java.lang.String) -> b + void scroll() -> b + void setCanvas(com.jotuntech.sketcher.common.Canvas) -> a + com.jotuntech.sketcher.client.Editor getEditor() -> c + javax.swing.JScrollPane getEditorPane() -> d + void setSmoothZoom(boolean) -> a + boolean isSmoothZoom() -> e + javax.swing.JCheckBoxMenuItem getSoundItem() -> f + javax.swing.JCheckBoxMenuItem getTagsItem() -> g + java.util.Set getToolPanels() -> h + com.jotuntech.sketcher.client.JUserList getUserList() -> i + void updateLayer() -> j + com.jotuntech.sketcher.client.UserInterface$BrushButton[] getBrushButtons() -> k + com.jotuntech.sketcher.client.Editor access$0(com.jotuntech.sketcher.client.UserInterface) -> a + javax.swing.JScrollPane access$1(com.jotuntech.sketcher.client.UserInterface) -> b + boolean access$2(com.jotuntech.sketcher.client.UserInterface) -> c + void access$3(com.jotuntech.sketcher.client.UserInterface,boolean) -> a + javax.swing.JSpinner access$4(com.jotuntech.sketcher.client.UserInterface) -> d + void access$5(com.jotuntech.sketcher.client.UserInterface,boolean) -> b + void access$6(com.jotuntech.sketcher.client.UserInterface) -> e + javax.swing.JSlider access$7(com.jotuntech.sketcher.client.UserInterface) -> f + javax.swing.JSlider access$8(com.jotuntech.sketcher.client.UserInterface) -> g + javax.swing.JSlider access$9(com.jotuntech.sketcher.client.UserInterface) -> h + javax.swing.JSlider access$10(com.jotuntech.sketcher.client.UserInterface) -> i + javax.swing.JSlider access$11(com.jotuntech.sketcher.client.UserInterface) -> j + javax.swing.JSlider access$12(com.jotuntech.sketcher.client.UserInterface) -> k + javax.swing.JSlider access$13(com.jotuntech.sketcher.client.UserInterface) -> l + javax.swing.JSlider access$14(com.jotuntech.sketcher.client.UserInterface) -> m + javax.swing.JSlider access$15(com.jotuntech.sketcher.client.UserInterface) -> n + javax.swing.JCheckBox access$16(com.jotuntech.sketcher.client.UserInterface) -> o + javax.swing.JCheckBox access$17(com.jotuntech.sketcher.client.UserInterface) -> p + javax.swing.JCheckBox access$18(com.jotuntech.sketcher.client.UserInterface) -> q + javax.swing.JCheckBox access$19(com.jotuntech.sketcher.client.UserInterface) -> r + boolean access$20(com.jotuntech.sketcher.client.UserInterface) -> s + javax.swing.JSlider access$21(com.jotuntech.sketcher.client.UserInterface) -> t + javax.swing.JSlider access$22(com.jotuntech.sketcher.client.UserInterface) -> u + javax.swing.JSlider access$23(com.jotuntech.sketcher.client.UserInterface) -> v + javax.swing.JPanel access$24(com.jotuntech.sketcher.client.UserInterface) -> w + javax.swing.JSlider access$25(com.jotuntech.sketcher.client.UserInterface) -> x + javax.swing.JSlider access$26(com.jotuntech.sketcher.client.UserInterface) -> y + javax.swing.JSlider access$27(com.jotuntech.sketcher.client.UserInterface) -> z + javax.swing.JPanel access$28(com.jotuntech.sketcher.client.UserInterface) -> A + javax.swing.JSlider access$29(com.jotuntech.sketcher.client.UserInterface) -> B + javax.swing.JSlider access$30(com.jotuntech.sketcher.client.UserInterface) -> C + javax.swing.JSlider access$31(com.jotuntech.sketcher.client.UserInterface) -> D + javax.swing.JPanel access$32(com.jotuntech.sketcher.client.UserInterface) -> E + javax.swing.JTextField access$33(com.jotuntech.sketcher.client.UserInterface) -> F + javax.swing.JScrollPane access$34(com.jotuntech.sketcher.client.UserInterface) -> G + javax.swing.JTextArea access$35(com.jotuntech.sketcher.client.UserInterface) -> H + javax.swing.JList access$36(com.jotuntech.sketcher.client.UserInterface) -> I + boolean access$37(com.jotuntech.sketcher.client.UserInterface) -> J +com.jotuntech.sketcher.client.UserInterface$1 -> com.jotuntech.sketcher.client.F: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + boolean val$selectionOnly -> b + java.io.File val$file -> c + javax.swing.JFileChooser val$fileChooser -> d + void run() -> run +com.jotuntech.sketcher.client.UserInterface$10 -> com.jotuntech.sketcher.client.G: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + com.jotuntech.sketcher.client.Client val$client -> b + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.UserInterface$11 -> com.jotuntech.sketcher.client.H: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + com.jotuntech.sketcher.client.Client val$client -> b + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.UserInterface$11$1 -> com.jotuntech.sketcher.client.I: + com.jotuntech.sketcher.client.UserInterface$11 this$1 -> a + javax.swing.JDialog val$blurDialog -> b + com.jotuntech.sketcher.client.Client val$client -> c + java.awt.Rectangle val$select -> d + javax.swing.SpinnerNumberModel val$sizeModel -> e + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.UserInterface$11$2 -> com.jotuntech.sketcher.client.J: + com.jotuntech.sketcher.client.UserInterface$11 this$1 -> a + javax.swing.JDialog val$blurDialog -> b + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.UserInterface$12 -> com.jotuntech.sketcher.client.K: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + com.jotuntech.sketcher.client.Client val$client -> b + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.UserInterface$13 -> com.jotuntech.sketcher.client.L: + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.UserInterface$13$1 -> com.jotuntech.sketcher.client.M: + javax.swing.JDialog val$aboutDialog -> a + void mouseClicked(java.awt.event.MouseEvent) -> mouseClicked + void mouseEntered(java.awt.event.MouseEvent) -> mouseEntered + void mouseExited(java.awt.event.MouseEvent) -> mouseExited + void mousePressed(java.awt.event.MouseEvent) -> mousePressed + void mouseReleased(java.awt.event.MouseEvent) -> mouseReleased +com.jotuntech.sketcher.client.UserInterface$14 -> com.jotuntech.sketcher.client.N: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + void mouseClicked(java.awt.event.MouseEvent) -> mouseClicked + void mouseEntered(java.awt.event.MouseEvent) -> mouseEntered + void mouseExited(java.awt.event.MouseEvent) -> mouseExited + void mousePressed(java.awt.event.MouseEvent) -> mousePressed + void mouseReleased(java.awt.event.MouseEvent) -> mouseReleased +com.jotuntech.sketcher.client.UserInterface$15 -> com.jotuntech.sketcher.client.O: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + com.jotuntech.sketcher.client.Client val$client -> b + void stateChanged(javax.swing.event.ChangeEvent) -> stateChanged +com.jotuntech.sketcher.client.UserInterface$16 -> com.jotuntech.sketcher.client.P: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.UserInterface$17 -> com.jotuntech.sketcher.client.Q: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + javax.swing.JSlider val$backgroundSlider -> b + void stateChanged(javax.swing.event.ChangeEvent) -> stateChanged +com.jotuntech.sketcher.client.UserInterface$18 -> com.jotuntech.sketcher.client.R: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.UserInterface$19 -> com.jotuntech.sketcher.client.S: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.UserInterface$2 -> com.jotuntech.sketcher.client.T: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + void adjustmentValueChanged(java.awt.event.AdjustmentEvent) -> adjustmentValueChanged +com.jotuntech.sketcher.client.UserInterface$20 -> com.jotuntech.sketcher.client.U: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.UserInterface$21 -> com.jotuntech.sketcher.client.V: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.UserInterface$22 -> com.jotuntech.sketcher.client.W: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.UserInterface$23 -> com.jotuntech.sketcher.client.X: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.UserInterface$24 -> com.jotuntech.sketcher.client.Y: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.UserInterface$25 -> com.jotuntech.sketcher.client.Z: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + com.jotuntech.sketcher.client.Client val$client -> b + void stateChanged(javax.swing.event.ChangeEvent) -> stateChanged +com.jotuntech.sketcher.client.UserInterface$26 -> com.jotuntech.sketcher.client.aa: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + com.jotuntech.sketcher.client.Client val$client -> b + void stateChanged(javax.swing.event.ChangeEvent) -> stateChanged +com.jotuntech.sketcher.client.UserInterface$27 -> com.jotuntech.sketcher.client.ab: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + com.jotuntech.sketcher.client.Client val$client -> b + void stateChanged(javax.swing.event.ChangeEvent) -> stateChanged +com.jotuntech.sketcher.client.UserInterface$28 -> com.jotuntech.sketcher.client.ac: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + com.jotuntech.sketcher.client.Client val$client -> b + void stateChanged(javax.swing.event.ChangeEvent) -> stateChanged +com.jotuntech.sketcher.client.UserInterface$29 -> com.jotuntech.sketcher.client.ad: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + com.jotuntech.sketcher.client.Client val$client -> b + void stateChanged(javax.swing.event.ChangeEvent) -> stateChanged +com.jotuntech.sketcher.client.UserInterface$3 -> com.jotuntech.sketcher.client.ae: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.UserInterface$30 -> com.jotuntech.sketcher.client.af: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + com.jotuntech.sketcher.client.Client val$client -> b + void stateChanged(javax.swing.event.ChangeEvent) -> stateChanged +com.jotuntech.sketcher.client.UserInterface$31 -> com.jotuntech.sketcher.client.ag: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + com.jotuntech.sketcher.client.Client val$client -> b + void stateChanged(javax.swing.event.ChangeEvent) -> stateChanged +com.jotuntech.sketcher.client.UserInterface$32 -> com.jotuntech.sketcher.client.ah: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + com.jotuntech.sketcher.client.Client val$client -> b + void stateChanged(javax.swing.event.ChangeEvent) -> stateChanged +com.jotuntech.sketcher.client.UserInterface$33 -> com.jotuntech.sketcher.client.ai: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + com.jotuntech.sketcher.client.Client val$client -> b + void stateChanged(javax.swing.event.ChangeEvent) -> stateChanged +com.jotuntech.sketcher.client.UserInterface$34 -> com.jotuntech.sketcher.client.aj: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + com.jotuntech.sketcher.client.Client val$client -> b + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.UserInterface$35 -> com.jotuntech.sketcher.client.ak: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + com.jotuntech.sketcher.client.Client val$client -> b + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.UserInterface$36 -> com.jotuntech.sketcher.client.al: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + com.jotuntech.sketcher.client.Client val$client -> b + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.UserInterface$37 -> com.jotuntech.sketcher.client.am: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + com.jotuntech.sketcher.client.Client val$client -> b + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.UserInterface$38 -> com.jotuntech.sketcher.client.an: + com.jotuntech.sketcher.client.Client val$client -> a + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.UserInterface$39 -> com.jotuntech.sketcher.client.ao: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + com.jotuntech.sketcher.client.Client val$client -> b + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.UserInterface$4 -> com.jotuntech.sketcher.client.ap: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.UserInterface$40 -> com.jotuntech.sketcher.client.aq: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + com.jotuntech.sketcher.client.Client val$client -> b + void stateChanged(javax.swing.event.ChangeEvent) -> stateChanged +com.jotuntech.sketcher.client.UserInterface$41 -> com.jotuntech.sketcher.client.ar: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + com.jotuntech.sketcher.client.Client val$client -> b + void stateChanged(javax.swing.event.ChangeEvent) -> stateChanged +com.jotuntech.sketcher.client.UserInterface$42 -> com.jotuntech.sketcher.client.as: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + com.jotuntech.sketcher.client.Client val$client -> b + void stateChanged(javax.swing.event.ChangeEvent) -> stateChanged +com.jotuntech.sketcher.client.UserInterface$43 -> com.jotuntech.sketcher.client.at: + javax.swing.JSlider val$smoothSlider -> a + void stateChanged(javax.swing.event.ChangeEvent) -> stateChanged +com.jotuntech.sketcher.client.UserInterface$44 -> com.jotuntech.sketcher.client.au: + javax.swing.JCheckBox val$curveSmoothBox -> a + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.UserInterface$45 -> com.jotuntech.sketcher.client.av: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + void componentHidden(java.awt.event.ComponentEvent) -> componentHidden + void componentMoved(java.awt.event.ComponentEvent) -> componentMoved + void componentResized(java.awt.event.ComponentEvent) -> componentResized + void componentShown(java.awt.event.ComponentEvent) -> componentShown +com.jotuntech.sketcher.client.UserInterface$46 -> com.jotuntech.sketcher.client.aw: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + com.jotuntech.sketcher.client.Client val$client -> b + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.UserInterface$47 -> com.jotuntech.sketcher.client.ax: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + java.lang.String val$str -> b + void run() -> run +com.jotuntech.sketcher.client.UserInterface$48 -> com.jotuntech.sketcher.client.ay: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + void stateChanged(javax.swing.event.ChangeEvent) -> stateChanged +com.jotuntech.sketcher.client.UserInterface$48$1 -> com.jotuntech.sketcher.client.az: + java.lang.Object[] val$array -> a + int getSize() -> getSize + java.lang.Object getElementAt(int) -> getElementAt +com.jotuntech.sketcher.client.UserInterface$49 -> com.jotuntech.sketcher.client.aA: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + com.jotuntech.sketcher.common.Canvas val$canvas -> b + void valueChanged(javax.swing.event.ListSelectionEvent) -> valueChanged +com.jotuntech.sketcher.client.UserInterface$5 -> com.jotuntech.sketcher.client.aB: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + com.jotuntech.sketcher.client.Client val$client -> b + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.UserInterface$5$1 -> com.jotuntech.sketcher.client.aC: + javax.swing.JDialog val$uploadDialog -> a + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.UserInterface$5$2 -> com.jotuntech.sketcher.client.aD: + com.jotuntech.sketcher.client.UserInterface$5 this$1 -> a + javax.swing.JTextField val$titleField -> b + javax.swing.JDialog val$uploadDialog -> c + javax.swing.JTextArea val$descArea -> d + javax.swing.JButton val$uploadButton -> e + javax.swing.JRadioButton val$jpegButton -> f + javax.swing.JRadioButton val$pngButton -> g + javax.swing.JCheckBox val$matureBox -> h + java.awt.Rectangle val$s -> i + javax.swing.JProgressBar val$progressBar -> j + com.jotuntech.sketcher.client.Client val$client -> k + javax.swing.JButton val$cancelButton -> l + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.UserInterface$5$2$1 -> com.jotuntech.sketcher.client.aE: + com.jotuntech.sketcher.client.UserInterface$5$2 this$2 -> a + javax.swing.JTextField val$titleField -> b + javax.swing.JDialog val$uploadDialog -> c + javax.swing.JTextArea val$descArea -> d + javax.swing.JButton val$uploadButton -> e + javax.swing.JRadioButton val$jpegButton -> f + javax.swing.JRadioButton val$pngButton -> g + javax.swing.JCheckBox val$matureBox -> h + java.awt.Rectangle val$s -> i + javax.swing.JProgressBar val$progressBar -> j + com.jotuntech.sketcher.client.Client val$client -> k + javax.swing.JButton val$cancelButton -> l + void run() -> run +com.jotuntech.sketcher.client.UserInterface$6 -> com.jotuntech.sketcher.client.aF: + com.jotuntech.sketcher.client.Client val$client -> a + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.UserInterface$7 -> com.jotuntech.sketcher.client.aG: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.UserInterface$8 -> com.jotuntech.sketcher.client.aH: + com.jotuntech.sketcher.client.Client val$client -> a + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.UserInterface$9 -> com.jotuntech.sketcher.client.aI: + com.jotuntech.sketcher.client.UserInterface this$0 -> a + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.UserInterface$BrushButton -> com.jotuntech.sketcher.client.aJ: +com.jotuntech.sketcher.client.UserInterface$BrushButton$1 -> com.jotuntech.sketcher.client.aK: + com.jotuntech.sketcher.client.UserInterface val$parent -> a + com.jotuntech.sketcher.common.Brush[] val$brushArray -> b + int val$brushIndex -> c + void actionPerformed(java.awt.event.ActionEvent) -> actionPerformed +com.jotuntech.sketcher.client.command.CanvasCommand -> com.jotuntech.sketcher.client.command.CanvasCommand: + int width -> a + int height -> b + int perform(com.jotuntech.sketcher.client.Client,com.jotuntech.sketcher.common.User) -> a + void decode(java.nio.ByteBuffer) -> a + void encode(java.nio.ByteBuffer) -> b +com.jotuntech.sketcher.client.command.CanvasCommand$1 -> com.jotuntech.sketcher.client.command.a: + com.jotuntech.sketcher.client.command.CanvasCommand this$0 -> a + com.jotuntech.sketcher.client.Client val$client -> b + void run() -> run +com.jotuntech.sketcher.client.command.CreateLayerCommand -> com.jotuntech.sketcher.client.command.CreateLayerCommand: + int layerKey -> a + int type -> b + java.lang.String name -> c + int perform(com.jotuntech.sketcher.client.Client,com.jotuntech.sketcher.common.User) -> a + void decode(java.nio.ByteBuffer) -> a + void encode(java.nio.ByteBuffer) -> b +com.jotuntech.sketcher.client.command.CursorCommand -> com.jotuntech.sketcher.client.command.CursorCommand: + com.jotuntech.sketcher.common.Input input -> a + int perform(com.jotuntech.sketcher.client.Client,com.jotuntech.sketcher.common.User) -> a + void decode(java.nio.ByteBuffer) -> a + void encode(java.nio.ByteBuffer) -> b +com.jotuntech.sketcher.client.command.DeleteLayerCommand -> com.jotuntech.sketcher.client.command.DeleteLayerCommand: + int layerKey -> a + int perform(com.jotuntech.sketcher.client.Client,com.jotuntech.sketcher.common.User) -> a + void decode(java.nio.ByteBuffer) -> a + void encode(java.nio.ByteBuffer) -> b +com.jotuntech.sketcher.client.command.FilterCommand -> com.jotuntech.sketcher.client.command.FilterCommand: + com.jotuntech.sketcher.common.filter.Filter filter -> a + int x -> b + int y -> c + int w -> d + int h -> e + float a -> f + float b -> g + float c -> h + int perform(com.jotuntech.sketcher.client.Client,com.jotuntech.sketcher.common.User) -> a + void decode(java.nio.ByteBuffer) -> a + void encode(java.nio.ByteBuffer) -> b +com.jotuntech.sketcher.client.command.KickCommand -> com.jotuntech.sketcher.client.command.KickCommand: + int targetKey -> a + java.lang.String reason -> b + int perform(com.jotuntech.sketcher.client.Client,com.jotuntech.sketcher.common.User) -> a + void decode(java.nio.ByteBuffer) -> a + void encode(java.nio.ByteBuffer) -> b +com.jotuntech.sketcher.client.command.LayerDataCommand -> com.jotuntech.sketcher.client.command.LayerDataCommand: + int layerKey -> a + boolean phantom -> b + byte[] data -> c + int perform(com.jotuntech.sketcher.client.Client,com.jotuntech.sketcher.common.User) -> a + void decode(java.nio.ByteBuffer) -> a + void encode(java.nio.ByteBuffer) -> b +com.jotuntech.sketcher.client.command.LineCommand -> com.jotuntech.sketcher.client.command.LineCommand: + com.jotuntech.sketcher.common.Input input -> a + int perform(com.jotuntech.sketcher.client.Client,com.jotuntech.sketcher.common.User) -> a + void decode(java.nio.ByteBuffer) -> a + void encode(java.nio.ByteBuffer) -> b +com.jotuntech.sketcher.client.command.MergeCommand -> com.jotuntech.sketcher.client.command.MergeCommand: + int perform(com.jotuntech.sketcher.client.Client,com.jotuntech.sketcher.common.User) -> a + void decode(java.nio.ByteBuffer) -> a + void encode(java.nio.ByteBuffer) -> b +com.jotuntech.sketcher.client.command.MoveCommand -> com.jotuntech.sketcher.client.command.MoveCommand: + float sx -> a + float sy -> b + float dx -> c + float dy -> d + float w -> e + float h -> f + int perform(com.jotuntech.sketcher.client.Client,com.jotuntech.sketcher.common.User) -> a + void decode(java.nio.ByteBuffer) -> a + void encode(java.nio.ByteBuffer) -> b +com.jotuntech.sketcher.client.command.PingCommand -> com.jotuntech.sketcher.client.command.PingCommand: + long timestamp -> a + int perform(com.jotuntech.sketcher.client.Client,com.jotuntech.sketcher.common.User) -> a + void decode(java.nio.ByteBuffer) -> a + void encode(java.nio.ByteBuffer) -> b +com.jotuntech.sketcher.client.command.SayCommand -> com.jotuntech.sketcher.client.command.SayCommand: + boolean isAction -> a + java.lang.String text -> b + int perform(com.jotuntech.sketcher.client.Client,com.jotuntech.sketcher.common.User) -> a + void decode(java.nio.ByteBuffer) -> a + void encode(java.nio.ByteBuffer) -> b +com.jotuntech.sketcher.client.command.ServerMessageCommand -> com.jotuntech.sketcher.client.command.ServerMessageCommand: + java.lang.String text -> a + int perform(com.jotuntech.sketcher.client.Client,com.jotuntech.sketcher.common.User) -> a + void decode(java.nio.ByteBuffer) -> a + void encode(java.nio.ByteBuffer) -> b +com.jotuntech.sketcher.client.command.SetBrushCommand -> com.jotuntech.sketcher.client.command.SetBrushCommand: + com.jotuntech.sketcher.common.Brush brush -> a + int perform(com.jotuntech.sketcher.client.Client,com.jotuntech.sketcher.common.User) -> a + void decode(java.nio.ByteBuffer) -> a + void encode(java.nio.ByteBuffer) -> b +com.jotuntech.sketcher.client.command.SetColorCommand -> com.jotuntech.sketcher.client.command.SetColorCommand: + boolean firstWhite -> a + int color -> b + int perform(com.jotuntech.sketcher.client.Client,com.jotuntech.sketcher.common.User) -> a + void decode(java.nio.ByteBuffer) -> a + void encode(java.nio.ByteBuffer) -> b +com.jotuntech.sketcher.client.command.SetLayerCommand -> com.jotuntech.sketcher.client.command.SetLayerCommand: + java.lang.Integer layerKey -> a + int perform(com.jotuntech.sketcher.client.Client,com.jotuntech.sketcher.common.User) -> a + void decode(java.nio.ByteBuffer) -> a + void encode(java.nio.ByteBuffer) -> b +com.jotuntech.sketcher.client.command.SignInCommand -> com.jotuntech.sketcher.client.command.SignInCommand: + java.lang.Integer peerKey -> a + java.lang.String login -> b + java.lang.String password -> c + boolean viewer -> d + void decode(java.nio.ByteBuffer) -> a + void encode(java.nio.ByteBuffer) -> b + int perform(com.jotuntech.sketcher.client.Client,com.jotuntech.sketcher.common.User) -> a +com.jotuntech.sketcher.client.command.SignOutCommand -> com.jotuntech.sketcher.client.command.SignOutCommand: + java.lang.String message -> a + int perform(com.jotuntech.sketcher.client.Client,com.jotuntech.sketcher.common.User) -> a + void decode(java.nio.ByteBuffer) -> a + void encode(java.nio.ByteBuffer) -> b +com.jotuntech.sketcher.client.command.UndoCommand -> com.jotuntech.sketcher.client.command.UndoCommand: + int perform(com.jotuntech.sketcher.client.Client,com.jotuntech.sketcher.common.User) -> a + void decode(java.nio.ByteBuffer) -> a + void encode(java.nio.ByteBuffer) -> b +com.jotuntech.sketcher.client.command.UndoDataCommand -> com.jotuntech.sketcher.client.command.UndoDataCommand: + byte[] data -> a + int perform(com.jotuntech.sketcher.client.Client,com.jotuntech.sketcher.common.User) -> a + void decode(java.nio.ByteBuffer) -> a + void encode(java.nio.ByteBuffer) -> b +com.jotuntech.sketcher.client.command.UndoEntryCommand -> com.jotuntech.sketcher.client.command.UndoEntryCommand: + java.lang.Integer layerKey -> a + int perform(com.jotuntech.sketcher.client.Client,com.jotuntech.sketcher.common.User) -> a + void decode(java.nio.ByteBuffer) -> a + void encode(java.nio.ByteBuffer) -> b +com.jotuntech.sketcher.client.command.VoiceCommand -> com.jotuntech.sketcher.client.command.VoiceCommand: + boolean voiceEnabled -> a + int perform(com.jotuntech.sketcher.client.Client,com.jotuntech.sketcher.common.User) -> a + void decode(java.nio.ByteBuffer) -> a + void encode(java.nio.ByteBuffer) -> b +com.jotuntech.sketcher.client.voice.Highpass24 -> com.jotuntech.sketcher.client.a.a: + float li1 -> a + float li2 -> b + float li3 -> c + float li4 -> d + float lo1 -> e + float lo2 -> f + float lo3 -> g + float lo4 -> h +com.jotuntech.sketcher.client.voice.VoiceChannel -> com.jotuntech.sketcher.client.a.b: + org.xiph.speex.SpeexDecoder decoder -> a + java.nio.ByteBuffer[] packetArray -> b + int expectedPacketNumber -> c + int newestPacketNumber -> d + float qualityPercent -> e + java.nio.ByteBuffer audioBuffer -> f + com.jotuntech.sketcher.client.voice.VoiceMixer voiceMixer -> g + java.lang.Integer channelKey -> h + boolean buffering -> i + long lastPlayTime -> j + boolean dead -> k + com.jotuntech.sketcher.client.voice.VoiceListener listener -> l + void packet(java.nio.ByteBuffer) -> a + void process() -> a + boolean isDead() -> b + void setListener(com.jotuntech.sketcher.client.voice.VoiceListener) -> a +com.jotuntech.sketcher.client.voice.VoiceClient -> com.jotuntech.sketcher.client.a.c: + java.nio.channels.DatagramChannel channel -> a + com.jotuntech.sketcher.client.voice.VoiceMixer voiceMixer -> b + java.util.Map voiceChannels -> c + java.lang.Integer peerKey -> d + javax.sound.sampled.AudioFormat targetFormat -> e + javax.sound.sampled.TargetDataLine target -> f + org.xiph.speex.SpeexEncoder encoder -> g + java.nio.ByteBuffer recordBuffer -> h + java.net.InetSocketAddress serverAddress -> i + int packetNumber -> j + boolean bigEndian -> k + float gain -> l + float gain2 -> m + com.jotuntech.sketcher.client.voice.VoiceListener listener -> n + com.jotuntech.sketcher.client.voice.Highpass24 highpass -> o + com.jotuntech.sketcher.client.voice.VoiceDetector detector -> p + void run() -> run + void setListener(com.jotuntech.sketcher.client.voice.VoiceListener) -> a +com.jotuntech.sketcher.client.voice.VoiceDetector -> com.jotuntech.sketcher.client.a.d: + float[] delayLine -> a + float alpha -> b + float mean -> c + float process(float) -> a +com.jotuntech.sketcher.client.voice.VoiceEvent -> com.jotuntech.sketcher.client.a.e: + int type -> a + java.lang.Integer channel -> b + int data1 -> c + int getType() -> a + java.lang.Integer getChannel() -> b + int getVolume() -> c +com.jotuntech.sketcher.client.voice.VoiceListener -> com.jotuntech.sketcher.client.a.f: + void voiceEvent(com.jotuntech.sketcher.client.voice.VoiceEvent) -> a +com.jotuntech.sketcher.client.voice.VoiceMixer -> com.jotuntech.sketcher.client.a.g: + javax.sound.sampled.SourceDataLine source -> a + javax.sound.sampled.AudioFormat sourceFormat -> b + int bufferSize -> c + java.util.Map channelMap -> d + java.nio.ByteBuffer mixBuffer -> e + void write(java.lang.Integer,java.nio.ByteBuffer) -> a + int available(java.lang.Integer) -> a + void drop(java.lang.Integer) -> b + boolean mix() -> a + void close() -> b + int getBufferSize() -> c +com.jotuntech.sketcher.common.BitmapLayer -> com.jotuntech.sketcher.common.a: + java.lang.String name -> a + float opacity -> b + int blendMode -> c + java.util.Map tiles -> d + java.awt.image.ImageObserver observer -> e + com.jotuntech.sketcher.common.BitmapTile currentTile -> f + java.awt.Point currentTilePoint -> g + com.jotuntech.sketcher.common.PixelUnpacker unpacker -> h + com.jotuntech.sketcher.common.BitmapTile tempTile -> i + void draw(java.awt.Graphics2D) -> a + void draw(java.awt.Graphics2D,java.util.Set) -> a + float getOpacity() -> a + com.jotuntech.sketcher.common.Input line(com.jotuntech.sketcher.common.Input,com.jotuntech.sketcher.common.Input,int,com.jotuntech.sketcher.common.Brush,com.jotuntech.sketcher.common.Layer) -> a + int getColor(com.jotuntech.sketcher.common.Input) -> a + int getPixel(int,int) -> a + void addImageObserver(java.awt.image.ImageObserver) -> a + java.awt.image.ImageObserver getImageObserver() -> b + java.lang.String getName() -> c + com.jotuntech.sketcher.common.BitmapTile getBitmapTile(int,int,boolean) -> a + void blendPixel(int,int,int,int,int) -> a + java.awt.Rectangle daub(com.jotuntech.sketcher.common.Input,com.jotuntech.sketcher.common.Brush,int,com.jotuntech.sketcher.common.BitmapLayer) -> a + void clean() -> d + void decode(byte[]) -> a + void setOpacity(float) -> a + boolean isEmpty() -> e + java.util.Map getTiles() -> f + com.jotuntech.sketcher.common.UndoData merge(com.jotuntech.sketcher.common.Layer) -> a + void setAlphaRule(int) -> a + void undo(com.jotuntech.sketcher.common.UndoData) -> a + com.jotuntech.sketcher.common.UndoData copyTo(com.jotuntech.sketcher.common.Layer,java.awt.image.ImageObserver,boolean,float,float,float,float,float,float) -> a + void applyFilter(com.jotuntech.sketcher.common.filter.Filter,java.awt.image.ImageObserver,int,int,int,int) -> a +com.jotuntech.sketcher.common.BitmapTile -> com.jotuntech.sketcher.common.b: + java.awt.image.BufferedImage image -> a + int[] pixels -> b + void blendPixel(int,int,int,int,int) -> a + java.awt.image.BufferedImage getImage() -> a + int[] getPixels() -> b +com.jotuntech.sketcher.common.BitmapTileDiff -> com.jotuntech.sketcher.common.c: + short[] diffAlpha -> a + short[] diffRed -> b + short[] diffGreen -> c + short[] diffBlue -> d +com.jotuntech.sketcher.common.BitmapUndoData -> com.jotuntech.sketcher.common.d: + com.jotuntech.sketcher.common.DiffUnpacker unpacker -> a + void decode(byte[]) -> a +com.jotuntech.sketcher.common.Brush -> com.jotuntech.sketcher.common.e: + java.lang.String name -> a + int opacity -> b + int flow -> c + float radius -> d + int hardness -> e + float spacing -> f + boolean pressureToOpacity -> g + boolean pressureToFlow -> h + boolean pressureToRadius -> i + float jitter -> j + int noise -> k + int water -> l + float waterArea -> m + boolean lockTransparency -> n + java.lang.String getName() -> a + void setName(java.lang.String) -> a + com.jotuntech.sketcher.common.Brush copy() -> b + void decode(java.nio.ByteBuffer) -> a + void encode(java.nio.ByteBuffer) -> b + void setOpacity(int) -> a + int getOpacity() -> c + void setFlow(int) -> b + int getFlow() -> d + void setRadius(float) -> a + float getRadius() -> e + void setHardness(int) -> c + int getHardness() -> f + void setSpacing(float) -> b + float getSpacing() -> g + void setPressureToFlow(boolean) -> a + boolean isPressureToFlow() -> h + void setPressureToRadius(boolean) -> b + boolean isPressureToRadius() -> i + void setJitter(float) -> c + float getJitter() -> j + void setNoise(int) -> d + int getNoise() -> k + void setWater(int) -> e + int getWater() -> l + void setWaterArea(float) -> d + float getWaterArea() -> m + void setLockTransparency(boolean) -> c + boolean isLockTransparency() -> n + void setPressureToOpacity(boolean) -> d + boolean isPressureToOpacity() -> o +com.jotuntech.sketcher.common.Canvas -> com.jotuntech.sketcher.common.f: + int width -> a + int height -> b + com.jotuntech.sketcher.common.TwoWayHashMap layerMap -> c + java.util.Set observers -> d + boolean drawing -> e + int getWidth() -> a + int getHeight() -> b + com.jotuntech.sketcher.common.TwoWayHashMap getLayerMap() -> c + void addImageObserver(java.awt.image.ImageObserver) -> a + boolean imageUpdate(java.awt.Image,int,int,int,int,int) -> imageUpdate + void setDrawing(boolean) -> a + boolean isDrawing() -> d + void access$0(com.jotuntech.sketcher.common.Canvas) -> a +com.jotuntech.sketcher.common.Canvas$1 -> com.jotuntech.sketcher.common.g: + com.jotuntech.sketcher.common.Canvas this$0 -> a + void stateChanged(javax.swing.event.ChangeEvent) -> stateChanged +com.jotuntech.sketcher.common.DiffUnpacker -> com.jotuntech.sketcher.common.h: + java.util.zip.Inflater inflater -> a + byte[] encodedDiff -> b + void unpack(byte[],int,int,short[],short[],short[],short[]) -> a +com.jotuntech.sketcher.common.Input -> com.jotuntech.sketcher.common.i: + int pressure -> a + com.jotuntech.sketcher.common.Input difference(com.jotuntech.sketcher.common.Input) -> a + void decode(java.nio.ByteBuffer) -> a + void encode(java.nio.ByteBuffer) -> b + java.lang.Object clone() -> clone +com.jotuntech.sketcher.common.Layer -> com.jotuntech.sketcher.common.j: + java.lang.String getName() -> c + void draw(java.awt.Graphics2D) -> a + com.jotuntech.sketcher.common.UndoData copyTo(com.jotuntech.sketcher.common.Layer,java.awt.image.ImageObserver,boolean,float,float,float,float,float,float) -> a + com.jotuntech.sketcher.common.UndoData merge(com.jotuntech.sketcher.common.Layer) -> a + void undo(com.jotuntech.sketcher.common.UndoData) -> a + void setAlphaRule(int) -> a + void setOpacity(float) -> a + com.jotuntech.sketcher.common.Input line(com.jotuntech.sketcher.common.Input,com.jotuntech.sketcher.common.Input,int,com.jotuntech.sketcher.common.Brush,com.jotuntech.sketcher.common.Layer) -> a + int getColor(com.jotuntech.sketcher.common.Input) -> a + void addImageObserver(java.awt.image.ImageObserver) -> a + java.awt.image.ImageObserver getImageObserver() -> b + void clean() -> d + boolean isEmpty() -> e + void decode(byte[]) -> a +com.jotuntech.sketcher.common.Log -> com.jotuntech.sketcher.common.k: + java.text.DateFormat df -> a + void debug(java.lang.String) -> a + void info(java.lang.String) -> b + void error(java.lang.String) -> c + void error(java.lang.Throwable) -> a + void warn(java.lang.String) -> d +com.jotuntech.sketcher.common.PixelUnpacker -> com.jotuntech.sketcher.common.l: + java.util.zip.Inflater inflater -> a + byte[] encodedPixels -> b + void unpack(byte[],int,int,int[]) -> a +com.jotuntech.sketcher.common.Pixels -> com.jotuntech.sketcher.common.m: + int[] randomNumbers -> a + int randomCounter -> b + int random() -> a + int getChannel1(int) -> a + int getChannel2(int) -> b + int pack(int,int,int) -> a + int pack(int,int,int,int) -> a + float gammaDecode(float) -> a + int gammaEncode(float) -> b +com.jotuntech.sketcher.common.Streamable -> com.jotuntech.sketcher.common.n: + void decode(java.nio.ByteBuffer) -> a + void encode(java.nio.ByteBuffer) -> b +com.jotuntech.sketcher.common.TwoWayHashMap -> com.jotuntech.sketcher.common.o: + java.util.Map keyMap -> a + java.util.Map valueMap -> b + java.util.List changeListeners -> c + java.lang.Object put(java.lang.Object,java.lang.Object) -> put + java.lang.Object remove(java.lang.Object) -> remove + java.lang.Object removeByValue(java.lang.Object) -> a + java.lang.Object get(java.lang.Object) -> get + java.lang.Object getKeyForValue(java.lang.Object) -> b + java.util.Set entrySet() -> entrySet + java.util.Set keySet() -> keySet + java.util.Collection values() -> values + void addChangeListener(javax.swing.event.ChangeListener) -> a + void change() -> a + void clear() -> clear + boolean containsKey(java.lang.Object) -> containsKey + boolean containsValue(java.lang.Object) -> containsValue + boolean isEmpty() -> isEmpty + void putAll(java.util.Map) -> putAll + int size() -> size +com.jotuntech.sketcher.common.UndoData -> com.jotuntech.sketcher.common.p: +com.jotuntech.sketcher.common.UndoEntry -> com.jotuntech.sketcher.common.q: + java.lang.ref.WeakReference layer -> a + com.jotuntech.sketcher.common.UndoData undoData -> b + void setLayer(com.jotuntech.sketcher.common.Layer) -> a + com.jotuntech.sketcher.common.Layer getLayer() -> a + void setUndoData(com.jotuntech.sketcher.common.UndoData) -> a + com.jotuntech.sketcher.common.UndoData getUndoData() -> b +com.jotuntech.sketcher.common.User -> com.jotuntech.sketcher.common.r: + java.lang.String name -> a + boolean viewer -> b + java.lang.ref.WeakReference layer -> c + com.jotuntech.sketcher.common.Layer phantomLayer -> d + com.jotuntech.sketcher.common.Brush brush -> e + int color -> f + com.jotuntech.sketcher.common.Input cursor -> g + java.util.Deque undoDeque -> h + java.awt.Rectangle tag -> i + java.lang.String getName() -> a + void setLayer(com.jotuntech.sketcher.common.Layer) -> a + com.jotuntech.sketcher.common.Layer getLayer() -> b + void setBrush(com.jotuntech.sketcher.common.Brush) -> a + com.jotuntech.sketcher.common.Brush getBrush() -> c + void setColor(int) -> a + int getColor() -> d + void setCursor(com.jotuntech.sketcher.common.Input) -> a + com.jotuntech.sketcher.common.Input getCursor() -> e + void setTag(java.awt.Rectangle) -> a + java.awt.Rectangle getTag() -> f + com.jotuntech.sketcher.common.Layer getPhantomLayer() -> g + java.util.Deque getUndoDeque() -> h + void setViewer(boolean) -> a + boolean isViewer() -> i + int compareTo(java.lang.Object) -> compareTo +com.jotuntech.sketcher.common.filter.AutoContrastFilter -> com.jotuntech.sketcher.common.filter.AutoContrastFilter: + int[] alphaHistogram -> a + int[] colorHistogram -> b + int alphaBlackPoint -> c + int alphaWhitePoint -> d + int colorBlackPoint -> e + int colorWhitePoint -> f + int totalPixels -> g + int alphaRange -> h + int colorRange -> i + float maxBlackFraction -> j + void setSize(int,int) -> a + void setParameterA(float) -> a + boolean isPass1ReadOnly() -> a + boolean isPass2Reversed() -> b + void startPass1() -> c + int processPass1Pixel(int) -> a + void startPass2() -> d + int processPass2Pixel(int) -> b +com.jotuntech.sketcher.common.filter.BlurFilter -> com.jotuntech.sketcher.common.filter.BlurFilter: + float xAlpha -> a + float xRed -> b + float xGreen -> c + float xBlue -> d + float cutoff -> e + float[] yAlpha -> f + float[] yRed -> g + float[] yGreen -> h + float[] yBlue -> i + int width -> j + int x -> k + int y -> l + boolean[] yAlphaWait -> m + void setSize(int,int) -> a + void setParameterA(float) -> a + int processPass1Pixel(int) -> a + int processPass2Pixel(int) -> b + boolean isPass1ReadOnly() -> a + boolean isPass2Reversed() -> b + void startPass1() -> c + void startPass2() -> d +com.jotuntech.sketcher.common.filter.Filter -> com.jotuntech.sketcher.common.filter.a: + void setSize(int,int) -> a + void setParameterA(float) -> a + boolean isPass1ReadOnly() -> a + boolean isPass2Reversed() -> b + void startPass1() -> c + int processPass1Pixel(int) -> a + void startPass2() -> d + int processPass2Pixel(int) -> b diff --git a/sketcher2-client-resources-20120527-1225.jar b/sketcher2-client-resources-20120527-1225.jar new file mode 100755 index 0000000..c972726 Binary files /dev/null and b/sketcher2-client-resources-20120527-1225.jar differ diff --git a/sketcher2-server-20120527-1225.jar b/sketcher2-server-20120527-1225.jar new file mode 100755 index 0000000..d2c4507 Binary files /dev/null and b/sketcher2-server-20120527-1225.jar differ diff --git a/src/com/jotuntech/sketcher/client/Client.java b/src/com/jotuntech/sketcher/client/Client.java new file mode 100755 index 0000000..e54f564 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/Client.java @@ -0,0 +1,626 @@ +package com.jotuntech.sketcher.client; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.util.Properties; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; + +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import com.jotuntech.sketcher.client.command.PingCommand; +import com.jotuntech.sketcher.client.command.SignInCommand; +import com.jotuntech.sketcher.client.command.SignOutCommand; +import com.jotuntech.sketcher.client.voice.VoiceClient; +import com.jotuntech.sketcher.client.voice.VoiceEvent; +import com.jotuntech.sketcher.client.voice.VoiceListener; +import com.jotuntech.sketcher.common.Brush; +import com.jotuntech.sketcher.common.Canvas; +import com.jotuntech.sketcher.common.Layer; +import com.jotuntech.sketcher.common.Log; +import com.jotuntech.sketcher.common.StreamableUtils; +import com.jotuntech.sketcher.common.TwoWayHashMap; +import com.jotuntech.sketcher.common.User; + +/** + * @author Thor Harald Johansen + * + */ + +public class Client extends Thread { + private UserInterface userInterface; + private TwoWayHashMap userMap; + private User[] userArray; + private Canvas canvas; + private long lastLayerClean; + private Connection connection; + private BlockingQueue commandQueue; + private ByteBuffer commandBuffer; + private String login; + private String password; + private String hostname; + private int port; + private VoiceClient voiceClient; + private boolean soundEnabled; + private boolean ads; + + //private Random sendRandom = new Random(0x707733360A596E0AL); + //private Random recvRandom = new Random(0x59A70A401C8CD1EAL); + + private Properties props; + private File propsFile; + Timer propsTimer = new Timer("PropsTimer", true); + + private Brush defaultBrushes[] = new Brush[] { + new Brush("Pen", 255, 255, 3.375f, 1, 0.27f, true, false, true, 0, 0, 0, false, 0.5f), + new Brush("Pencil", 255, 255, 2f, 2, 0.45f, true, false, true, 1.5f, 0, 0, false, 0.5f), + new Brush("Water", 255, 255, 16f, 4, 0.22f, true, false, true, 0, 0, 128, false, 0.5f), + new Brush("Eraser", -255, 192, 16f, 4, 0.2f, false, true, false, 0, 0, 0, false, 0.5f), + new Brush("Ink", 255, 255, 4f, 1, 0.04f, false, false, true, 0, 0, 0, false, 0.5f), + new Brush("Wipe", -255, 255, 64f, 7, 0.2f, false, false, false, 0, 0, 0, false, 0.5f) + }; + + protected Brush[] brushes = new Brush[defaultBrushes.length]; + + public Client(String hostname, int port, String login, String password, boolean ads) { + super("ClientThread"); + + /** Save hostname and port */ + this.hostname = hostname; + this.port = port; + + /** Save login and password */ + this.login = login; + this.password = password; + + /** Save ad setting */ + this.ads = ads; + + /** Create user map & array */ + userMap = new TwoWayHashMap(); + userMap.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent arg0) { + /** Populate user array in a thread-safe way */ + User[] newUserArray = new User[0]; + newUserArray = userMap.values().toArray(newUserArray); + userArray = newUserArray; + } + }); + + /** Create user array */ + userArray = new User[0]; + + /** Create command queue */ + commandQueue = new ArrayBlockingQueue(65536, true); + + for(int i = 0; i < brushes.length; i++) { + brushes[i] = defaultBrushes[i].copy(); + } + + /** Create user interface */ + userInterface = new UserInterface(this); + + /** Load properties file */ + try { + propsFile = new File(System.getProperty("user.home"), "sketcher.properties"); + } catch(SecurityException e) { + propsFile = null; + } + + loadProps(); + + /** Set last layer cleanup time */ + lastLayerClean = System.currentTimeMillis(); + + propsTimer.schedule(new TimerTask() { + public void run() { + saveProps(); + } + }, 60000, 60000); + } + + synchronized public void resetProps() { + if(propsFile != null) { + propsFile.delete(); + } + loadProps(); + } + + synchronized private void loadProps() { + props = new Properties(); + + if(propsFile != null) { + try { + if(propsFile.exists()) { + FileInputStream is = new FileInputStream(propsFile); + props.load(is); + is.close(); + } + } catch(IOException e) { + + } catch(SecurityException e) { + + } + } + + int propsVersion = Integer.valueOf(props.getProperty("file.version", "0")); + + soundEnabled = "true".equals(props.getProperty("sound.enabled", "true")); + userInterface.getSoundItem().setSelected(soundEnabled); + + userInterface.setSmoothZoom("true".equals(props.getProperty("zoom.smooth", "true"))); + + userInterface.getEditor().setTagsEnabled("true".equals(props.getProperty("tags.enabled", "true"))); + userInterface.getEditor().setSoftwareCursorEnabled("true".equals(props.getProperty("cursor.software", "false"))); + + userInterface.getTagsItem().setSelected(userInterface.getEditor().isTagsEnabled()); + + for(JCollapsiblePanel panel : userInterface.getToolPanels()) { + panel.setExpanded("true".equals(props.getProperty("panel" + panel.getId() + ".expanded", panel.isExpandedByDefault() ? "true" : "false")), false); + panel.setStuck("true".equals(props.getProperty("panel" + panel.getId() + ".stuck", panel.isStuckByDefault() ? "true" : "false"))); + } + + for(int i = 0; i < brushes.length; i++) { + brushes[i] = defaultBrushes[i].copy(); + } + + for(int i = 0; i < defaultBrushes.length; i++) { + String name = props.getProperty("brush" + i + ".name"); + String opacity = props.getProperty("brush" + i + ".opacity"); + String flow = props.getProperty("brush" + i + ".flow"); + String radius = props.getProperty("brush" + i + ".radius"); + String hardness = props.getProperty("brush" + i + ".hardness"); + String spacing = props.getProperty("brush" + i + ".spacing"); + String pressureToFlow, pressureToOpacity; + if(propsVersion < 1) { + pressureToFlow = props.getProperty("brush" + i + ".pressureToOpacity"); + pressureToOpacity = props.getProperty("brush" + i + ".realPressureToOpacity"); + } else { + pressureToFlow = props.getProperty("brush" + i + ".pressureToFlow"); + pressureToOpacity = props.getProperty("brush" + i + ".pressureToOpacity"); + } + String pressureToRadius = props.getProperty("brush" + i + ".pressureToRadius"); + String jitter = props.getProperty("brush" + i + ".jitter"); + String water = props.getProperty("brush" + i + ".water"); + String waterArea = props.getProperty("brush" + i + ".waterArea"); + String lockTransparency = props.getProperty("brush" + i + ".lockTransparency"); + + Brush b = brushes[i]; + if(name != null) { + b.setName(name); + } + if(opacity != null) { + b.setOpacity(Integer.valueOf(opacity)); + } + if(flow != null) { + b.setFlow(Integer.valueOf(flow)); + } + if(radius != null) { + b.setRadius(Float.valueOf(radius)); + } + if(hardness != null) { + b.setHardness(Integer.valueOf(hardness)); + } + if(spacing != null) { + b.setSpacing(Float.valueOf(spacing)); + } + if(pressureToOpacity != null) { + b.setPressureToOpacity("true".equals(pressureToOpacity)); + } + if(pressureToFlow != null) { + b.setPressureToFlow("true".equals(pressureToFlow)); + } + if(pressureToRadius != null) { + b.setPressureToRadius("true".equals(pressureToRadius)); + } + if(jitter != null) { + b.setJitter(Float.valueOf(jitter)); + } + if(water != null) { + b.setWater(Integer.valueOf(water)); + } + if(waterArea != null) { + b.setWaterArea(Float.valueOf(waterArea)); + } + if(lockTransparency != null) { + b.setLockTransparency("true".equals(lockTransparency)); + } + } + } + + synchronized private void saveProps() { + if(propsFile == null) { + return; + } + + props.setProperty("file.version", "1"); // Increase this when making format changes! + + props.setProperty("sound.enabled", soundEnabled ? "true" : "false"); + + props.setProperty("zoom.smooth", userInterface.isSmoothZoom() ? "true" : "false"); + + props.setProperty("tags.enabled", userInterface.getEditor().isTagsEnabled() ? "true" : "false"); + props.setProperty("cursor.software", userInterface.getEditor().isSoftwareCursorEnabled() ? "true" : "false"); + + for(JCollapsiblePanel panel : userInterface.getToolPanels()) { + props.setProperty("panel" + panel.getId() + ".expanded", panel.isExpanded() ? "true" : "false"); + props.setProperty("panel" + panel.getId() + ".stuck", panel.isStuck() ? "true" : "false"); + } + + for(int i = 0; i < brushes.length; i++) { + props.setProperty("brush" + i + ".name", brushes[i].getName()); + props.setProperty("brush" + i + ".opacity", String.valueOf(brushes[i].getOpacity())); + props.setProperty("brush" + i + ".flow", String.valueOf(brushes[i].getFlow())); + props.setProperty("brush" + i + ".radius", String.valueOf(brushes[i].getRadius())); + props.setProperty("brush" + i + ".hardness", String.valueOf(brushes[i].getHardness())); + props.setProperty("brush" + i + ".spacing", String.valueOf(brushes[i].getSpacing())); + props.setProperty("brush" + i + ".pressureToOpacity", brushes[i].isPressureToOpacity() ? "true" : "false"); + props.setProperty("brush" + i + ".pressureToFlow", brushes[i].isPressureToFlow() ? "true" : "false"); + props.setProperty("brush" + i + ".pressureToRadius", brushes[i].isPressureToRadius() ? "true" : "false"); + props.setProperty("brush" + i + ".jitter", String.valueOf(brushes[i].getJitter())); + props.setProperty("brush" + i + ".noise", String.valueOf(brushes[i].getNoise())); + props.setProperty("brush" + i + ".water", String.valueOf(brushes[i].getWater())); + props.setProperty("brush" + i + ".waterArea", String.valueOf(brushes[i].getWaterArea())); + props.setProperty("brush" + i + ".lockTransparency", brushes[i].isLockTransparency() ? "true" : "false"); + } + + try { + FileOutputStream os = new FileOutputStream(propsFile); + props.store(os, "Sketcher Properties File"); + os.close(); + } catch(IOException e) { + + } catch(SecurityException e) { + propsTimer.cancel(); + } + } + + public void close(String message) { + propsTimer.cancel(); + saveProps(); + + /** Empty the send queue */ + connection.getSendQueue().clear(); + + /** Send sign-out command */ + connection.getSendQueue().offer(new CommandEntry(0, new SignOutCommand(message))); + + /** Schedule connection death */ + connection.setTimeOfDeath(System.currentTimeMillis() + 1000); + + /** Play sound */ + if("true".equals(props.getProperty("sound.enabled", "true"))) { + UserInterface.AUDIO_OUTTRO.play(); + } + } + + public TwoWayHashMap getUserMap() { + return userMap; + } + + public User[] getUserArray() { + return userArray; + } + + public Canvas getCanvas() { + return canvas; + } + + public void setCanvas(Canvas canvas) { + this.canvas = canvas; + userInterface.setCanvas(canvas); + } + + public UserInterface getUserInterface() { + return userInterface; + } + + public Connection getConnection() { + return connection; + } + + public void run() { + try { + /** Create client connection */ + connection = new Connection(SocketChannel.open()); + + userInterface.println("Connecting... "); + + /** Enable blocking for connect */ + connection.getChannel().configureBlocking(true); + + /** Attempt to connect to server */ + connection.getChannel().connect(new InetSocketAddress(hostname, port)); + + /** Disable blocking after connect */ + connection.getChannel().configureBlocking(false); + + userInterface.print("Success!"); + + /** Play audio */ + if("true".equals(props.getProperty("sound.enabled", "true"))) { + UserInterface.AUDIO_INTRO.play(); + } + + /** Create command buffer */ + commandBuffer = ByteBuffer.allocate(65538); + + /** Send sign-in command */ + connection.getSendQueue().offer(new CommandEntry(0, new SignInCommand(0, login, password, false))); + + /** Set last ping time */ + connection.setLastPing(0); + + while(!Thread.interrupted()) { + if(!connection.getChannel().isConnected()) { + userInterface.println("Disconnected (Connection was closed)"); + break; + } else if(connection.isTimeOfDeath()) { + try { connection.getChannel().close(); } catch(IOException e) { } + userInterface.println("Disconnected (Exit)"); + break; + } + + if(System.currentTimeMillis() - connection.getLastPing() >= Connection.PING_INTERVAL) { + long timestamp = System.currentTimeMillis(); + connection.getSendQueue().offer(new CommandEntry(0, new PingCommand(timestamp))); + connection.setLastPing(timestamp); + } + + for(CommandEntry e = commandQueue.poll(20, TimeUnit.MILLISECONDS); e != null; e = commandQueue.poll(20, TimeUnit.MILLISECONDS)) { + if(e.getCommand().perform(this, e.getSourceKey() == 0 ? connection.getUser() : userMap.get(e.getSourceKey())) == Connection.SEND_OTHERS) { + connection.getSendQueue().offer(new CommandEntry(e.getSourceKey(), e.getCommand())); + } + } + + if(canvas != null && lastLayerClean + 30000 < System.currentTimeMillis()) { + lastLayerClean = System.currentTimeMillis(); + for(Layer e : canvas.getLayerMap().values()) { + e.clean(); + } + } + + ByteBuffer inputBuffer = connection.getInputBuffer(); + ByteBuffer outputBuffer = connection.getOutputBuffer(); + + ArrayBlockingQueue sendQueue = connection.getSendQueue(); + + //int startPosition = outputBuffer.position(); + + /** Only check queue if room for command length, peer key, name length, and one character of name */ + while(outputBuffer.remaining() > 2 + 4 + 1 + 2 && sendQueue.size() > 0) { + CommandEntry e = sendQueue.peek(); + Integer k = e.getSourceKey(); + Command c = e.getCommand(); + + /** Clear command buffer */ + commandBuffer.clear(); + + /** Write peer key */ + commandBuffer.putInt(k); + + /** Write command name */ + String commandName = c.getClass().getSimpleName(); + commandBuffer.put((byte) (commandName.length() - 1)); + for(int i = 0; i < commandName.length(); i++) { + commandBuffer.putChar(commandName.charAt(i)); + } + + /** Write command */ + c.encode(commandBuffer); + + /** Flip command buffer before reading */ + commandBuffer.flip(); + + /** Do not send this command yet if no room in output buffer */ + if(commandBuffer.remaining() + 2 > outputBuffer.remaining()) { + break; + } + + /** Write append command length and command data to output buffer */ + outputBuffer.putShort((short) (commandBuffer.remaining() - 1)); + outputBuffer.put(commandBuffer); + + /** Remove command from queue */ + sendQueue.remove(); + } + + // Primitive encryption + //for(int i = startPosition; i < outputBuffer.position(); i++) { + // outputBuffer.put(i, (byte) (outputBuffer.get(i) ^ sendRandom.nextInt(256))); + //} + + /** Flip output buffer */ + outputBuffer.flip(); + + if(outputBuffer.remaining() > 0) { + connection.getChannel().write(outputBuffer); + } + + /** Compact buffer, preparing it for appending */ + outputBuffer.compact(); + + if(canvas == null || !canvas.isDrawing()) { + try { + /** Append to input buffer */ + //startPosition = inputBuffer.position(); + int readResult = connection.getChannel().read(inputBuffer); + if(readResult == -1) { + try { connection.getChannel().close(); } catch(IOException e) { } + userInterface.println("Disconnected (Connection was closed)"); + break; + } else if(readResult > 0) { + //for(int i = startPosition; i < inputBuffer.position(); i++) { + // inputBuffer.put(i, (byte) (inputBuffer.get(i) ^ recvRandom.nextInt(256))); + //} + + /** Prepare buffer for reading */ + inputBuffer.flip(); + + /** Long enough to peek at length? */ + while((canvas == null || !canvas.isDrawing()) && inputBuffer.remaining() >= 2) { + /** Peek at length */ + int commandLength = (inputBuffer.getShort(inputBuffer.position()) & 0xFFFF) + 1; + + /** Don't read if not long enough to read entire command. */ + if(inputBuffer.remaining() < commandLength + 2) { + break; + } + + /** We already have the command length */ + inputBuffer.getShort(); + + /** Read command buffer */ + ByteBuffer commandBuffer = inputBuffer.slice(); + commandBuffer.limit(commandLength); + inputBuffer.position(inputBuffer.position() + commandLength); + + /** Read user key */ + Integer userKey = commandBuffer.getInt(); + + /** Read command name */ + int commandNameLength = (commandBuffer.get() & 0xFF) + 1; + StringBuffer commandNameBuffer = new StringBuffer(); + for(int i = 0; i < commandNameLength; i++) { + commandNameBuffer.append(commandBuffer.getChar()); + } + String commandName = commandNameBuffer.toString(); + + Command command = (Command) StreamableUtils.create("com.jotuntech.sketcher.client.command." + commandName, commandBuffer); + command.perform(this, userMap.get(userKey)); + + //++iterations; + } + + /** Prepare input buffer for appending */ + inputBuffer.compact(); + } + } catch (IOException e) { + try { connection.getChannel().close(); } catch(IOException e2) { } + userInterface.println("Disconnected (Connection was broken: " + e.getMessage() + ")"); + break; + } catch(Throwable t) { + sendQueue.clear(); + sendQueue.offer(new CommandEntry(0, new SignOutCommand("Client crashed: " + t.getClass().getSimpleName()))); + connection.setTimeOfDeath(System.currentTimeMillis() + 5000); + Log.error(t); + userInterface.println("Disconnected (Client crashed: " + t.getClass().getSimpleName() + ")"); + } + } + } + } catch (InterruptedException e) { + Log.debug("Client interrupted."); + } catch (IOException e) { + userInterface.println("Disconnected (Connection was broken: " + e.getMessage() + ")"); + Log.error(e); + } + + try { connection.getChannel().close(); } catch(IOException e) { } + connection.setChannel(null); + connection.setUser(null); + + if("true".equals(props.getProperty("sound.enabled", "true"))) { + UserInterface.AUDIO_OUTTRO.play(); + } + } + + public BlockingQueue getCommandQueue() { + return commandQueue; + } + + public boolean isVoiceEnabled() { + return voiceClient != null; + } + + public void setVoiceEnabled(boolean voiceEnabled) { + if(voiceEnabled) { + final Integer peerKey = getUserMap().getKeyForValue(getConnection().getUser()); + voiceClient = new VoiceClient(new InetSocketAddress(hostname, port + 100), peerKey); + voiceClient.setListener(new VoiceListener() { + public void voiceEvent(VoiceEvent e) { + switch(e.getType()) { + case VoiceEvent.TYPE_CHANNEL_NEW: + Integer userKey = e.getChannel(); + User user = userMap.get(userKey); + if(user == null) { + break; + } + break; + + case VoiceEvent.TYPE_CHANNEL_DEAD: { + JUserEntry ue = userInterface.getUserList().getEntry(e.getChannel()); + if(ue == null) { + break; + } + ue.clearVoice(); + break; + } + + case VoiceEvent.TYPE_PACKET_VOLUME: { + JUserEntry ue = userInterface.getUserList().getEntry(e.getChannel()); + if(ue == null) { + break; + } + if(e.getChannel() == peerKey) { + ue.setVolume(e.getVolume(), 0); + } else { + ue.setVolume(e.getVolume(), 12); + } + break; + } + } + } + }); + voiceClient.start(); + } else { + voiceClient.interrupt(); + try { voiceClient.join(); } catch (InterruptedException e) { } + voiceClient = null; + userInterface.getUserList().clearVoice(); + } + } + public VoiceClient getVoiceClient() { + return voiceClient; + } + + public Properties getProps() { + return props; + } + + public void setSoundEnabled(boolean soundEnabled) { + this.soundEnabled = soundEnabled; + } + + public boolean isSoundEnabled() { + return soundEnabled; + } + + public void setAds(boolean ads) { + this.ads = ads; + } + + public boolean isAds() { + return ads; + } + + public String getLogin() { + return login; + } + + public String getPassword() { + return password; + } + + public String getHostname() { + return hostname; + } +} \ No newline at end of file diff --git a/src/com/jotuntech/sketcher/client/ClientApplet.java b/src/com/jotuntech/sketcher/client/ClientApplet.java new file mode 100755 index 0000000..450da2f --- /dev/null +++ b/src/com/jotuntech/sketcher/client/ClientApplet.java @@ -0,0 +1,54 @@ +package com.jotuntech.sketcher.client; + +import java.awt.HeadlessException; + +import javax.swing.JApplet; +import javax.swing.JOptionPane; + +import com.jotuntech.sketcher.common.Log; + +public class ClientApplet extends JApplet { + private Client client; + String login, password, hostname; + int port; + boolean ads; + + public ClientApplet() throws HeadlessException { + super(); + } + + public void init() { + login = getParameter("nick"); + password = getParameter("secret"); + hostname = getParameter("server"); + port = Integer.valueOf(getParameter("port")); + ads = "1".equals(getParameter("ads")); + + Log.info("Sketcher applet is initializing."); + } + + public void start() { + System.gc(); + Log.info("Sketcher applet is starting."); + if(Runtime.getRuntime().totalMemory() < 92274688) { + JOptionPane.showMessageDialog(this, "There is too little Java heap memory space for Sketcher to function.\nYour platform likely requires manual configuration. Please pass the\nfollowing run-time parameters to your Java Virtual Machine:\n\n -ms96M -mx384M\n\nContact the website administrator for further assistance."); + } + client = new Client(hostname, port, login, password, ads); + getContentPane().removeAll(); + getContentPane().add(client.getUserInterface()); + client.start(); + } + + public void stop() { + Log.info("Sketcher applet is stopping."); + client.close("Exit"); + System.gc(); + } + + public void destroy() { + Log.info("Sketcher applet is being destroyed."); + getContentPane().removeAll(); + client = null; + } +} + diff --git a/src/com/jotuntech/sketcher/client/Command.java b/src/com/jotuntech/sketcher/client/Command.java new file mode 100755 index 0000000..ac5cb26 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/Command.java @@ -0,0 +1,8 @@ +package com.jotuntech.sketcher.client; + +import com.jotuntech.sketcher.common.Streamable; +import com.jotuntech.sketcher.common.User; + +public interface Command extends Streamable { + public int perform(Client client, User user); +} diff --git a/src/com/jotuntech/sketcher/client/CommandEntry.java b/src/com/jotuntech/sketcher/client/CommandEntry.java new file mode 100755 index 0000000..46547af --- /dev/null +++ b/src/com/jotuntech/sketcher/client/CommandEntry.java @@ -0,0 +1,24 @@ +package com.jotuntech.sketcher.client; + +public class CommandEntry { + private Integer sourceKey; + private Command command; + + public CommandEntry(Integer sourceKey, Command command) { + if(sourceKey == null) { + throw new NullPointerException("Source key can't be null."); + } else if(command == null) { + throw new NullPointerException("Command can't be null."); + } + this.sourceKey = sourceKey; + this.command = command; + } + + public Integer getSourceKey() { + return sourceKey; + } + + public Command getCommand() { + return command; + } +} diff --git a/src/com/jotuntech/sketcher/client/Connection.java b/src/com/jotuntech/sketcher/client/Connection.java new file mode 100755 index 0000000..a742b6b --- /dev/null +++ b/src/com/jotuntech/sketcher/client/Connection.java @@ -0,0 +1,113 @@ +/** + * + */ +package com.jotuntech.sketcher.client; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.util.concurrent.ArrayBlockingQueue; + +import com.jotuntech.sketcher.common.User; + +/** + * Client connection + * + * @author Thor Harald Johansen + */ +public class Connection { + /** Return values for Command */ + public final static int SEND_NONE = 0; /** Send to nobody */ + public final static int SEND_SELF = 1; /** Send to self only */ + public final static int SEND_OTHERS = 2; /** Send to others only */ + public final static int SEND_ALL = 3; /** Send to self and others */ + + public final static long PING_INTERVAL = 10000; + + /** Socket channel */ + private SocketChannel channel; + + /** User to which connection belongs. */ + private User user; + + /** Input buffer */ + private ByteBuffer inputBuffer; + + /** Output buffer */ + private ByteBuffer outputBuffer; + + /** Send queue */ + private ArrayBlockingQueue sendQueue = new ArrayBlockingQueue(98304); + + /** Time of death */ + private long timeOfDeath; + + /** Last ping */ + private long lastPing; + + public Connection() { + + } + + public Connection(SocketChannel channel) throws IOException { + /** Store socket channel. */ + this.channel = channel; + + /** Allocate transmission buffers */ + inputBuffer = ByteBuffer.allocate(65538); + outputBuffer = ByteBuffer.allocate(65538); + + /** Disable time of death */ + timeOfDeath = Long.MAX_VALUE; + } + + /** Set user for connection */ + public void setUser(User user) { + this.user = user; + } + + /** Get user for connection */ + public User getUser() { + return user; + } + + public void setChannel(SocketChannel channel) { + this.channel = channel; + } + + public SocketChannel getChannel() { + return channel; + } + + public ByteBuffer getInputBuffer() { + return inputBuffer; + } + + public ByteBuffer getOutputBuffer() { + return outputBuffer; + } + + public ArrayBlockingQueue getSendQueue() { + return sendQueue; + } + + public void setTimeOfDeath(long timeOfDeath) { + this.timeOfDeath = timeOfDeath; + } + + public boolean hasTimeOfDeath() { + return timeOfDeath != Long.MAX_VALUE; + } + + public boolean isTimeOfDeath() { + return timeOfDeath != Long.MAX_VALUE && System.currentTimeMillis() >= timeOfDeath; + } + + public void setLastPing(long lastPing) { + this.lastPing = lastPing; + } + + public long getLastPing() { + return lastPing; + } +} \ No newline at end of file diff --git a/src/com/jotuntech/sketcher/client/Editor.java b/src/com/jotuntech/sketcher/client/Editor.java new file mode 100755 index 0000000..0bd4f16 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/Editor.java @@ -0,0 +1,1278 @@ +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> selectedLayers = new IdentityHashMap>(); + for(User u : client.getUserMap().values()) { + Layer l = u.getLayer(); + + if(l == null) { + continue; + } + + Set us = selectedLayers.get(l); + + if(us == null) { + us = new HashSet(); + selectedLayers.put(l, us); + } + + us.add(u.getPhantomLayer()); + } + + for(Layer l : client.getCanvas().getLayerMap().values()) { + Set 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 inputList = smoother.get(); + for(Iterator 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; + } +} diff --git a/src/com/jotuntech/sketcher/client/JCollapsiblePanel.java b/src/com/jotuntech/sketcher/client/JCollapsiblePanel.java new file mode 100755 index 0000000..306c84c --- /dev/null +++ b/src/com/jotuntech/sketcher/client/JCollapsiblePanel.java @@ -0,0 +1,154 @@ +package com.jotuntech.sketcher.client; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; + +import javax.swing.Icon; +import javax.swing.ImageIcon; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.UIManager; +import javax.swing.border.EmptyBorder; + +public class JCollapsiblePanel extends JPanel { + private final static Icon expandedIcon = UIManager.getIcon("Tree.expandedIcon"); + private final static Icon collapsedIcon = UIManager.getIcon("Tree.collapsedIcon"); + private final static Icon pinNormalIcon = new ImageIcon(JCollapsiblePanel.class.getResource("images/pin_normal.png")); + private final static Icon pinStuckIcon = new ImageIcon(JCollapsiblePanel.class.getResource("images/pin_stuck.png")); + private JLabel label; + private JLabel pin; + private JPanel contentPane; + private JCollapsiblePanelGroup group; + private boolean stuck; + private int id; + private boolean expandedByDefault, stuckByDefault; + + public JCollapsiblePanel(int id, String title, boolean expanded, boolean stuckx) { + super(); + + this.id = id; + + this.stuck = stuckx; + this.stuckByDefault = stuckx; + this.expandedByDefault = expanded; + + setBorder(new EmptyBorder(0, 0, 1, 0)); + setAlignmentX(Component.LEFT_ALIGNMENT); + setLayout(new BorderLayout()); + + JPanel topPanel = new JPanel(new BorderLayout()); + topPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + add(topPanel, BorderLayout.NORTH); + + label = new JLabel(); + label.setIcon(expanded ? expandedIcon : collapsedIcon); + label.setText(title); + label.addMouseListener(new MouseListener() { + public void mouseClicked(MouseEvent arg0) { + } + + public void mouseEntered(MouseEvent arg0) { + } + + public void mouseExited(MouseEvent arg0) { + } + + public void mousePressed(MouseEvent arg0) { + } + + public void mouseReleased(MouseEvent arg0) { + if(!stuck) { + setExpanded(!contentPane.isVisible(), true); + } + } + }); + label.setAlignmentX(Component.LEFT_ALIGNMENT); + label.setBackground(new Color(172, 200, 230)); + label.setOpaque(true); + topPanel.add(label, BorderLayout.CENTER); + + pin = new JLabel(); + pin.setIcon(stuck ? pinStuckIcon : pinNormalIcon); + pin.addMouseListener(new MouseListener() { + public void mouseClicked(MouseEvent e) { + } + + public void mouseEntered(MouseEvent e) { + } + + public void mouseExited(MouseEvent e) { + } + + public void mousePressed(MouseEvent e) { + } + + public void mouseReleased(MouseEvent e) { + setStuck(!isStuck()); + } + }); + + pin.setAlignmentX(Component.LEFT_ALIGNMENT); + pin.setBackground(new Color(172, 200, 230)); + pin.setOpaque(true); + topPanel.add(pin, BorderLayout.EAST); + + contentPane = new JPanel(); + contentPane.setVisible(expanded); + contentPane.setBorder(new EmptyBorder(4, 4, 4, 4)); + add(contentPane, BorderLayout.CENTER); + } + + public JPanel getContentPane() { + return contentPane; + } + + public void setExpanded(boolean expanded, boolean interactive) { + if(interactive && expanded && group != null) { + for(JCollapsiblePanel panel : group) { + if(panel == this || panel.isStuck()) { + continue; + } + panel.setExpanded(false, false); + } + } + contentPane.setVisible(expanded); + label.setIcon(expanded ? expandedIcon : collapsedIcon); + } + + public boolean isExpanded() { + return contentPane.isVisible(); + } + + public void setGroup(JCollapsiblePanelGroup group) { + this.group = group; + group.add(this); + } + + public void setStuck(boolean stuck) { + this.stuck = stuck; + pin.setIcon(stuck ? pinStuckIcon : pinNormalIcon); + } + + public boolean isStuck() { + return stuck; + } + + public void setId(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public boolean isExpandedByDefault() { + return expandedByDefault; + } + + public boolean isStuckByDefault() { + return stuckByDefault; + } +} diff --git a/src/com/jotuntech/sketcher/client/JCollapsiblePanelGroup.java b/src/com/jotuntech/sketcher/client/JCollapsiblePanelGroup.java new file mode 100755 index 0000000..c20668f --- /dev/null +++ b/src/com/jotuntech/sketcher/client/JCollapsiblePanelGroup.java @@ -0,0 +1,7 @@ +package com.jotuntech.sketcher.client; + +import java.util.HashSet; + +public class JCollapsiblePanelGroup extends HashSet { + +} diff --git a/src/com/jotuntech/sketcher/client/JUserEntry.java b/src/com/jotuntech/sketcher/client/JUserEntry.java new file mode 100755 index 0000000..d33ddbe --- /dev/null +++ b/src/com/jotuntech/sketcher/client/JUserEntry.java @@ -0,0 +1,74 @@ +package com.jotuntech.sketcher.client; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.util.Arrays; + +import javax.swing.BoxLayout; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JProgressBar; + +public class JUserEntry extends JPanel { + private JLabel nameLabel; + private JPanel infoPanel; + private JProgressBar volumeBar; + private double slowVolume; + private int[] delayLine; + + public JUserEntry(String name) { + setLayout(new BorderLayout()); + + setOpaque(false); + + nameLabel = new JLabel(name); + add(nameLabel, BorderLayout.CENTER); + + infoPanel = new JPanel(); + infoPanel.setOpaque(false); + infoPanel.setLayout(new BoxLayout(infoPanel, BoxLayout.X_AXIS)); + add(infoPanel, BorderLayout.EAST); + + volumeBar = new JProgressBar(0, 32768); + volumeBar.setPreferredSize(new Dimension(64, 16)); + volumeBar.setOpaque(false); + volumeBar.setValue(32768); + volumeBar.setEnabled(false); + volumeBar.setToolTipText(name + " is not on voice."); + infoPanel.add(volumeBar); + } + + public void setVolume(int volume) { + setVolume(volume, 0); + } + + public void setVolume(int volume, int delay) { + if(volume <= 0) { + volume = 1; + } + if(delayLine == null || delayLine.length <= delay) { + delayLine = new int[delay + 1]; + Arrays.fill(delayLine, 1); + } + delayLine[delay] = volume; + double newVolume = Math.log(delayLine[0]) * 32768d / Math.log(32768); + + for(int i = 0; i < delayLine.length - 1; i++) { + delayLine[i] = delayLine[i + 1]; + } + delayLine[delayLine.length - 1] = 1; + + slowVolume += (newVolume - slowVolume) * 0.333d; + volumeBar.setValue((int) Math.round(slowVolume)); + if(!volumeBar.isEnabled()) { + volumeBar.setEnabled(true); + volumeBar.setToolTipText(nameLabel.getText() + " is on voice."); + } + } + + public void clearVoice() { + volumeBar.setValue(32768); + volumeBar.setEnabled(false); + volumeBar.setToolTipText(nameLabel.getText() + " is not on voice."); + } +} diff --git a/src/com/jotuntech/sketcher/client/JUserList.java b/src/com/jotuntech/sketcher/client/JUserList.java new file mode 100755 index 0000000..984a036 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/JUserList.java @@ -0,0 +1,57 @@ +package com.jotuntech.sketcher.client; + +import java.awt.Color; +import java.util.HashMap; +import java.util.Map; + +import javax.swing.BoxLayout; +import javax.swing.JPanel; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import com.jotuntech.sketcher.common.TwoWayHashMap; +import com.jotuntech.sketcher.common.User; + +public class JUserList extends JPanel { + TwoWayHashMap userMap; + Map entryMap; + + public JUserList(TwoWayHashMap userMap) { + setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + + this.setBackground(Color.WHITE); + this.setOpaque(true); + + entryMap = new HashMap(); + + this.userMap = userMap; + userMap.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent e) { + redo(); + } + }); + + redo(); + } + + private void redo() { + entryMap.clear(); + removeAll(); + for(Map.Entry e : userMap.entrySet()) { + JUserEntry ue = new JUserEntry(e.getValue().getName()); + add(ue); + entryMap.put(e.getKey(), ue); + } + this.revalidate(); + } + + public JUserEntry getEntry(Integer key) { + return entryMap.get(key); + } + + public void clearVoice() { + for(JUserEntry ue : entryMap.values()) { + ue.clearVoice(); + } + } +} diff --git a/src/com/jotuntech/sketcher/client/LookAndFeel.java b/src/com/jotuntech/sketcher/client/LookAndFeel.java new file mode 100755 index 0000000..aede630 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/LookAndFeel.java @@ -0,0 +1,81 @@ +package com.jotuntech.sketcher.client; + +import javax.swing.UIManager; +import javax.swing.UIManager.LookAndFeelInfo; + +/** + * An collection of built-in Swing look and feels. This saves clients the + * trouble of manually setting the look and feel. + * + * @author Aaron Faanes + * + * @see javax.swing.LookAndFeel + */ +public enum LookAndFeel { + + /** + * Represents the Nimbus look-and-feel that was added in Java 6 Update + * 10. + */ + NIMBUS() { + + private volatile String name; + + @Override + public String getName() { + if (this.name == null) { + for (final LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) { + if (info.getClassName().contains("nimbus")) { + this.name = info.getClassName(); + } + } + } + return this.name; + } + + }, + + /** + * Represents the look-and-feel that emulates the system + * + * @see UIManager#getSystemLookAndFeelClassName() + */ + SYSTEM() { + + @Override + public String getName() { + return UIManager.getSystemLookAndFeelClassName(); + } + }; + + /** + * Returns the name of the class that implements this look-and-feel. The + * returned name, if non-null, can be directly used in a + * {@link UIManager#setLookAndFeel(String)} invocation. If the returned + * value is null, then no class name could be found. + * + * + * @return the name of the class that implements this look-and-feel, or + * {@code null} if no class name could be found + */ + public abstract String getName(); + + /** + * Sets the global look and feel to this look and feel. + * + * @return {@code true} if setting the look-and-feel was successful, + * otherwise {@code false} + */ + public boolean activate() { + final String name = this.getName(); + if (name == null) { + return false; + } + try { + UIManager.setLookAndFeel(name); + return true; + } catch (final Exception e) { + return false; + } + } +} diff --git a/src/com/jotuntech/sketcher/client/PSDEncoder.java b/src/com/jotuntech/sketcher/client/PSDEncoder.java new file mode 100755 index 0000000..e4bc8be --- /dev/null +++ b/src/com/jotuntech/sketcher/client/PSDEncoder.java @@ -0,0 +1,568 @@ +package com.jotuntech.sketcher.client; + +import java.awt.Point; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.io.DataOutput; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.Arrays; +import java.util.Map; + +import com.jotuntech.sketcher.common.BitmapLayer; +import com.jotuntech.sketcher.common.BitmapTile; +import com.jotuntech.sketcher.common.Layer; +import com.jotuntech.sketcher.common.TwoWayHashMap; + +public class PSDEncoder { + private RandomAccessFile raf; + private BufferedImage image; + private BitmapLayer[] layers; + private long[][] channelLengthPositions; + + public PSDEncoder(RandomAccessFile raf, BufferedImage image, TwoWayHashMap layers) throws IOException { + this.raf = raf; + this.image = image; + + int layersLength = 0; + for(Layer l : layers.values()) { + if(l instanceof BitmapLayer) { + ++layersLength; + } + } + + this.layers = new BitmapLayer[layersLength]; + int layerIndex = 0; + for(Layer l : layers.values()) { + if(l instanceof BitmapLayer) { + this.layers[layerIndex++] = (BitmapLayer) l; + } + } + + channelLengthPositions = new long[this.layers.length][4]; + } + + + public void encode() throws IOException { + /* Signature */ + raf.write(new byte[] {'8', 'B', 'P', 'S'}); + + /* Version */ + raf.writeShort(1); + + /* Reserved */ + raf.write(new byte[] {0, 0, 0, 0, 0, 0}); + + /* Number of channels */ + raf.writeShort(4); + + /* Image height */ + raf.writeInt(image.getHeight()); + + /* Image width */ + raf.writeInt(image.getWidth()); + + /* Bits per channel */ + raf.writeShort(8); + + /* Color mode: RGB color */ + raf.writeShort(3); + + /* Length of color mode data section */ + raf.writeInt(0); + + /* Length of image resources section */ + raf.writeInt(0); + + /* Layer and mask information */ + writeLayerAndMaskInfo(raf); + + /* Image data */ + writeImagePixelData(raf); + } + + private void writeLayerAndMaskInfo(RandomAccessFile raf) throws IOException { + long lengthPointer = raf.getFilePointer(); + raf.writeInt(0); + + /* Layer info */ + writeLayerInfo(raf); + + /* Mask info */ + writeMaskInfo(raf); + + /* Go back and write length */ + long endPointer = raf.getFilePointer(); + raf.seek(lengthPointer); + raf.writeInt((int)(endPointer - lengthPointer - 4)); + raf.seek(endPointer); + } + + + private void writeLayerInfo(RandomAccessFile odo) throws IOException { + System.err.println("Writing layer info."); + + long lengthPointer = raf.getFilePointer(); + odo.writeInt(0); + + /* Layers struct */ + writeLayersStruct(odo); + + /* Pixel data */ + writeAllLayersPixelData(odo); + + /* Go back and write length */ + long endPointer = raf.getFilePointer(); + raf.seek(lengthPointer); + raf.writeInt((int)(endPointer - lengthPointer - 4)); + raf.seek(endPointer); + } + + private void writeLayersStruct(RandomAccessFile odo) throws IOException { + System.err.println("Writing layers struct."); + + /* Layer count */ + odo.writeShort(layers.length); + + /* Layers */ + for(int layerIndex = 0; layerIndex < layers.length; layerIndex++) { + writeLayer(odo, layerIndex); + } + } + + private void writeLayer(RandomAccessFile odo, int layerIndex) throws IOException { + /* Get layer */ + BitmapLayer l = layers[layerIndex]; + + System.err.println("Writing layer: " + l.getName()); + + /* Layer top */ + odo.writeInt(0); + + /* Layer left */ + odo.writeInt(0); + + /* Layer bottom */ + odo.writeInt(image.getHeight()); + + /* Layer right */ + odo.writeInt(image.getWidth()); + + /* Number of channels */ + odo.writeShort(4); + + /* Channel length info */ + writeChannelLengthInfo(odo, layerIndex); + + /* Blend mode signature */ + odo.write(new byte[] {'8', 'B', 'I', 'M'}); + + /* Blend mode key */ + odo.write(new byte[] {'n', 'o', 'r', 'm'}); + + /* Opacity */ + odo.writeByte(Math.round(l.getOpacity() * 255f)); + + /* Clipping: Base */ + odo.writeByte(0); + + /* Flags: Transparency not protected and visible */ + odo.writeByte(0x80); + + /* Filler */ + odo.writeByte(0); + + /* Prepare layer name */ + String name = l.getName(); + int origLength = name.length(); + int moduloLength = (origLength + 1) % 4; + int padLength = 0; + if(moduloLength > 0) { + padLength = 4 - moduloLength; + for(int i = 0; i < padLength; i++) { + name += "\000"; + } + } + + /* Extra data size */ + odo.writeInt(4 + 44 + 1 + origLength + padLength); + + /* Layer mask data size */ + odo.writeInt(0); + + /* Layer blending ranges */ + writeLayerBlendingRanges(odo); + + odo.writeByte(origLength); + odo.writeBytes(name); + } + + private void writeChannelLengthInfo(RandomAccessFile raf, int layerIndex) throws IOException { + System.err.println("Writing channel length info."); + + /* Channel ID: Transparency Mask */ + raf.writeShort(-1); + + /* Channel length */ + channelLengthPositions[layerIndex][0] = raf.getFilePointer(); + raf.writeInt(0); + + /* Channel ID: Red */ + raf.writeShort(0); + + /* Channel length */ + channelLengthPositions[layerIndex][1] = raf.getFilePointer(); + raf.writeInt(0); + + /* Channel ID: Green */ + raf.writeShort(1); + + /* Channel length */ + channelLengthPositions[layerIndex][2] = raf.getFilePointer(); + raf.writeInt(0); + + /* Channel ID: Blue */ + raf.writeShort(2); + + /* Channel length */ + channelLengthPositions[layerIndex][3] = raf.getFilePointer(); + raf.writeInt(0); + } + + private void writeLayerBlendingRanges(DataOutput odo) throws IOException { + System.err.println("Writing layer blending ranges."); + + /* Length */ + odo.writeInt(40); + + /* Composite gray blend source */ + odo.writeInt(0x0000FFFF); + + /* Composite gray blend destination */ + odo.writeInt(0x0000FFFF); + + /* First channel source range */ + odo.writeInt(0x0000FFFF); + + /* First channel destination range */ + odo.writeInt(0x0000FFFF); + + /* Second channel source range */ + odo.writeInt(0x0000FFFF); + + /* Second channel destination range */ + odo.writeInt(0x0000FFFF); + + /* Third channel source range */ + odo.writeInt(0x0000FFFF); + + /* Third channel destination range */ + odo.writeInt(0x0000FFFF); + + /* Fourth channel source range */ + odo.writeInt(0x0000FFFF); + + /* Fourth channel destination range */ + odo.writeInt(0x0000FFFF); + } + + private void writeAllLayersPixelData(RandomAccessFile odo) throws IOException { + System.out.println("Writing pixel layer data."); + for(int layerIndex = 0; layerIndex < layers.length; layerIndex++) { + writeLayerPixelData(odo, layerIndex); + } + } + + private void writeMaskInfo(RandomAccessFile raf) throws IOException { + System.err.println("Writing mask info."); + + /* Mask info length */ + long lengthPointer = raf.getFilePointer(); + raf.writeInt(0); + + /* Overlay color space */ + raf.writeShort(0); + + /* Color components */ + raf.writeShort(0); + raf.writeShort(0); + raf.writeShort(0); + raf.writeShort(0); + + /* Opacity */ + raf.writeShort(0); + + /* Kind: Use value stored per layer */ + raf.writeByte(128); + + /* Filler */ + raf.writeByte(0); + + /* Go back and write length */ + long endPointer = raf.getFilePointer(); + raf.seek(lengthPointer); + raf.writeInt((int)(endPointer - lengthPointer - 4)); + raf.seek(endPointer); + } + + public void writeImagePixelData(RandomAccessFile odo) throws IOException { + System.out.println("Writing image data."); + + int[] pixels = ((DataBufferInt)image.getRaster().getDataBuffer()).getData(); + byte[] scanline = new byte[image.getWidth()]; + + /* Compression: RLE */ + odo.writeShort(1); + + /* Placeholder scanline bytecounts */ + long redCountsPosition = raf.getFilePointer(); + for(int i = 0; i < image.getHeight(); i++) { + odo.writeShort(0); + } + long greenCountsPosition = raf.getFilePointer(); + for(int i = 0; i < image.getHeight(); i++) { + odo.writeShort(0); + } + long blueCountsPosition = raf.getFilePointer(); + for(int i = 0; i < image.getHeight(); i++) { + odo.writeShort(0); + } + long alphaCountsPosition = raf.getFilePointer(); + for(int i = 0; i < image.getHeight(); i++) { + odo.writeShort(0); + } + + PackBitsOutputStream pbos = new PackBitsOutputStream(raf); + + /* Image data - Red */ + for(int y = 0; y < image.getHeight(); y++) { + for(int x = 0; x < scanline.length; x++) { + scanline[x] = (byte) ((pixels[y * scanline.length + x] >> 16) & 0xFF); + } + long scanlineStart = raf.getFilePointer(); + pbos.write(scanline); + pbos.flush(); + long scanlineStop = raf.getFilePointer(); + raf.seek(redCountsPosition + y * 2); + raf.writeShort((short)(scanlineStop - scanlineStart)); + raf.seek(scanlineStop); + } + + /* Image data - Green */ + for(int y = 0; y < image.getHeight(); y++) { + for(int x = 0; x < scanline.length; x++) { + scanline[x] = (byte) ((pixels[y * scanline.length + x] >> 8) & 0xFF); + } + long scanlineStart = raf.getFilePointer(); + pbos.write(scanline); + pbos.flush(); + long scanlineStop = raf.getFilePointer(); + raf.seek(greenCountsPosition + y * 2); + raf.writeShort((short)(scanlineStop - scanlineStart)); + raf.seek(scanlineStop); + } + + /* Image data - Blue */ + for(int y = 0; y < image.getHeight(); y++) { + for(int x = 0; x < scanline.length; x++) { + scanline[x] = (byte) (pixels[y * scanline.length + x] & 0xFF); + } + long scanlineStart = raf.getFilePointer(); + pbos.write(scanline); + pbos.flush(); + long scanlineStop = raf.getFilePointer(); + raf.seek(blueCountsPosition + y * 2); + raf.writeShort((short)(scanlineStop - scanlineStart)); + raf.seek(scanlineStop); + } + + /* Image data - Alpha */ + for(int y = 0; y < image.getHeight(); y++) { + for(int x = 0; x < scanline.length; x++) { + scanline[x] = (byte) ((pixels[y * scanline.length + x] >> 24) & 0xFF); + } + long scanlineStart = raf.getFilePointer(); + pbos.write(scanline); + pbos.flush(); + long scanlineStop = raf.getFilePointer(); + raf.seek(alphaCountsPosition + y * 2); + raf.writeShort((short)(scanlineStop - scanlineStart)); + raf.seek(scanlineStop); + } + } + + public void writeLayerPixelData(RandomAccessFile raf, int layerIndex) throws IOException { + System.out.println("Writing image data."); + + byte[] scanline = new byte[image.getWidth()]; + + /* Get tiles */ + Map tiles = layers[layerIndex].getTiles(); + + PackBitsOutputStream pbos = new PackBitsOutputStream(raf); + + long startOfAlpha = raf.getFilePointer(); + int alphaLength; + + /* Compression: RLE */ + raf.writeShort(1); + + /* Alpha scanline counts */ + long alphaCountsPosition = raf.getFilePointer(); + for(int y = 0; y < image.getHeight(); y++) { + raf.writeShort(0); + } + + /* Alpha image data */ + for(int y = 0; y < image.getHeight(); y++) { + int yModulus = y & BitmapTile.SIZE_MASK; + for(int x = 0; x < scanline.length; x += BitmapTile.SIZE) { + BitmapTile t = tiles.get(new Point(x >> BitmapTile.SIZE_2, y >> BitmapTile.SIZE_2)); + if(t == null) { + Arrays.fill(scanline, x, x + BitmapTile.SIZE, (byte) 0); + } else { + int[] tilePixels = t.getPixels(); + for(int offset = 0; offset < BitmapTile.SIZE; offset++) { + scanline[x + offset] = (byte) ((tilePixels[(yModulus << BitmapTile.SIZE_2) + offset] >> 24) & 0xFF); + } + } + } + long scanlineStart = raf.getFilePointer(); + pbos.write(scanline); + pbos.flush(); + long scanlineEnd = raf.getFilePointer(); + raf.seek(alphaCountsPosition + y * 2); + raf.writeShort((short) (scanlineEnd - scanlineStart)); + raf.seek(scanlineEnd); + } + + long endOfAlpha = raf.getFilePointer(); + alphaLength = (int) (endOfAlpha - startOfAlpha); + raf.seek(channelLengthPositions[layerIndex][0]); + raf.writeInt(alphaLength); + raf.seek(endOfAlpha); + + long startOfRed = raf.getFilePointer(); + int redLength; + + /* Compression: RLE */ + raf.writeShort(1); + + /* Red scanline counts */ + long redCountsPosition = raf.getFilePointer(); + for(int y = 0; y < image.getHeight(); y++) { + raf.writeShort(0); + } + + /* Red image data */ + for(int y = 0; y < image.getHeight(); y++) { + int yModulus = y & BitmapTile.SIZE_MASK; + for(int x = 0; x < scanline.length; x += BitmapTile.SIZE) { + BitmapTile t = tiles.get(new Point(x >> BitmapTile.SIZE_2, y >> BitmapTile.SIZE_2)); + if(t == null) { + Arrays.fill(scanline, x, x + BitmapTile.SIZE, (byte) 0); + } else { + for(int offset = 0; offset < BitmapTile.SIZE; offset++) { + int[] tilePixels = t.getPixels(); + scanline[x + offset] = (byte) ((tilePixels[(yModulus << BitmapTile.SIZE_2) + offset] >> 16) & 0xFF); + } + } + } + long scanlineStart = raf.getFilePointer(); + pbos.write(scanline); + pbos.flush(); + long scanlineEnd = raf.getFilePointer(); + raf.seek(redCountsPosition + y * 2); + raf.writeShort((short) (scanlineEnd - scanlineStart)); + raf.seek(scanlineEnd); + } + + long endOfRed = raf.getFilePointer(); + redLength = (int) (endOfRed - startOfRed); + raf.seek(channelLengthPositions[layerIndex][1]); + raf.writeInt(redLength); + raf.seek(endOfRed); + + long startOfGreen = raf.getFilePointer(); + int greenLength; + + /* Compression: RLE */ + raf.writeShort(1); + + /* Green scanline counts */ + long greenCountsPosition = raf.getFilePointer(); + for(int y = 0; y < image.getHeight(); y++) { + raf.writeShort(0); + } + + /* Green image data */ + for(int y = 0; y < image.getHeight(); y++) { + int yModulus = y & BitmapTile.SIZE_MASK; + for(int x = 0; x < scanline.length; x += BitmapTile.SIZE) { + BitmapTile t = tiles.get(new Point(x >> BitmapTile.SIZE_2, y >> BitmapTile.SIZE_2)); + if(t == null) { + Arrays.fill(scanline, x, x + BitmapTile.SIZE, (byte) 0); + } else { + for(int offset = 0; offset < BitmapTile.SIZE; offset++) { + int[] tilePixels = t.getPixels(); + scanline[x + offset] = (byte) ((tilePixels[(yModulus << BitmapTile.SIZE_2) + offset] >> 8) & 0xFF); + } + } + } + long scanlineStart = raf.getFilePointer(); + pbos.write(scanline); + pbos.flush(); + long scanlineEnd = raf.getFilePointer(); + raf.seek(greenCountsPosition + y * 2); + raf.writeShort((short)(scanlineEnd - scanlineStart)); + raf.seek(scanlineEnd); + } + + long endOfGreen = raf.getFilePointer(); + greenLength = (int) (endOfGreen - startOfGreen); + raf.seek(channelLengthPositions[layerIndex][2]); + raf.writeInt(greenLength); + raf.seek(endOfGreen); + + long startOfBlue = raf.getFilePointer(); + int blueLength; + + /* Compression: RLE */ + raf.writeShort(1); + + /* Blue scanline counts */ + long blueCountsPosition = raf.getFilePointer(); + for(int y = 0; y < image.getHeight(); y++) { + raf.writeShort(0); + } + + /* Blue image data */ + for(int y = 0; y < image.getHeight(); y++) { + int yModulus = y & BitmapTile.SIZE_MASK; + for(int x = 0; x < scanline.length; x += BitmapTile.SIZE) { + BitmapTile t = tiles.get(new Point(x >> BitmapTile.SIZE_2, y >> BitmapTile.SIZE_2)); + if(t == null) { + Arrays.fill(scanline, x, x + BitmapTile.SIZE, (byte) 0); + } else { + int[] tilePixels = t.getPixels(); + for(int offset = 0; offset < BitmapTile.SIZE; offset++) { + scanline[x + offset] = (byte) (tilePixels[(yModulus << BitmapTile.SIZE_2) + offset] & 0xFF); + } + } + } + long scanlineStart = raf.getFilePointer(); + pbos.write(scanline); + pbos.flush(); + long scanlineEnd = raf.getFilePointer(); + raf.seek(blueCountsPosition + y * 2); + raf.writeShort((short)(scanlineEnd - scanlineStart)); + raf.seek(scanlineEnd); + } + + long endOfBlue = raf.getFilePointer(); + blueLength = (int) (endOfBlue - startOfBlue); + raf.seek(channelLengthPositions[layerIndex][3]); + raf.writeInt(blueLength); + raf.seek(endOfBlue); + } +} diff --git a/src/com/jotuntech/sketcher/client/PackBitsOutputStream.java b/src/com/jotuntech/sketcher/client/PackBitsOutputStream.java new file mode 100755 index 0000000..7eeb226 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/PackBitsOutputStream.java @@ -0,0 +1,141 @@ +package com.jotuntech.sketcher.client; + +import java.io.DataOutput; +import java.io.IOException; +import java.io.OutputStream; + +public class PackBitsOutputStream extends OutputStream { + private DataOutput mdo; + + private final static int IN_BUFFER_SIZE = 2048; + private final static int IN_BUFFER_MASK = IN_BUFFER_SIZE - 1; + private final static int OUT_BUFFER_SIZE = 4096; + + private int[] inBuffer; + private int inBufferWritePos, inBufferReadPos, inBufferMark; + private byte[] outBuffer; + private int outBufferWritePos; + + public PackBitsOutputStream(DataOutput mdo) { + this.mdo = mdo; + this.inBuffer = new int[IN_BUFFER_SIZE]; + this.inBufferWritePos = 0; + this.inBufferReadPos = 0; + this.inBufferMark = 0; + this.outBuffer = new byte[OUT_BUFFER_SIZE]; + this.outBufferWritePos = 0; + } + + public void write(int b) throws IOException { + inBuffer[inBufferWritePos++ & IN_BUFFER_MASK] = b; + + if(inBufferWritePos - inBufferReadPos == IN_BUFFER_SIZE) { + pack(); + } + } + + public void write(byte[] b) throws IOException { + for(int i = 0; i < b.length; i++) { + inBuffer[inBufferWritePos++ & IN_BUFFER_MASK] = b[i]; + + if(inBufferWritePos - inBufferReadPos == IN_BUFFER_SIZE) { + pack(); + } + } + } + + public void write(byte[] b, int off, int len) throws IOException { + for(int i = off; i < off + len; i++) { + inBuffer[inBufferWritePos++ & IN_BUFFER_MASK] = b[i]; + + if(inBufferWritePos - inBufferReadPos == IN_BUFFER_SIZE) { + pack(); + } + } + } + + public void flush() throws IOException { + pack(); + } + + private void output(int b) { + outBuffer[outBufferWritePos++] = (byte) b; + } + + private void pack() throws IOException { + outBufferWritePos = 0; + + while(available() > 0) { + int b = read(); + int repeat = 1; + while(available() > 0 && peek() == b && repeat < 128) { + ++repeat; + skip(); + } + + if(repeat == 1) { + mark(); + int b2 = read(); + int literal = 0; + while(available() > 0 && peek() != b2 && literal < 127) { + b2 = read(); + ++literal; + } + + if(available() == 0 && literal < 127) { + ++literal; + } + + if(literal == 0) { + reset(); + output(0); + output(b); + } else { + reset(); + output(literal); + output(b); + for(int i = 0; i < literal; i++) { + output(read()); + } + } + } else if(repeat == 2) { + output(1); + output(b); + output(b); + } else { + output(1 - repeat); + output(b); + } + } + + mdo.write(outBuffer, 0, outBufferWritePos); + } + + public void close() throws IOException { + pack(); + } + + private int available() { + return inBufferWritePos - inBufferReadPos; + } + + private int peek() { + return inBuffer[inBufferReadPos & IN_BUFFER_MASK]; + } + + private int read() { + return inBuffer[inBufferReadPos++ & IN_BUFFER_MASK]; + } + + private void skip() { + ++inBufferReadPos; + } + + private void mark() { + inBufferMark = inBufferReadPos; + } + + private void reset() { + inBufferReadPos = inBufferMark; + } +} diff --git a/src/com/jotuntech/sketcher/client/Smoother.java b/src/com/jotuntech/sketcher/client/Smoother.java new file mode 100755 index 0000000..80bc299 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/Smoother.java @@ -0,0 +1,150 @@ +package com.jotuntech.sketcher.client; + +import java.util.ArrayList; +import java.util.List; + +import com.jotuntech.sketcher.common.Input; + +/** + * Static singleton for input interpolation. + * + * @author Thor Harald Johansen + */ +public class Smoother { + /* Smoothing threshold */ + private static float threshold = 4f; + private static float smoothness = 1.5f; + private static boolean enabled = true; + + Input[] cursor = new Input[] {new Input(), new Input(), new Input()}; + + /** + * Sets all points to given input + * @param d input to add. + */ + + public void set(Input d) { + cursor[0] = d; + cursor[1] = cursor[0]; + cursor[2] = cursor[1]; + } + + public void setPressure(int pressure) { + //cursor[0].pressure = pressure; + //cursor[1].pressure = pressure; + //cursor[2].pressure = pressure; + } + + /** + * Adds a new input for smoothing. + * + * @param d input to add. + */ + + public void add(Input d) { + float dx = d.x - cursor[2].x; + float dy = d.y - cursor[2].y; + float dd = d.pressure - cursor[2].pressure; + + float h = (float)Math.hypot(dx, dy); + + float d2 = h - smoothness; + + if(d2 > 0) { + float sx = dx * d2 / h; + float sy = dy * d2 / h; + float sd = dd * d2 / h; + + cursor[0] = cursor[1]; + cursor[1] = cursor[2]; + cursor[2] = new Input(cursor[2].x + sx, cursor[2].y + sy, (int)(cursor[2].pressure + sd)); + } + } + + /** + * Return a list of smoothed inputs. + * + * @return List of smoothed inputs. + */ + + public List get() { + /* Create list for inputs. */ + List inputs = new ArrayList(); + + if(enabled) { + /* Segment cursor. */ + segment(inputs, cursor[0], cursor[1], cursor[2]); + } else { + inputs.add(cursor[2]); + } + + return inputs; + } + + public Input getIndex(int index) { + return cursor[index]; + } + + // uses a, b and c to interpolate inputs between b and c, store in inputs. + void segment(List inputs, Input a, Input b, Input c) { + // difference AB & BC + Input dab = a.difference(b); + Input dbc = b.difference(c); + + // magnitude AB & BC + float mab = (float)Math.hypot(dab.x, dab.y); + float mbc = (float)Math.hypot(dbc.x, dbc.y); + + // orientation AB & BC + float oab = (float)Math.acos(dab.x / mab) * (dab.y < 0 ? -1 : 1); + float obc = (float)Math.acos(dbc.x / mbc) * (dbc.y < 0 ? -1 : 1); + + // normalize + if(obc - oab > Math.PI || obc - oab < -Math.PI) { + obc -= Math.PI * 2; + } + + // angle ABC + float abc = Math.abs(obc - oab); + + // threshold of operation + if(mbc * abc > threshold && abc < Math.PI / 2) { + // three-quarter of angle ABC + float angle = (oab + (obc * 3)) / 4; + + // half of magnitude BC + Input d = new Input(b.x + ((float)Math.cos(angle) * mbc / 2), b.y + + ((float)Math.sin(angle) * mbc / 2), (b.pressure + c.pressure) / 2); + + segment(inputs, a, b, d); + inputs.add(d); + segment(inputs, b, d, c); + } else { + inputs.add(c); + } + } + + public static void setThreshold(float threshold) { + Smoother.threshold = threshold; + } + + public static float getThreshold() { + return threshold; + } + + public static void setSmoothness(float smoothness) { + Smoother.smoothness = smoothness; + } + + public static float getSmoothness() { + return smoothness; + } + + public static void setEnabled(boolean enabled) { + Smoother.enabled = enabled; + } + + public static boolean isEnabled() { + return enabled; + } +} diff --git a/src/com/jotuntech/sketcher/client/UserInterface.java b/src/com/jotuntech/sketcher/client/UserInterface.java new file mode 100755 index 0000000..9ebb774 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/UserInterface.java @@ -0,0 +1,1723 @@ +package com.jotuntech.sketcher.client; + +import info.clearthought.layout.TableLayout; + +import java.applet.Applet; +import java.applet.AudioClip; +import java.awt.AlphaComposite; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Container; +import java.awt.Cursor; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Insets; +import java.awt.Rectangle; +import java.awt.Toolkit; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.AdjustmentEvent; +import java.awt.event.AdjustmentListener; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.util.Map; +import java.util.Set; + +import javax.imageio.ImageIO; +import javax.swing.AbstractListModel; +import javax.swing.BorderFactory; +import javax.swing.BoundedRangeModel; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.ButtonGroup; +import javax.swing.Icon; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JDialog; +import javax.swing.JFileChooser; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JProgressBar; +import javax.swing.JRadioButton; +import javax.swing.JScrollBar; +import javax.swing.JScrollPane; +import javax.swing.JSlider; +import javax.swing.JSpinner; +import javax.swing.JSplitPane; +import javax.swing.JTabbedPane; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.JToggleButton; +import javax.swing.ListModel; +import javax.swing.SpinnerNumberModel; +import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; + +import com.jotuntech.sketcher.client.command.CreateLayerCommand; +import com.jotuntech.sketcher.client.command.DeleteLayerCommand; +import com.jotuntech.sketcher.client.command.FilterCommand; +import com.jotuntech.sketcher.client.command.KickCommand; +import com.jotuntech.sketcher.client.command.MergeCommand; +import com.jotuntech.sketcher.client.command.SayCommand; +import com.jotuntech.sketcher.client.command.SetBrushCommand; +import com.jotuntech.sketcher.client.command.SetColorCommand; +import com.jotuntech.sketcher.client.command.SetLayerCommand; +import com.jotuntech.sketcher.client.command.VoiceCommand; +import com.jotuntech.sketcher.common.Brush; +import com.jotuntech.sketcher.common.Canvas; +import com.jotuntech.sketcher.common.Layer; +import com.jotuntech.sketcher.common.LayerType; +import com.jotuntech.sketcher.common.Log; +import com.jotuntech.sketcher.common.Pixels; +import com.jotuntech.sketcher.common.TwoWayHashMap; +import com.jotuntech.sketcher.common.User; +import com.jotuntech.sketcher.common.filter.AutoContrastFilter; +import com.jotuntech.sketcher.common.filter.BlurFilter; + +public class UserInterface extends JPanel { + public final static Insets NO_INSETS = new Insets(0, 0, 0, 0); + + protected Client client; + + private JSplitPane splitPane; + + private JScrollPane editorPane; + private Editor editor; + + private JPanel rightPanel; + + private JMenuBar menuBar; + private JMenu filterMenu; + + private JCheckBoxMenuItem soundItem, tagsItem; + + JCollapsiblePanelGroup mainGroup; + + private JScrollPane chatPane; + private JTextArea chatLog; + private JTextField chatField; + + private BrushButton[] brushButtons; + + private JLabel zoomLabel; + private JSpinner zoomSpin; + JCheckBox smoothBox; + + private JCheckBox opacityBox, flowBox, radiusBox, lockTransBox; + + private JUserList userList; + + private JList layerList; + + private JSlider hardnessSlider, sizeSlider, opacitySlider, flowSlider, spacingSlider, jitterSlider, noiseSlider, waterSlider, waterAreaSlider; + + private JSlider redSlider, greenSlider, blueSlider; + private JPanel rgbColor; + + private JSlider hueSlider, satSlider, brightSlider; + private JPanel hsbColor; + + private JSlider cyanSlider, magentaSlider, yellowSlider; + private JPanel cmyColor; + + private JCollapsiblePanel brushPanel, brushSettingsPanel, colorPanel, inputPanel; + + private JToggleButton selectButton, moveButton, freehandButton, lineButton, bezierButton, rectangleButton, ovalButton; + + private boolean smoothZoom = true; + private boolean colorAdjusting = false; + protected boolean brushAdjusting = false; + private boolean layerAdjusting = false; + + public static AudioClip + AUDIO_INTRO = Applet.newAudioClip(UserInterface.class.getResource("audio/intro.wav")), + AUDIO_SIGN_IN = Applet.newAudioClip(UserInterface.class.getResource("audio/signin.wav")), + AUDIO_SIGN_OUT = Applet.newAudioClip(UserInterface.class.getResource("audio/signout.wav")), + AUDIO_KICK = Applet.newAudioClip(UserInterface.class.getResource("audio/kick.wav")), + AUDIO_CHAT = Applet.newAudioClip(UserInterface.class.getResource("audio/chat.wav")), + AUDIO_OUTTRO = Applet.newAudioClip(UserInterface.class.getResource("audio/outtro.wav")); + + private static Icon[] brushIcons = new Icon[] { + new ImageIcon(UserInterface.class.getResource("images/pen.png")), + new ImageIcon(UserInterface.class.getResource("images/pencil.png")), + new ImageIcon(UserInterface.class.getResource("images/water2.png")), + new ImageIcon(UserInterface.class.getResource("images/eraser.png")), + new ImageIcon(UserInterface.class.getResource("images/ink.png")), + new ImageIcon(UserInterface.class.getResource("images/wipe.png")) + }; + + private void saveFileToDisk(final boolean selectionOnly) { + try { + final JFileChooser fileChooser = new JFileChooser(); + if(fileChooser.showSaveDialog(null) != JFileChooser.APPROVE_OPTION) { + return; + } + + final File file = fileChooser.getSelectedFile(); + + if(file.exists()) { + if(JOptionPane.showConfirmDialog(null, "Overwrite existing file?", "Confirm", JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) { + return; + } + } + + Thread saveThread = new Thread("SaveThread") { + public void run() { + BufferedImage image; + if(selectionOnly) { + Rectangle s = editor.getSelect(); + if(s == null) { + JOptionPane.showMessageDialog(UserInterface.this, "Cannot save empty selection!", "Error", JOptionPane.ERROR_MESSAGE); + return; + } + image = editor.getImage().getSubimage(s.x, s.y, s.width, s.height); + } else { + image = editor.getImage(); + } + + if(file.getName().toUpperCase().endsWith(".PNG")) { + println("Saving PNG file. Please wait..."); + try { + ImageIO.write(image, "PNG", file); + } catch (Throwable e) { + JOptionPane.showMessageDialog(UserInterface.this, "Unable to save file! (" + e.getMessage() + ")", "Error", JOptionPane.ERROR_MESSAGE); + return; + } + println("Completed!"); + } else if(file.getName().toUpperCase().endsWith(".PSD")) { + if(selectionOnly) { + JOptionPane.showMessageDialog(UserInterface.this, "Cannot save partial canvases in PSD format!", "Error", JOptionPane.ERROR_MESSAGE); + return; + } + println("Saving PSD file. Please wait..."); + Canvas canvas = client.getCanvas(); + TwoWayHashMap lm = canvas.getLayerMap(); + try { + RandomAccessFile raf = new RandomAccessFile(fileChooser.getSelectedFile(), "rw"); + + PSDEncoder psde = new PSDEncoder(raf, image, lm); + psde.encode(); + raf.close(); + System.gc(); + } catch (Throwable e) { + JOptionPane.showMessageDialog(UserInterface.this, "Unable to save file! (" + e.getMessage() + ")", "Error", JOptionPane.ERROR_MESSAGE); + return; + } + println("Completed!"); + } else { + JOptionPane.showMessageDialog(UserInterface.this, "You need to add an extension like .png or .psd to save a file!", "Error", JOptionPane.ERROR_MESSAGE); + } + } + }; + saveThread.setPriority(Thread.MIN_PRIORITY); + saveThread.start(); + } catch(SecurityException e) { + JOptionPane.showMessageDialog(UserInterface.this, "You cannot use the save feature without accepting the security certificate!", "Error", JOptionPane.ERROR_MESSAGE); + } + } + + public UserInterface(final Client client) { + super(new BorderLayout()); + + this.client = client; + + editor = new Editor(client); + editor.setSmoothZoom(true); + + if(client.isAds()) { + try { + BufferedImage bi = ImageIO.read(UserInterface.class.getResource("images/ad1.png")); + editor.setAd(bi); + editor.setAdPosition(-468, -60); + } catch (IOException e1) { + e1.printStackTrace(); + } + } + + if(!LookAndFeel.NIMBUS.activate()) { + LookAndFeel.SYSTEM.activate(); + } + + editorPane = new JScrollPane(editor); + + AdjustmentListener canvasScroller = new AdjustmentListener() { + public void adjustmentValueChanged(AdjustmentEvent arg0) { + boolean adjusting = arg0.getValueIsAdjusting(); + editor.setAdPosition(editorPane.getHorizontalScrollBar().getValue() + editorPane.getViewport().getWidth() - 478, editorPane.getVerticalScrollBar().getValue() + 10); + if(smoothZoom) { + if(adjusting) { + editor.setSmoothZoom(false); + } else { + editor.setSmoothZoom(true); + editor.repaint(); + } + } + } + + }; + + editorPane.getVerticalScrollBar().addAdjustmentListener(canvasScroller); + editorPane.getHorizontalScrollBar().addAdjustmentListener(canvasScroller); + + /** Create panel for tools */ + rightPanel = new JPanel(); + rightPanel.setLayout(new BoxLayout(rightPanel, BoxLayout.Y_AXIS)); + + /** Make split pane with canvas on left, tools on right */ + splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, editorPane, rightPanel); + splitPane.setResizeWeight(1.0); + splitPane.setOneTouchExpandable(true); + add(splitPane, BorderLayout.CENTER); + + menuBar = new JMenuBar(); + menuBar.setAlignmentX(Component.LEFT_ALIGNMENT); + menuBar.setMinimumSize(new Dimension(0, 24)); + menuBar.setMaximumSize(new Dimension(Short.MAX_VALUE, Short.MAX_VALUE)); + rightPanel.add(menuBar); + + JMenu fileMenu = new JMenu("File"); + menuBar.add(fileMenu); + + JMenuItem saveItem = new JMenuItem("Save Canvas As..."); + saveItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent arg0) { + saveFileToDisk(false); + } + }); + fileMenu.add(saveItem); + + JMenuItem saveSelectItem = new JMenuItem("Save Selection As..."); + saveSelectItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent arg0) { + saveFileToDisk(true); + } + }); + fileMenu.add(saveSelectItem); + + JMenuItem uploadSelectItem = new JMenuItem("Upload Selection..."); + uploadSelectItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent arg0) { + final Rectangle s = editor.getSelect(); + if(s == null) { + JOptionPane.showMessageDialog(UserInterface.this, "Cannot upload empty selection!", "Error", JOptionPane.ERROR_MESSAGE); + return; + } + + final JDialog uploadDialog = new JDialog(); + uploadDialog.setTitle("Upload Selection to ArtGrounds.com"); + uploadDialog.setResizable(false); + uploadDialog.setModal(true); + Container contentPane = uploadDialog.getContentPane(); + contentPane.setLayout(new BorderLayout()); + JPanel northPanel = new JPanel(); + northPanel.setBorder(BorderFactory.createEmptyBorder(8, 8, 0, 8)); + northPanel.setLayout(new BoxLayout(northPanel, BoxLayout.Y_AXIS)); + northPanel.add(new JLabel("Format:")); + JPanel formatPanel = new JPanel(); + formatPanel.setLayout(new BoxLayout(formatPanel, BoxLayout.X_AXIS)); + ButtonGroup formatGroup = new ButtonGroup(); + final JRadioButton jpegButton = new JRadioButton("JPEG (realism, paintings, soft shading)"); + jpegButton.setSelected(true); + formatGroup.add(jpegButton); + formatPanel.add(jpegButton); + formatPanel.add(Box.createRigidArea(new Dimension(8, 0))); + final JRadioButton pngButton = new JRadioButton("PNG (cartoons, drawings, hard shading)"); + formatGroup.add(pngButton); + formatPanel.add(pngButton); + northPanel.add(formatPanel); + formatPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + northPanel.add(new JLabel("Title:")); + final JTextField titleField = new JTextField(); + titleField.setAlignmentX(Component.LEFT_ALIGNMENT); + northPanel.add(titleField); + northPanel.add(Box.createRigidArea(new Dimension(0, 8))); + northPanel.add(new JLabel("Description:")); + contentPane.add(northPanel, BorderLayout.NORTH); + contentPane.add(Box.createRigidArea(new Dimension(8, 0)), BorderLayout.WEST); + final JTextArea descArea = new JTextArea(); + contentPane.add(descArea, BorderLayout.CENTER); + contentPane.add(Box.createRigidArea(new Dimension(8, 0)), BorderLayout.EAST); + JPanel southPanel = new JPanel(); + southPanel.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); + southPanel.setLayout(new BoxLayout(southPanel, BoxLayout.X_AXIS)); + final JProgressBar progressBar = new JProgressBar(); + southPanel.add(progressBar); + southPanel.add(Box.createRigidArea(new Dimension(8, 0))); + final JCheckBox matureBox = new JCheckBox("Mature Content"); + southPanel.add(matureBox); + southPanel.add(Box.createRigidArea(new Dimension(8, 0))); + final JButton cancelButton = new JButton("Cancel"); + cancelButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + uploadDialog.setVisible(false); + } + }); + final JButton uploadButton = new JButton("Upload"); + uploadButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + Thread saveThread = new Thread("SaveThread") { + public void run() { + try { + String title = titleField.getText().trim(); + if(title.isEmpty()) { + JOptionPane.showMessageDialog(uploadDialog, "Title cannot be empty!", "Error", JOptionPane.ERROR_MESSAGE); + return; + } + String description = descArea.getText().trim(); + if(description.isEmpty()) { + JOptionPane.showMessageDialog(uploadDialog, "Description cannot be empty!", "Error", JOptionPane.ERROR_MESSAGE); + return; + } + + uploadButton.setEnabled(false); + jpegButton.setEnabled(false); + pngButton.setEnabled(false); + titleField.setEnabled(false); + descArea.setEnabled(false); + matureBox.setEnabled(false); + + BufferedImage argbImage = editor.getImage().getSubimage(s.x, s.y, s.width, s.height); + BufferedImage rgbImage = new BufferedImage(s.width, s.height, BufferedImage.TYPE_INT_RGB); + Graphics g = rgbImage.getGraphics(); + g.drawImage(argbImage, 0, 0, editor.getCanvasBackground(), null); + + BufferedImage madeImage = ImageIO.read(UserInterface.class.getResource("images/madeinsketcher.png")); + g.drawImage(madeImage, s.width - madeImage.getWidth() - 2, 2, null); + + ByteArrayOutputStream imgbaos = new ByteArrayOutputStream(); + ImageIO.write(rgbImage, jpegButton.isSelected() ? "JPG" : "PNG", imgbaos); + byte[] imgarr = imgbaos.toByteArray(); + progressBar.setMaximum(imgarr.length); + + ByteArrayOutputStream hdrbaos = new ByteArrayOutputStream(); + DataOutputStream hdrdos = new DataOutputStream(hdrbaos); + hdrdos.writeUTF(client.getLogin()); + hdrdos.writeUTF(client.getPassword()); + hdrdos.writeUTF(title); + hdrdos.writeUTF(description); + hdrdos.writeBoolean(matureBox.isSelected()); + hdrdos.writeInt(imgarr.length); + byte[] hdrarr = hdrbaos.toByteArray(); + + int contentLength = hdrarr.length + imgarr.length; + + URL url = new URL("http://www.artgrounds.com/sketcher-upload/"); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setFixedLengthStreamingMode(contentLength); + conn.setDoOutput(true); + + OutputStream httpos = conn.getOutputStream(); + + httpos.write(hdrarr); + + for(int ofs = 0; ofs < imgarr.length; ofs += 1458) { + progressBar.setValue(ofs); + httpos.write(imgarr, ofs, Math.min(1458, imgarr.length - ofs)); + httpos.flush(); + } + progressBar.setValue(imgarr.length); + httpos.flush(); + httpos.close(); + + if(conn.getResponseCode() != HttpURLConnection.HTTP_OK) { + progressBar.setValue(0); + uploadButton.setEnabled(true); + jpegButton.setEnabled(true); + pngButton.setEnabled(true); + titleField.setEnabled(true); + descArea.setEnabled(true); + matureBox.setEnabled(true); + JOptionPane.showMessageDialog(uploadDialog, "Unable to upload image! (" + conn.getResponseCode() + " " + conn.getResponseMessage() + ")", "Error", JOptionPane.ERROR_MESSAGE); + return; + } + + cancelButton.setText("Close"); + + JOptionPane.showMessageDialog(uploadDialog, "Upload completed!", "Information", JOptionPane.INFORMATION_MESSAGE); + } catch (Throwable t) { + progressBar.setValue(0); + uploadButton.setEnabled(true); + jpegButton.setEnabled(true); + pngButton.setEnabled(true); + titleField.setEnabled(true); + descArea.setEnabled(true); + matureBox.setEnabled(true); + JOptionPane.showMessageDialog(uploadDialog, "Unable to upload image! (" + t.getClass().getSimpleName() + " " + t.getMessage() + ")", "Error", JOptionPane.ERROR_MESSAGE); + Log.error(t); + } + } + }; + saveThread.setPriority(Thread.MIN_PRIORITY); + saveThread.start(); + } + }); + southPanel.add(uploadButton); + southPanel.add(cancelButton); + contentPane.add(southPanel, BorderLayout.SOUTH); + uploadDialog.setPreferredSize(new Dimension(512, 512)); + uploadDialog.pack(); + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + uploadDialog.setLocation((screenSize.width - uploadDialog.getWidth()) / 2, (screenSize.height - uploadDialog.getHeight()) / 2); + uploadDialog.setVisible(true); + } + }); + fileMenu.add(uploadSelectItem); + + JMenu optMenu = new JMenu("Options"); + menuBar.add(optMenu); + + soundItem = new JCheckBoxMenuItem("Sounds", client.isSoundEnabled()); + soundItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + client.setSoundEnabled(((JCheckBoxMenuItem) e.getSource()).isSelected()); + } + }); + + optMenu.add(soundItem); + + tagsItem = new JCheckBoxMenuItem("Tags", editor.isTagsEnabled()); + tagsItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + editor.setTagsEnabled(((JCheckBoxMenuItem) e.getSource()).isSelected()); + editor.repaint(); + } + }); + + optMenu.add(tagsItem); + + JCheckBoxMenuItem voiceItem = new JCheckBoxMenuItem("Voice", client.isVoiceEnabled()); + voiceItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + client.getCommandQueue().offer(new CommandEntry(0, new VoiceCommand(((JCheckBoxMenuItem) e.getSource()).isSelected()))); + } + }); + + optMenu.add(voiceItem); + + JCheckBoxMenuItem swCursorItem = new JCheckBoxMenuItem("Software Cursor", editor.isSoftwareCursorEnabled()); + swCursorItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + editor.setSoftwareCursorEnabled(((JCheckBoxMenuItem) e.getSource()).isSelected()); + editor.setCursorType(Editor.CursorType.SOFTWARE); + } + }); + + optMenu.add(swCursorItem); + + + JMenuItem resetItem = new JMenuItem("Reset"); + resetItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + int confirm = JOptionPane.showConfirmDialog(UserInterface.this, "This will erase all your settings permanently. Continue?", "Confirm", JOptionPane.YES_NO_OPTION); + if(confirm != JOptionPane.OK_OPTION) { + return; + } + client.resetProps(); + } + }); + + optMenu.add(resetItem); + + filterMenu = new JMenu("Filters"); + filterMenu.setEnabled(false); + menuBar.add(filterMenu); + + JMenuItem blurItem = new JMenuItem("Blur"); + blurItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + final Rectangle select = editor.getSelect(); + if(select == null) { + JOptionPane.showMessageDialog(UserInterface.this, "You must select an area before you can blur anything!", "Error", JOptionPane.ERROR_MESSAGE); + return; + } + + final JDialog blurDialog = new JDialog(); + blurDialog.setTitle("Blur"); + blurDialog.setModal(true); + + Container contentPane = blurDialog.getContentPane(); + contentPane.setLayout(new BoxLayout(contentPane, BoxLayout.X_AXIS)); + + contentPane.add(new JLabel("Size: ")); + final SpinnerNumberModel sizeModel = new SpinnerNumberModel(5, 0.5, 50, 0.5); + JSpinner sizeSpin = new JSpinner(sizeModel); + contentPane.add(sizeSpin); + JButton okButton = new JButton("OK"); + okButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + blurDialog.setVisible(false); + editor.requestFocusInWindow(); + client.getCommandQueue().offer(new CommandEntry(0, new FilterCommand(new BlurFilter(), select.x, select.y, select.width, select.height, ((Double) sizeModel.getValue()).floatValue(), 0f, 0f))); + client.getCommandQueue().offer(new CommandEntry(0, new MergeCommand())); + } + }); + contentPane.add(okButton); + JButton cancelButton = new JButton("Cancel"); + cancelButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + editor.requestFocusInWindow(); + blurDialog.setVisible(false); + } + }); + contentPane.add(cancelButton); + blurDialog.pack(); + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + blurDialog.setLocation((screenSize.width - blurDialog.getWidth()) / 2, (screenSize.height - blurDialog.getHeight()) / 2); + blurDialog.setVisible(true); + } + }); + filterMenu.add(blurItem); + + JMenuItem autoContrastItem = new JMenuItem("Auto Contrast"); + autoContrastItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + final Rectangle select = editor.getSelect(); + if(select == null) { + JOptionPane.showMessageDialog(UserInterface.this, "You must select an area before you can correct its contrast!", "Error", JOptionPane.ERROR_MESSAGE); + return; + } + + client.getCommandQueue().offer(new CommandEntry(0, new FilterCommand(new AutoContrastFilter(), select.x, select.y, select.width, select.height, 0.05f, 0.05f, 0.05f))); + client.getCommandQueue().offer(new CommandEntry(0, new MergeCommand())); + } + }); + filterMenu.add(autoContrastItem); + + menuBar.add(Box.createHorizontalGlue()); + + JMenu helpMenu = new JMenu("Help"); + menuBar.add(helpMenu); + + JMenuItem aboutItem = new JMenuItem("About"); + aboutItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + final JDialog aboutDialog = new JDialog(); + aboutDialog.setTitle("About Sketcher\u2122"); + aboutDialog.setResizable(false); + aboutDialog.setModal(true); + Container contentPane = aboutDialog.getContentPane(); + contentPane.setLayout(new BoxLayout(contentPane, BoxLayout.Y_AXIS)); + JLabel label = new JLabel(new ImageIcon(getClass().getResource("images/about.png"))); + label.addMouseListener(new MouseListener() { + public void mouseClicked(MouseEvent e) { + aboutDialog.setVisible(false); + } + + public void mouseEntered(MouseEvent e) { } + public void mouseExited(MouseEvent e) { } + public void mousePressed(MouseEvent e) { } + public void mouseReleased(MouseEvent e) { } + }); + contentPane.add(label); + aboutDialog.pack(); + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + aboutDialog.setLocation((screenSize.width - aboutDialog.getWidth()) / 2, (screenSize.height - aboutDialog.getHeight()) / 2); + aboutDialog.setVisible(true); + } + }); + helpMenu.add(aboutItem); + + JLabel copyright = new JLabel(new ImageIcon(UserInterface.class.getResource("images/sketcherlogo.png"))); + copyright.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + copyright.setAlignmentX(Component.LEFT_ALIGNMENT); + copyright.setHorizontalAlignment(SwingConstants.CENTER); + copyright.setMaximumSize(new Dimension(Short.MAX_VALUE, Short.MAX_VALUE)); + copyright.addMouseListener(new MouseListener() { + public void mouseClicked(MouseEvent e) { + 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://sketcher.artgrounds.com/")); // desktop.browse(uri); + return; + } + } + } catch(Throwable t) { } + + JOptionPane.showMessageDialog(UserInterface.this, "Visit sketcher.artgrounds.com for more information about Sketcher!"); + } + + public void mouseEntered(MouseEvent e) { } + public void mouseExited(MouseEvent e) { } + public void mousePressed(MouseEvent e) { } + public void mouseReleased(MouseEvent e) { } + }); + rightPanel.add(copyright); + + mainGroup = new JCollapsiblePanelGroup(); + + /** Set up view panel */ + JCollapsiblePanel viewPanel = new JCollapsiblePanel(100, "Canvas", false, false); + viewPanel.setGroup(mainGroup); + JPanel viewPanelContent = viewPanel.getContentPane(); + viewPanelContent.setLayout(new BoxLayout(viewPanelContent, BoxLayout.Y_AXIS)); + + /* Set up zoom panel */ + JPanel zoomPanel = new JPanel(); + zoomPanel.setLayout(new BoxLayout(zoomPanel, BoxLayout.X_AXIS)); + viewPanelContent.add(zoomPanel); + + zoomPanel.add(Box.createHorizontalGlue()); + + zoomLabel = new JLabel(new ImageIcon(UserInterface.class.getResource("images/zoom.png"))); + zoomLabel.setAlignmentY(Component.CENTER_ALIGNMENT); + zoomPanel.add(zoomLabel); + + zoomSpin = new JSpinner(new SpinnerNumberModel(100, 10, 800, 25)); + zoomSpin.setToolTipText("Zoom - grow or shrink the displayed canvas."); + zoomSpin.setAlignmentY(Component.CENTER_ALIGNMENT); + zoomPanel.add(zoomSpin); + + zoomSpin.getModel().addChangeListener(new ChangeListener() { + + public void stateChanged(ChangeEvent arg0) { + if(client.getCanvas() != null) { + try { + int newZoom = (Integer)zoomSpin.getValue(); + + if(newZoom > 0 && newZoom <= 800) { + /* Fetch scrollbars */ + JScrollBar v = editorPane.getHorizontalScrollBar(); + JScrollBar h = editorPane.getVerticalScrollBar(); + + /* Fetch fractions */ + float hf = (float)h.getValue() / (float)h.getMaximum(); + float vf = (float)v.getValue() / (float)v.getMaximum(); + float hvis = (float)h.getVisibleAmount() / (float)h.getMaximum(); + float vvis = (float)v.getVisibleAmount() / (float)v.getMaximum(); + + /* Change zoom */ + editor.setZoom(newZoom / 100f); + editor.repaint(); + + /* Apply old fractions to new viewport */ + float nhvis = (float)h.getVisibleAmount() / (float)h.getMaximum(); + float nvvis = (float)v.getVisibleAmount() / (float)v.getMaximum(); + h.setValue((int)((hf + (hvis - nhvis) / 2) * h.getMaximum())); + v.setValue((int)((vf + (vvis - nvvis) / 2) * v.getMaximum())); + } + } catch(NumberFormatException e) { + /** Ignore bad numbers */ + } + } + } + + }); + + /** Smooth zoom box */ + smoothBox = new JCheckBox("Smoothly"); + smoothBox.setToolTipText("Display the canvas smoothly for zoom settings not divisble by 100. Somewhat computing intensive."); + smoothBox.setAlignmentY(Component.CENTER_ALIGNMENT); + smoothBox.setHorizontalAlignment(JCheckBox.CENTER); + smoothBox.setSelected(smoothZoom); + zoomPanel.add(smoothBox); + + smoothBox.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent arg0) { + smoothZoom = smoothBox.isSelected(); + editor.setSmoothZoom(smoothZoom); + editor.repaint(); + } + + }); + + zoomPanel.add(Box.createHorizontalGlue()); + + /* Background brightness */ + final JSlider backgroundSlider = new JSlider(0, 255, 255); + backgroundSlider.setToolTipText("Background brightness - adjust the background brightness to see layer transparency."); + backgroundSlider.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent arg0) { + if(!backgroundSlider.getModel().getValueIsAdjusting()) { + int brightness = backgroundSlider.getValue(); + editor.setCanvasBackground(new Color(brightness, brightness, brightness)); + editor.repaint(); + } + } + }); + viewPanelContent.add(backgroundSlider); + + rightPanel.add(viewPanel); + + + /** Create Tools panel, add to right panel */ + JCollapsiblePanel toolsPanel = new JCollapsiblePanel(150, "Tools", true, true); + toolsPanel.setGroup(mainGroup); + rightPanel.add(toolsPanel); + JPanel toolsPane = toolsPanel.getContentPane(); + toolsPane.setLayout(new BoxLayout(toolsPane, BoxLayout.X_AXIS)); + + /** Make group for buttons */ + ButtonGroup toolButtonGroup = new ButtonGroup(); + + toolsPane.add(Box.createHorizontalGlue()); + + selectButton = new JToggleButton(loadImageIcon("select.png")); + selectButton.setToolTipText("Select"); + selectButton.setMargin(UserInterface.NO_INSETS); + selectButton.setMinimumSize(new Dimension(32, 32)); + selectButton.setPreferredSize(new Dimension(32, 32)); + selectButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + editor.setState(Editor.State.SELECT_HOVER); + updatePanels(); + } + }); + toolsPane.add(selectButton); + toolButtonGroup.add(selectButton); + + moveButton = new JToggleButton(loadImageIcon("move.png")); + moveButton.setToolTipText("Duplicate"); + moveButton.setMargin(UserInterface.NO_INSETS); + moveButton.setMinimumSize(new Dimension(32, 32)); + moveButton.setPreferredSize(new Dimension(32, 32)); + moveButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + editor.setState(Editor.State.MOVE_HOVER); + updatePanels(); + } + }); + toolsPane.add(moveButton); + toolButtonGroup.add(moveButton); + + freehandButton = new JToggleButton(loadImageIcon("freehand.png")); + freehandButton.setSelected(true); + freehandButton.setToolTipText("Freehand"); + freehandButton.setMargin(UserInterface.NO_INSETS); + freehandButton.setMinimumSize(new Dimension(32, 32)); + freehandButton.setPreferredSize(new Dimension(32, 32)); + freehandButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + editor.setState(Editor.State.DRAW_HOVER); + updatePanels(); + } + }); + toolsPane.add(freehandButton); + toolButtonGroup.add(freehandButton); + + lineButton = new JToggleButton(loadImageIcon("line.png")); + lineButton.setToolTipText("Line"); + lineButton.setMargin(UserInterface.NO_INSETS); + lineButton.setMinimumSize(new Dimension(32, 32)); + lineButton.setPreferredSize(new Dimension(32, 32)); + lineButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + editor.setState(Editor.State.LINE_HOVER); + updatePanels(); + } + }); + toolsPane.add(lineButton); + toolButtonGroup.add(lineButton); + + bezierButton = new JToggleButton(loadImageIcon("bezier.png")); + bezierButton.setToolTipText("Bezier"); + bezierButton.setMargin(UserInterface.NO_INSETS); + bezierButton.setMinimumSize(new Dimension(32, 32)); + bezierButton.setPreferredSize(new Dimension(32, 32)); + bezierButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + editor.setState(Editor.State.BEZIER_HOVER); + updatePanels(); + } + }); + toolsPane.add(bezierButton); + toolButtonGroup.add(bezierButton); + + rectangleButton = new JToggleButton(loadImageIcon("rectangle.png")); + rectangleButton.setToolTipText("Rectangle"); + rectangleButton.setMargin(UserInterface.NO_INSETS); + rectangleButton.setMinimumSize(new Dimension(32, 32)); + rectangleButton.setPreferredSize(new Dimension(32, 32)); + rectangleButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + editor.setState(Editor.State.RECT_HOVER); + updatePanels(); + } + }); + toolsPane.add(rectangleButton); + toolButtonGroup.add(rectangleButton); + + ovalButton = new JToggleButton(loadImageIcon("oval.png")); + ovalButton.setToolTipText("Oval"); + ovalButton.setMargin(UserInterface.NO_INSETS); + ovalButton.setMinimumSize(new Dimension(32, 32)); + ovalButton.setPreferredSize(new Dimension(32, 32)); + ovalButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + editor.setState(Editor.State.OVAL_HOVER); + updatePanels(); + } + }); + toolsPane.add(ovalButton); + toolButtonGroup.add(ovalButton); + + toolsPane.add(Box.createHorizontalGlue()); + + /** Create Brushes panel, add to right panel */ + brushPanel = new JCollapsiblePanel(200, "Brushes", true, true); + brushPanel.setGroup(mainGroup); + rightPanel.add(brushPanel); + JPanel brushesPane = brushPanel.getContentPane(); + brushesPane.setLayout(new BoxLayout(brushesPane, BoxLayout.X_AXIS)); + + /** Make group for buttons */ + ButtonGroup brushButtonGroup = new ButtonGroup(); + + /** Make, add and group brush buttons */ + brushButtons = new BrushButton[client.brushes.length]; + + brushesPane.add(Box.createHorizontalGlue()); + + for(int i = 0; i < brushButtons.length; i++) { + BrushButton b = new BrushButton(this, client.brushes, brushIcons, i); + brushesPane.add(b); + brushButtonGroup.add(b); + brushButtons[i] = b; + } + + brushesPane.add(Box.createHorizontalGlue()); + + /** BRUSH panel */ + TableLayout brushTabLayout = new TableLayout(new double[][] {{0, -2, -1, -2, 0}, {-2, -2, -2, -2, -2, -2, -2, -2, -2}}); + brushSettingsPanel = new JCollapsiblePanel(300, "Brush settings", false, false); + brushSettingsPanel.setGroup(mainGroup); + JPanel brushTab = brushSettingsPanel.getContentPane(); + brushTab.setLayout(brushTabLayout); + brushTab.setAlignmentX(Component.LEFT_ALIGNMENT); + rightPanel.add(brushSettingsPanel); + + /** HARDNESS */ + brushTab.add(new JLabel(loadImageIcon("hardness.png")), "1 0 r c"); + hardnessSlider = new JSlider(1, 12, 1); + hardnessSlider.setToolTipText("Hardness - controls the crispness of the brush."); + hardnessSlider.setSnapToTicks(true); + brushTab.add(hardnessSlider, "2 0 2 0"); + hardnessSlider.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent arg0) { + if(!hardnessSlider.getModel().getValueIsAdjusting() && !brushAdjusting && client.getConnection().getUser() != null && client.getConnection().getUser().getBrush() != null) { + client.getConnection().getUser().getBrush().setHardness(hardnessSlider.getValue()); + client.getConnection().getSendQueue().offer(new CommandEntry(0, new SetBrushCommand(client.getConnection().getUser().getBrush()))); + } + } + }); + + /** SIZE */ + brushTab.add(new JLabel(loadImageIcon("size.png")), "1 1 r c"); + sizeSlider = new JSlider(1, 1008, 1); + sizeSlider.setToolTipText("Size - controls the diameter of the brush."); + brushTab.add(sizeSlider, "2 1 2 1"); + sizeSlider.addChangeListener(new ChangeListener() { + + public void stateChanged(ChangeEvent arg0) { + if(!sizeSlider.getModel().getValueIsAdjusting() && !brushAdjusting && client.getConnection().getUser() != null && client.getConnection().getUser().getBrush() != null) { + client.getConnection().getUser().getBrush().setRadius(sizeSlider.getValue() / 8f); + client.getConnection().getSendQueue().offer(new CommandEntry(0, new SetBrushCommand(client.getConnection().getUser().getBrush()))); + } + } + }); + + /** OPACITY */ + brushTab.add(new JLabel(loadImageIcon("opacity.png")), "1 2 r c"); + opacitySlider = new JSlider(-255, 255, 0); + opacitySlider.setMajorTickSpacing(255); + opacitySlider.setPaintTicks(true); + opacitySlider.setToolTipText("Opacity - controls the opacity of the brush."); + brushTab.add(opacitySlider, "2 2 2 2"); + opacitySlider.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent arg0) { + if(!opacitySlider.getModel().getValueIsAdjusting() && !brushAdjusting && client.getConnection().getUser() != null && client.getConnection().getUser().getBrush() != null) { + Brush b = client.getConnection().getUser().getBrush(); + b.setOpacity(opacitySlider.getValue()); + client.getConnection().getSendQueue().offer(new CommandEntry(0, new SetBrushCommand(client.getConnection().getUser().getBrush()))); + } + } + }); + + /** FLOW */ + brushTab.add(new JLabel(loadImageIcon("flow.png")), "1 3 r c"); + flowSlider = new JSlider(0, 255, 0); + flowSlider.setToolTipText("Flow - controls the flow of the brush."); + brushTab.add(flowSlider, "2 3 2 3"); + flowSlider.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent arg0) { + if(!flowSlider.getModel().getValueIsAdjusting() && !brushAdjusting && client.getConnection().getUser() != null && client.getConnection().getUser().getBrush() != null) { + client.getConnection().getUser().getBrush().setFlow(flowSlider.getValue()); + client.getConnection().getSendQueue().offer(new CommandEntry(0, new SetBrushCommand(client.getConnection().getUser().getBrush()))); + } + } + }); + + /** SPACING */ + brushTab.add(new JLabel(loadImageIcon("spacing.png")), "1 4 r c"); + spacingSlider = new JSlider(4, 100, 4); + spacingSlider.setToolTipText("Spacing - controls the density of the brush."); + brushTab.add(spacingSlider, "2 4 2 4"); + spacingSlider.addChangeListener(new ChangeListener() { + + public void stateChanged(ChangeEvent arg0) { + if(!spacingSlider.getModel().getValueIsAdjusting() && !brushAdjusting && client.getConnection().getUser() != null && client.getConnection().getUser().getBrush() != null) { + client.getConnection().getUser().getBrush().setSpacing(spacingSlider.getValue() / 100f); + client.getConnection().getSendQueue().offer(new CommandEntry(0, new SetBrushCommand(client.getConnection().getUser().getBrush()))); + } + } + + }); + + /** JITTER */ + brushTab.add(new JLabel(loadImageIcon("jitter.png")), "1 5 r c"); + jitterSlider = new JSlider(0, 400, 0); + jitterSlider.setToolTipText("Jitter - adds unpredictability to the position of the brush."); + brushTab.add(jitterSlider, "2 5 2 5"); + jitterSlider.addChangeListener(new ChangeListener() { + + public void stateChanged(ChangeEvent arg0) { + if(!jitterSlider.getModel().getValueIsAdjusting() && !brushAdjusting && client.getConnection().getUser() != null && client.getConnection().getUser().getBrush() != null) { + client.getConnection().getUser().getBrush().setJitter(jitterSlider.getValue() / 100f); + client.getConnection().getSendQueue().offer(new CommandEntry(0, new SetBrushCommand(client.getConnection().getUser().getBrush()))); + } + } + + }); + + /** NOISE */ + brushTab.add(new JLabel(loadImageIcon("noise.png")), "1 6 r c"); + noiseSlider = new JSlider(0, 255, 0); + noiseSlider.setToolTipText("Noise - adds grains to the brush."); + brushTab.add(noiseSlider, "2 6 2 6"); + noiseSlider.addChangeListener(new ChangeListener() { + + public void stateChanged(ChangeEvent arg0) { + if(!noiseSlider.getModel().getValueIsAdjusting() && !brushAdjusting && client.getConnection().getUser() != null && client.getConnection().getUser().getBrush() != null) { + client.getConnection().getUser().getBrush().setNoise(noiseSlider.getValue()); + client.getConnection().getSendQueue().offer(new CommandEntry(0, new SetBrushCommand(client.getConnection().getUser().getBrush()))); + } + } + + }); + + /** WATER */ + brushTab.add(new JLabel(loadImageIcon("water.png")), "1 7 r c"); + waterSlider = new JSlider(0, 255, 0); + waterSlider.setToolTipText("Watercolor - amount of color that is sampled and mixed with the current color."); + brushTab.add(waterSlider, "2 7 2 7"); + waterSlider.addChangeListener(new ChangeListener() { + + public void stateChanged(ChangeEvent arg0) { + if(!waterSlider.getModel().getValueIsAdjusting() && !brushAdjusting && client.getConnection().getUser() != null && client.getConnection().getUser().getBrush() != null) { + client.getConnection().getUser().getBrush().setWater(waterSlider.getValue()); + client.getConnection().getSendQueue().offer(new CommandEntry(0, new SetBrushCommand(client.getConnection().getUser().getBrush()))); + } + } + + }); + + /** WATER AREA */ + brushTab.add(new JLabel(loadImageIcon("waterarea.png")), "1 8 r c"); + waterAreaSlider = new JSlider(1, 100, 75); + waterAreaSlider.setToolTipText("Watercolor Pickup Area - size of color sampling area compared to the brush size."); + brushTab.add(waterAreaSlider, "2 8 2 8"); + waterAreaSlider.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent arg0) { + if(!waterSlider.getModel().getValueIsAdjusting() && !brushAdjusting && client.getConnection().getUser() != null && client.getConnection().getUser().getBrush() != null) { + client.getConnection().getUser().getBrush().setWaterArea(waterAreaSlider.getValue() / 100f); + client.getConnection().getSendQueue().offer(new CommandEntry(0, new SetBrushCommand(client.getConnection().getUser().getBrush()))); + } + } + }); + + /** Pressure-to-Radius box */ + radiusBox = new JCheckBox(); + radiusBox.setToolTipText("Pressure to Size - vary brush size with pen pressure"); + radiusBox.setEnabled(false); + brushTab.add(radiusBox, "3 1 c c"); + + radiusBox.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent arg0) { + if(!brushAdjusting && client.getConnection().getUser() != null && client.getConnection().getUser().getBrush() != null) { + client.getConnection().getUser().getBrush().setPressureToRadius(radiusBox.isSelected()); + client.getConnection().getSendQueue().offer(new CommandEntry(0, new SetBrushCommand(client.getConnection().getUser().getBrush()))); + } + } + }); + + /** Pressure-to-Opacity box */ + opacityBox = new JCheckBox(); + opacityBox.setToolTipText("Pressure to Opacity - vary brush opacity with pen pressure"); + opacityBox.setEnabled(false); + brushTab.add(opacityBox, "3 2 c c"); + + opacityBox.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent arg0) { + if(!brushAdjusting && client.getConnection().getUser() != null && client.getConnection().getUser().getBrush() != null) { + client.getConnection().getUser().getBrush().setPressureToOpacity(opacityBox.isSelected()); + client.getConnection().getSendQueue().offer(new CommandEntry(0, new SetBrushCommand(client.getConnection().getUser().getBrush()))); + } + } + + }); + + /** Pressure-to-Flow box */ + flowBox = new JCheckBox(); + flowBox.setToolTipText("Pressure to Flow - vary brush flow with pen pressure"); + opacityBox.setEnabled(false); + flowBox.setEnabled(false); + brushTab.add(flowBox, "3 3 c c"); + + flowBox.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent arg0) { + if(!brushAdjusting && client.getConnection().getUser() != null && client.getConnection().getUser().getBrush() != null) { + client.getConnection().getUser().getBrush().setPressureToFlow(flowBox.isSelected()); + client.getConnection().getSendQueue().offer(new CommandEntry(0, new SetBrushCommand(client.getConnection().getUser().getBrush()))); + } + } + + }); + + /** Lock Transparency box */ + lockTransBox = new JCheckBox(); + lockTransBox.setToolTipText("Lock Transparency - alter colors while simultaneously preserving underlying shapes"); + brushTab.add(lockTransBox, "3 0 c c"); + + lockTransBox.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent arg0) { + if(!brushAdjusting && client.getConnection().getUser() != null && client.getConnection().getUser().getBrush() != null) { + Brush b = client.getConnection().getUser().getBrush(); + b.setLockTransparency(lockTransBox.isSelected()); + Layer l = client.getConnection().getUser().getPhantomLayer(); + if(lockTransBox.isSelected()) { + l.setAlphaRule(AlphaComposite.SRC_ATOP); + } else if(b.getOpacity() >= 0) { + l.setAlphaRule(AlphaComposite.SRC_OVER); + } else { + l.setAlphaRule(AlphaComposite.DST_OUT); + } + client.getConnection().getSendQueue().offer(new CommandEntry(0, new SetBrushCommand(client.getConnection().getUser().getBrush()))); + } + } + }); + + /* Layers tab */ + final JCollapsiblePanel layersPanel = new JCollapsiblePanel(400, "Layers", true, true); + layersPanel.setGroup(mainGroup); + JPanel layersTab = layersPanel.getContentPane(); + + TableLayout layersTabLayout = new TableLayout(new double[][] {{-1, -2}, {-2, -2, -1}}); + layersTabLayout.setHGap(4); + layersTabLayout.setVGap(4); + layersTab.setLayout(layersTabLayout); + rightPanel.add(layersPanel); + + layerList = new JList(); + + layersTab.add(layerList, "0 0 0 2"); + + JButton addLayerButton = new JButton("Add"); + addLayerButton.setToolTipText("Adds a new layer to the canvas. Prompts you for a name first. Layers are visible to all users."); + addLayerButton.setMargin(NO_INSETS); + layersTab.add(addLayerButton, "1 0"); + addLayerButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent arg0) { + String layerName = JOptionPane.showInputDialog("Name of new layer:"); + if(layerName == null) { + return; + } + + layerName = layerName.trim(); + + if(layerName.length() == 0) { + return; + } + + client.getConnection().getSendQueue().offer(new CommandEntry(0, new CreateLayerCommand(0, LayerType.BITMAP, layerName))); + } + + }); + + JButton remLayerButton = new JButton("Remove"); + remLayerButton.setToolTipText("Removes the currently selected layer from the canvas. Prompts you for confirmation first."); + remLayerButton.setMargin(NO_INSETS); + layersTab.add(remLayerButton, "1 1"); + remLayerButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent arg0) { + if(client.getConnection().getUser() == null || client.getConnection().getUser().getLayer() == null) { + return; + } + + Layer layer = client.getConnection().getUser().getLayer(); + + int confirm = JOptionPane.showConfirmDialog(UserInterface.this, "Really remove layer " + layer.getName() + "?", "Confirm", JOptionPane.YES_NO_OPTION); + if(confirm != JOptionPane.OK_OPTION) { + return; + } + + if(!layer.isEmpty()) { + confirm = JOptionPane.showConfirmDialog(UserInterface.this, "Layer " + layer.getName() + " is not empty! Are you absolutely sure?", "Confirm", JOptionPane.YES_NO_OPTION); + if(confirm != JOptionPane.OK_OPTION) { + return; + } + } + + client.getConnection().getSendQueue().offer(new CommandEntry(0, new DeleteLayerCommand(client.getCanvas().getLayerMap().getKeyForValue(layer)))); + } + }); + + /** Color tab */ + colorPanel = new JCollapsiblePanel(500, "Color", false, false); + colorPanel.setGroup(mainGroup); + JPanel colorTab = colorPanel.getContentPane(); + colorTab.setLayout(new BorderLayout()); + rightPanel.add(colorPanel); + + /** Color space tabs */ + JTabbedPane colorSpaceTabs = new JTabbedPane(JTabbedPane.TOP); + colorTab.add(colorSpaceTabs, BorderLayout.CENTER); + + /** RGB tab */ + JPanel rgbTab = new JPanel(); + colorSpaceTabs.addTab(null, loadImageIcon("rgb.png"), rgbTab, "RGB - picks colors with red, green, and blue sliders."); + rgbTab.setLayout(new TableLayout(new double[][] {{TableLayout.PREFERRED, TableLayout.FILL}, {TableLayout.PREFERRED, TableLayout.PREFERRED, TableLayout.PREFERRED, TableLayout.FILL}})); + + ChangeListener rgbChangeListener = new ChangeListener() { + public void stateChanged(ChangeEvent arg0) { + if(!colorAdjusting) { + int color = Pixels.pack(redSlider.getValue(), greenSlider.getValue(), blueSlider.getValue()) & 0xFFFFFF; + if(!((JSlider) arg0.getSource()).getModel().getValueIsAdjusting()) { + client.getCommandQueue().offer(new CommandEntry(0, new SetColorCommand(color))); + setCMYSliders(color); + setHSBSliders(color); + } + rgbColor.setBackground(new Color(color)); + } + } + }; + + rgbTab.add(new JLabel(loadImageIcon("red.png")), "0 0 r c"); + redSlider = new JSlider(0, 255, 0); + redSlider.setToolTipText("Red"); + rgbTab.add(redSlider, "1 0 1 0"); + redSlider.addChangeListener(rgbChangeListener); + + rgbTab.add(new JLabel(loadImageIcon("green.png")), "0 1 r c"); + greenSlider = new JSlider(0, 255, 0); + greenSlider.setToolTipText("Green"); + rgbTab.add(greenSlider, "1 1 1 1"); + greenSlider.addChangeListener(rgbChangeListener); + + rgbTab.add(new JLabel(loadImageIcon("blue.png")), "0 2 r c"); + blueSlider = new JSlider(0, 255, 0); + blueSlider.setToolTipText("Blue"); + rgbTab.add(blueSlider, "1 2 1 2"); + blueSlider.addChangeListener(rgbChangeListener); + + rgbColor = new JPanel(); + rgbColor.setBackground(Color.BLACK); + rgbTab.add(rgbColor, "0 3 1 3"); + + /** CMY tab */ + JPanel cmyTab = new JPanel(); + cmyTab.setLayout(new TableLayout(new double[][] {{TableLayout.PREFERRED, TableLayout.FILL}, {TableLayout.PREFERRED, TableLayout.PREFERRED, TableLayout.PREFERRED, TableLayout.FILL}})); + colorSpaceTabs.addTab(null, loadImageIcon("cmy.png"), cmyTab, "CMY - picks colors with cyan, magenta and yellow sliders."); + + ChangeListener cmyChangeListener = new ChangeListener() { + public void stateChanged(ChangeEvent arg0) { + if(!colorAdjusting) { + int color = Pixels.pack(255 - cyanSlider.getValue(), 255 - magentaSlider.getValue(), 255 - yellowSlider.getValue()) & 0xFFFFFF; + if(!((JSlider) arg0.getSource()).getModel().getValueIsAdjusting()) { + client.getCommandQueue().offer(new CommandEntry(0, new SetColorCommand(color))); + setRGBSliders(color); + setHSBSliders(color); + } + cmyColor.setBackground(new Color(color)); + } + } + }; + + cmyTab.add(new JLabel(loadImageIcon("cyan.png")), "0 0 r c"); + cyanSlider = new JSlider(0, 255, 255); + cyanSlider.setToolTipText("Cyan"); + cmyTab.add(cyanSlider, "1 0 1 0"); + cyanSlider.addChangeListener(cmyChangeListener); + + cmyTab.add(new JLabel(loadImageIcon("magenta.png")), "0 1 r c"); + magentaSlider = new JSlider(0, 255, 255); + magentaSlider.setToolTipText("Magenta"); + cmyTab.add(magentaSlider, "1 1 1 1"); + magentaSlider.addChangeListener(cmyChangeListener); + + cmyTab.add(new JLabel(loadImageIcon("yellow.png")), "0 2 r c"); + yellowSlider = new JSlider(0, 255, 255); + yellowSlider.setToolTipText("Yellow"); + cmyTab.add(yellowSlider, "1 2 1 2"); + yellowSlider.addChangeListener(cmyChangeListener); + + cmyColor = new JPanel(); + cmyColor.setBackground(Color.BLACK); + cmyTab.add(cmyColor, "0 3 1 3"); + + /** HSB tab */ + JPanel hsbTab = new JPanel(); + hsbTab.setLayout(new TableLayout(new double[][] {{TableLayout.PREFERRED, TableLayout.FILL}, {TableLayout.PREFERRED, TableLayout.PREFERRED, TableLayout.PREFERRED, TableLayout.FILL}})); + colorSpaceTabs.addTab(null, loadImageIcon("hsb.png"), hsbTab, "HSB - picks colors with hue, saturation, and brightness sliders."); + + /** HSB change listener */ + ChangeListener hsbChangeListener = new ChangeListener() { + public void stateChanged(ChangeEvent arg0) { + if(!colorAdjusting) { + int color = Color.HSBtoRGB(hueSlider.getValue() / 256f, satSlider.getValue() / 256f, brightSlider.getValue() / 256f) & 0xFFFFFF; + if(!((JSlider) arg0.getSource()).getModel().getValueIsAdjusting()) { + client.getCommandQueue().offer(new CommandEntry(0, new SetColorCommand(color))); + setRGBSliders(color); + setCMYSliders(color); + } + hsbColor.setBackground(new Color(color)); + } + } + }; + + hsbTab.add(new JLabel(new ImageIcon(getClass().getResource("images/hue.png"))), "0 0 r c"); + hueSlider = new JSlider(0, 256, 0); + hueSlider.setToolTipText("Hue - basic color"); + hsbTab.add(hueSlider, "1 0 1 0"); + hueSlider.addChangeListener(hsbChangeListener); + + hsbTab.add(new JLabel(new ImageIcon(getClass().getResource( + "images/saturation.png"))), "0 1 r c"); + satSlider = new JSlider(0, 256, 0); + satSlider.setToolTipText("Saturation - color amount"); + hsbTab.add(satSlider, "1 1 1 1"); + satSlider.addChangeListener(hsbChangeListener); + + hsbTab.add(new JLabel(new ImageIcon(getClass().getResource( + "images/brightness.png"))), "0 2 r c"); + brightSlider = new JSlider(0, 256, 0); + brightSlider.setToolTipText("Brightness"); + hsbTab.add(brightSlider, "1 2 1 2"); + brightSlider.addChangeListener(hsbChangeListener); + + hsbColor = new JPanel(); + hsbColor.setBackground(Color.BLACK); + hsbTab.add(hsbColor, "0 3 1 3"); + + inputPanel = new JCollapsiblePanel(600, "Input", false, false); + inputPanel.setGroup(mainGroup); + rightPanel.add(inputPanel); + JPanel smoothPanelContent = inputPanel.getContentPane(); + smoothPanelContent.setLayout(new TableLayout(new double[][] {{TableLayout.PREFERRED, TableLayout.FILL}, {TableLayout.PREFERRED, TableLayout.PREFERRED, TableLayout.PREFERRED, TableLayout.PREFERRED}})); + + JLabel jtabletLabel = new JLabel(); + jtabletLabel.setBackground(Color.BLUE); + jtabletLabel.setHorizontalAlignment(SwingConstants.CENTER); + if(editor.hasJTablet()) { + jtabletLabel.setText("JTablet detected!"); + jtabletLabel.setForeground(new Color(0, 160, 0)); + } else { + jtabletLabel.setText("JTablet NOT detected!"); + jtabletLabel.setForeground(Color.RED); + } + smoothPanelContent.add(jtabletLabel, "0 0 1 0"); + + smoothPanelContent.add(new JLabel(loadImageIcon("trailing.png")), "0 1 r c"); + final JSlider smoothSlider = new JSlider(0, 100, 15); + smoothSlider.setToolTipText("Cursor Trailing - aids the unsteady hand in drawing sweeping curves."); + smoothPanelContent.add(smoothSlider, "1 1"); + smoothSlider.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent arg0) { + if(!smoothSlider.getModel().getValueIsAdjusting()) { + Smoother.setSmoothness(smoothSlider.getValue() / 10f); + } + } + }); + + final JCheckBox curveSmoothBox = new JCheckBox("Curve Interpolation"); + curveSmoothBox.setToolTipText("Analyzes cursor motion and smooths segmented lines into curves."); + curveSmoothBox.setSelected(true); + smoothPanelContent.add(curveSmoothBox, "0 2 1 2"); + + curveSmoothBox.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent arg0) { + Smoother.setEnabled(curveSmoothBox.isSelected()); + } + }); + + /** Create user list panel */ + JCollapsiblePanel userListPanel = new JCollapsiblePanel(700, "Users", true, true); + userListPanel.setGroup(mainGroup); + final JPanel userListContent = userListPanel.getContentPane(); + userListContent.setLayout(new BorderLayout()); + rightPanel.add(userListPanel); + + userList = new JUserList(client.getUserMap()); + userListContent.add(userList, BorderLayout.CENTER); + + /** Create chat panel */ + JCollapsiblePanel chatPanel = new JCollapsiblePanel(800, "Chat", true, true); + chatPanel.setGroup(mainGroup); + JPanel chatContent = chatPanel.getContentPane(); + chatContent.setLayout(new BorderLayout()); + rightPanel.add(chatPanel); + + /** Create chat scroll pane, add to chat panel */ + chatPane = new JScrollPane(); + ComponentListener chatPaneListener = new ComponentListener() { + public void componentHidden(ComponentEvent arg0) { + } + + public void componentMoved(ComponentEvent arg0) { + } + + public void componentResized(ComponentEvent arg0) { + scroll(); + } + + public void componentShown(ComponentEvent arg0) { + } + }; + chatPane.addComponentListener(chatPaneListener); + chatContent.add(chatPane, BorderLayout.CENTER); + + /** Create chat log, add to chat pane */ + chatLog = new JTextArea(); + chatPane.setViewportView(chatLog); + + chatLog.setCursor(new Cursor(Cursor.TEXT_CURSOR)); + chatLog.setEditable(false); + chatLog.setLineWrap(true); + chatLog.setWrapStyleWord(true); + chatLog.getCaret().setVisible(false); + chatLog.setText("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"); + + /** Create chat field, add to chat panel */ + chatField = new JTextField(); + chatContent.add(chatField, BorderLayout.SOUTH); + chatField.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent arg0) { + String input = chatField.getText(); + + if(client.getConnection().getUser() == null) { + return; + } + + String username = client.getConnection().getUser() == null ? null : client.getConnection().getUser().getName(); + if(input.startsWith("*") && input.endsWith("*")) { + client.getConnection().getSendQueue().offer(new CommandEntry(0, new SayCommand(" " + input.substring(1, input.length() - 1), true))); + } else if(input.startsWith(username)) { + client.getConnection().getSendQueue().offer(new CommandEntry(0, new SayCommand(input.substring(username.length()), true))); + } else if(input.startsWith("/me")) { + client.getConnection().getSendQueue().offer(new CommandEntry(0, new SayCommand(input.substring(3), true))); + } else if(input.startsWith("/kick ")) { + String kickUsername = input.substring(6); + boolean kicked = false; + for(Map.Entry e: client.getUserMap().entrySet()) { + if(kickUsername.equalsIgnoreCase(e.getValue().getName())) { + client.getConnection().getSendQueue().offer(new CommandEntry(0, new KickCommand(e.getKey(), "Kicked by " + username))); + kicked = true; + break; + } + } + + if(!kicked) { + println("The user you are trying to kick doesn't exist."); + } + } else if("/brush".equals(input)) { + Brush brush = client.getConnection().getUser().getBrush(); + if(brush != null) { + println("/*"); + println(" Hardness: " + brush.getHardness()); + println(" Size: " + brush.getRadius()); + println(" Opacity: " + brush.getFlow()); + println(" Spacing: " + brush.getSpacing()); + println(" Jitter: " + brush.getJitter()); + println(" Noise: " + brush.getNoise()); + println(" Water: " + brush.getWater()); + println(" WaterArea: " + brush.getWaterArea()); + println(" PressuteToOpacity: " + brush.isPressureToOpacity()); + println(" PressureToFlow: " + brush.isPressureToFlow()); + println(" PressureToSize: " + brush.isPressureToRadius()); + println("*/"); + } + } else if(!input.trim().isEmpty()) { + client.getConnection().getSendQueue().offer(new CommandEntry(0, new SayCommand(input, false))); + } + + chatField.setText(null); + } + + }); + + /** Adjust size of editor */ + editor.adjust(); + + if(editor.hasJTablet()) { + opacityBox.setEnabled(true); + flowBox.setEnabled(true); + radiusBox.setEnabled(true); + } + + validate(); + + scroll(); + } + + ImageIcon loadImageIcon(String filename) { + return new ImageIcon(getClass().getResource("images/" + filename)); + } + + private void updatePanels() { + boolean hasBrush = freehandButton.isSelected() || lineButton.isSelected() || bezierButton.isSelected() || rectangleButton.isSelected() || ovalButton.isSelected(); + brushPanel.setVisible(hasBrush); + brushSettingsPanel.setVisible(hasBrush); + colorPanel.setVisible(hasBrush); + inputPanel.setVisible(hasBrush); + filterMenu.setEnabled(selectButton.isSelected()); + } + + public void updateBrushSliders() { + if(client.getConnection() == null || client.getConnection().getUser() == null || client.getConnection().getUser().getBrush() == null) { + return; + } + + Brush brush = client.getConnection().getUser().getBrush(); + + brushAdjusting = true; + + for(int i = 0; i < client.brushes.length; i++) { + brushButtons[i].setSelected(brush == client.brushes[i]); + } + + hardnessSlider.setValue(brush.getHardness()); + sizeSlider.setValue(Math.round(brush.getRadius() * 8f)); + opacitySlider.setValue(brush.getOpacity()); + flowSlider.setValue(brush.getFlow()); + spacingSlider.setValue(Math.round(brush.getSpacing() * 100f)); + jitterSlider.setValue(Math.round(brush.getJitter() * 100f)); + noiseSlider.setValue(brush.getNoise()); + waterSlider.setValue(brush.getWater()); + waterAreaSlider.setValue(Math.round(brush.getWaterArea() * 100f)); + opacityBox.setSelected(brush.isPressureToOpacity()); + flowBox.setSelected(brush.isPressureToFlow()); + radiusBox.setSelected(brush.isPressureToRadius()); + lockTransBox.setSelected(brush.isLockTransparency()); + + brushAdjusting = false; + } + + public void setColorSliders(int color) { + setRGBSliders(color); + setCMYSliders(color); + setHSBSliders(color); + } + + public void setRGBSliders(int color) { + rgbColor.setBackground(new Color(color)); + + colorAdjusting = true; + redSlider.setValue(Pixels.getChannel1(color)); + greenSlider.setValue(Pixels.getChannel2(color)); + blueSlider.setValue(Pixels.getChannel3(color)); + colorAdjusting = false; + } + + public void setHSBSliders(int color) { + hsbColor.setBackground(new Color(color)); + + float[] hsb = new float[3]; + Color.RGBtoHSB(Pixels.getChannel1(color), Pixels.getChannel2(color), Pixels.getChannel3(color), hsb); + + hsb[0] *= 256f; + hsb[1] *= 256f; + hsb[2] *= 256f; + + colorAdjusting = true; + hueSlider.setValue(Math.round(hsb[0])); + satSlider.setValue(Math.round(hsb[1])); + brightSlider.setValue(Math.round(hsb[2])); + colorAdjusting = false; + } + + public void setCMYSliders(int color) { + cmyColor.setBackground(new Color(color)); + + colorAdjusting = true; + cyanSlider.setValue(255 - Pixels.getChannel1(color)); + magentaSlider.setValue(255 - Pixels.getChannel2(color)); + yellowSlider.setValue(255 - Pixels.getChannel3(color)); + colorAdjusting = false; + } + + public void println(String str) { + print("\n" + str); + } + + public void print(final String str) { + Runnable swingWork = new Runnable() { + public void run() { + JScrollBar scrollBar = chatPane.getVerticalScrollBar(); + BoundedRangeModel scrollModel = scrollBar.getModel(); + boolean scroll = !scrollModel.getValueIsAdjusting() && scrollModel.getValue() == scrollModel.getMaximum() - scrollModel.getExtent(); + + chatLog.append(str); + + if(scroll) { + scroll(); + } + } + }; + + SwingUtilities.invokeLater(swingWork); + } + + public void scroll() { + chatPane.validate(); + BoundedRangeModel model = chatPane.getVerticalScrollBar().getModel(); + model.setValue(model.getMaximum() - model.getExtent()); + } + + public void setCanvas(final Canvas canvas) { + editor.setCanvas(canvas); + + canvas.getLayerMap().addChangeListener(new ChangeListener() { + @SuppressWarnings("unchecked") + public void stateChanged(ChangeEvent e) { + TwoWayHashMap map = (TwoWayHashMap)e.getSource(); + final Object[] array = map.values().toArray(); + + ListModel model = new AbstractListModel() { + public int getSize() { + return array.length; + } + + public Object getElementAt(int index) { + Layer l = (Layer)array[array.length - 1 - index]; + return l.getName(); + } + }; + + layerList.setModel(model); + layerList.invalidate(); + } + }); + + layerList.addListSelectionListener(new ListSelectionListener() { + public void valueChanged(ListSelectionEvent e) { + if(layerAdjusting) { + return; + } + JList source = (JList)e.getSource(); + Object[] array = canvas.getLayerMap().keySet().toArray(); + if(source.getSelectedIndex() != -1) { + client.getCommandQueue().offer(new CommandEntry(0, new SetLayerCommand((Integer)array[array.length - 1 - source.getSelectedIndex()]))); + } + } + }); + } + + public Editor getEditor() { + return editor; + } + + public JScrollPane getEditorPane() { + return editorPane; + } + + public void setSmoothZoom(boolean smoothZoom) { + this.smoothZoom = smoothZoom; + smoothBox.setSelected(smoothZoom); + editor.setSmoothZoom(smoothZoom); + editor.repaint(); + } + + public boolean isSmoothZoom() { + return smoothZoom; + } + + public void setSoundItem(JCheckBoxMenuItem soundItem) { + this.soundItem = soundItem; + } + + public JCheckBoxMenuItem getSoundItem() { + return soundItem; + } + + public void setTagsItem(JCheckBoxMenuItem tagsItem) { + this.tagsItem = tagsItem; + } + + public JCheckBoxMenuItem getTagsItem() { + return tagsItem; + } + + public Set getToolPanels() { + return mainGroup; + } + + public JUserList getUserList() { + return userList; + } + + public void updateLayer() { + Layer currentLayer = client.getConnection().getUser().getLayer(); + Set> es = client.getCanvas().getLayerMap().entrySet(); + int index = es.size() - 1; + for(Map.Entry e : es) { + if(e.getValue() == currentLayer) { + break; + } + --index; + } + layerAdjusting = true; + layerList.setSelectedIndex(index); + layerAdjusting = false; + } + + public BrushButton[] getBrushButtons() { + return brushButtons; + } + + public class BrushButton extends JToggleButton { + public BrushButton(final UserInterface parent, final Brush[] brushArray, final Icon[] brushIconArray, final int brushIndex) { + super(); + setIcon(brushIconArray[brushIndex]); + setToolTipText(brushArray[brushIndex].getName()); + setMargin(UserInterface.NO_INSETS); + setMinimumSize(new Dimension(32, 32)); + setPreferredSize(new Dimension(32, 32)); + + addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent arg0) { + if(!parent.brushAdjusting) { + parent.client.getCommandQueue().offer(new CommandEntry(0, new SetBrushCommand(brushArray[brushIndex]))); + } + } + }); + } + } +} \ No newline at end of file diff --git a/src/com/jotuntech/sketcher/client/audio/chat.wav b/src/com/jotuntech/sketcher/client/audio/chat.wav new file mode 100755 index 0000000..e4272c2 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/audio/chat.wav differ diff --git a/src/com/jotuntech/sketcher/client/audio/intro.wav b/src/com/jotuntech/sketcher/client/audio/intro.wav new file mode 100755 index 0000000..8667eb2 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/audio/intro.wav differ diff --git a/src/com/jotuntech/sketcher/client/audio/kick.wav b/src/com/jotuntech/sketcher/client/audio/kick.wav new file mode 100755 index 0000000..ef37466 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/audio/kick.wav differ diff --git a/src/com/jotuntech/sketcher/client/audio/outtro.wav b/src/com/jotuntech/sketcher/client/audio/outtro.wav new file mode 100755 index 0000000..2d9648f Binary files /dev/null and b/src/com/jotuntech/sketcher/client/audio/outtro.wav differ diff --git a/src/com/jotuntech/sketcher/client/audio/query.wav b/src/com/jotuntech/sketcher/client/audio/query.wav new file mode 100755 index 0000000..d1587af Binary files /dev/null and b/src/com/jotuntech/sketcher/client/audio/query.wav differ diff --git a/src/com/jotuntech/sketcher/client/audio/signin.wav b/src/com/jotuntech/sketcher/client/audio/signin.wav new file mode 100755 index 0000000..3e89b0d Binary files /dev/null and b/src/com/jotuntech/sketcher/client/audio/signin.wav differ diff --git a/src/com/jotuntech/sketcher/client/audio/signout.wav b/src/com/jotuntech/sketcher/client/audio/signout.wav new file mode 100755 index 0000000..5698fdf Binary files /dev/null and b/src/com/jotuntech/sketcher/client/audio/signout.wav differ diff --git a/src/com/jotuntech/sketcher/client/command/CanvasCommand.java b/src/com/jotuntech/sketcher/client/command/CanvasCommand.java new file mode 100755 index 0000000..69329b9 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/command/CanvasCommand.java @@ -0,0 +1,56 @@ +package com.jotuntech.sketcher.client.command; + +import java.lang.reflect.InvocationTargetException; +import java.nio.ByteBuffer; + +import javax.swing.SwingUtilities; + +import com.jotuntech.sketcher.client.Client; +import com.jotuntech.sketcher.client.Command; +import com.jotuntech.sketcher.client.Connection; +import com.jotuntech.sketcher.common.Canvas; +import com.jotuntech.sketcher.common.Log; +import com.jotuntech.sketcher.common.User; + +public class CanvasCommand implements Command { + protected int width, height; + + public CanvasCommand() { + + } + + public CanvasCommand(int width, int height) { + this.width = width; + this.height = height; + } + + public int perform(final Client client, User user) { + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + /** Create and set new canvas */ + client.setCanvas(new Canvas(width, height)); + Log.info("Created canvas."); + } + }); + } catch (InterruptedException e) { + Log.error(e); + } catch (InvocationTargetException e) { + Log.error(e); + } + + /** Return silently */ + return Connection.SEND_NONE; + } + + public void decode(ByteBuffer in) { + width = in.getInt(); + height = in.getInt(); + } + + public void encode(ByteBuffer out) { + out.putInt(width); + out.putInt(height); + } + +} diff --git a/src/com/jotuntech/sketcher/client/command/CreateLayerCommand.java b/src/com/jotuntech/sketcher/client/command/CreateLayerCommand.java new file mode 100755 index 0000000..b636cae --- /dev/null +++ b/src/com/jotuntech/sketcher/client/command/CreateLayerCommand.java @@ -0,0 +1,56 @@ +package com.jotuntech.sketcher.client.command; + +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.client.Client; +import com.jotuntech.sketcher.client.Command; +import com.jotuntech.sketcher.client.Connection; +import com.jotuntech.sketcher.common.BitmapLayer; +import com.jotuntech.sketcher.common.Layer; +import com.jotuntech.sketcher.common.User; + +public class CreateLayerCommand implements Command { + int layerKey; + int type; + String name; + + public CreateLayerCommand() { + + } + + public CreateLayerCommand(int layerKey, int type, String name) { + this.layerKey = layerKey; + this.type = type; + this.name = name; + } + + public int perform(Client client, User user) { + Layer l = new BitmapLayer(name); + client.getCanvas().getLayerMap().put(layerKey, l); + + if(user != null) { + client.getUserInterface().println(user.getName() + " created layer " + l.getName()); + } + + return Connection.SEND_ALL; + } + + public void decode(ByteBuffer in) { + layerKey = in.getInt(); + type = in.get(); + StringBuffer nameBuffer = new StringBuffer(); + while(in.remaining() >= 2) { + nameBuffer.append(in.getChar()); + } + name = nameBuffer.toString(); + } + + public void encode(ByteBuffer out) { + out.putInt(layerKey); + out.put((byte) type); + for(int i = 0; i < name.length(); i++) { + out.putChar(name.charAt(i)); + } + } + +} diff --git a/src/com/jotuntech/sketcher/client/command/CursorCommand.java b/src/com/jotuntech/sketcher/client/command/CursorCommand.java new file mode 100755 index 0000000..6962c08 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/command/CursorCommand.java @@ -0,0 +1,56 @@ + +package com.jotuntech.sketcher.client.command; + +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.client.Client; +import com.jotuntech.sketcher.client.Command; +import com.jotuntech.sketcher.client.Connection; +import com.jotuntech.sketcher.common.Input; +import com.jotuntech.sketcher.common.User; + +/** Cursor command. + * @author Thor Harald Johansen + * + */ +public class CursorCommand implements Command { + Input input; + + /** Constructs an Cursor command. */ + public CursorCommand() { + + } + + /** Constructs an Cursor command. + * @param user Source user. + * @param input Cursor position */ + public CursorCommand(Input input) { + this.input = input; + } + + public int perform(Client client, User user) { + if(user == null) { + /** Signed in users only. */ + return Connection.SEND_NONE; + } + + if(user.isViewer()) { + return Connection.SEND_NONE; + } + + user.setCursor(input); + + /** Return and broadcast. */ + return Connection.SEND_OTHERS; + } + + public void decode(ByteBuffer in) { + input = new Input(); + input.decode(in); + } + + public void encode(ByteBuffer out) { + input.encode(out); + } +} + diff --git a/src/com/jotuntech/sketcher/client/command/DeleteLayerCommand.java b/src/com/jotuntech/sketcher/client/command/DeleteLayerCommand.java new file mode 100755 index 0000000..23c2732 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/command/DeleteLayerCommand.java @@ -0,0 +1,74 @@ +package com.jotuntech.sketcher.client.command; + +import java.awt.image.ImageObserver; +import java.nio.ByteBuffer; +import java.util.HashSet; +import java.util.Set; + +import com.jotuntech.sketcher.client.Client; +import com.jotuntech.sketcher.client.Command; +import com.jotuntech.sketcher.client.Connection; +import com.jotuntech.sketcher.common.Canvas; +import com.jotuntech.sketcher.common.Layer; +import com.jotuntech.sketcher.common.UndoEntry; +import com.jotuntech.sketcher.common.User; + +public class DeleteLayerCommand implements Command { + int layerKey; + + public DeleteLayerCommand() { + + } + + public DeleteLayerCommand(int layerKey) { + this.layerKey = layerKey; + } + + public int perform(Client client, User user) { + if(user == null) { + return Connection.SEND_NONE; + } + + if(user.isViewer()) { + return Connection.SEND_NONE; + } + + Canvas canvas = client.getCanvas(); + Layer layer = canvas.getLayerMap().remove(layerKey); + + if(layer == null) { + return Connection.SEND_NONE; + } + + // Remove references to layer from undo deque + for(User u: client.getUserMap().values()) { + Set uers = new HashSet(); + for(UndoEntry ue : u.getUndoDeque()) { + if(ue.getLayer() == layer) { + uers.add(ue); + } + } + for(UndoEntry ue : uers) { + u.getUndoDeque().remove(ue); + } + } + + if(canvas.getImageObserver() != null) { + canvas.getImageObserver().imageUpdate(null, ImageObserver.ALLBITS, 0, 0, canvas.getWidth(), canvas.getHeight()); + } + + /** Notify the user */ + client.getUserInterface().println(user.getName() + " deleted layer " + layer.getName()); + + /** Return and broadcast. */ + return Connection.SEND_ALL; + } + + public void decode(ByteBuffer in) { + layerKey = in.getInt(); + } + + public void encode(ByteBuffer out) { + out.putInt(layerKey); + } +} diff --git a/src/com/jotuntech/sketcher/client/command/FilterCommand.java b/src/com/jotuntech/sketcher/client/command/FilterCommand.java new file mode 100755 index 0000000..47c63e6 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/command/FilterCommand.java @@ -0,0 +1,82 @@ +package com.jotuntech.sketcher.client.command; + +import java.awt.AlphaComposite; +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.client.Client; +import com.jotuntech.sketcher.client.Command; +import com.jotuntech.sketcher.client.Connection; +import com.jotuntech.sketcher.common.BitmapLayer; +import com.jotuntech.sketcher.common.User; +import com.jotuntech.sketcher.common.filter.Filter; + +public class FilterCommand implements Command { + private Filter filter; + private int x, y, w, h; + float a, b, c; + + public FilterCommand() { + + } + + public FilterCommand(Filter filter, float x, float y, float w, float h, float a, float b, float c) { + this.filter = filter; + this.x = (int) x; + this.y = (int) y; + this.w = (int) w; + this.h = (int) h; + this.a = a; + this.b = b; + this.c = c; + } + + public int perform(Client client, User user) { + if(user == null) { + return Connection.SEND_NONE; + } + + BitmapLayer pl = (BitmapLayer) user.getPhantomLayer(); + filter.setSize(w, h); + filter.setParameterA(a); + user.getLayer().copyTo(pl, null, false, x, y, x, y, w, h); + pl.setAlphaRule(AlphaComposite.SRC); + pl.setOpacity(1); + pl.applyFilter(filter, user.getLayer().getImageObserver(), x, y, w, h); + return Connection.SEND_OTHERS; + } + + public void decode(ByteBuffer in) { + x = (int) in.getFloat(); + y = (int) in.getFloat(); + w = (int) in.getFloat(); + h = (int) in.getFloat(); + a = in.getFloat(); + b = in.getFloat(); + c = in.getFloat(); + + StringBuffer filterNameBuffer = new StringBuffer(); + while(in.remaining() >= 2) { + filterNameBuffer.append(in.getChar()); + } + String filterName = "com.jotuntech.sketcher.common.filter." + filterNameBuffer.toString(); + try { + filter = (Filter) Class.forName(filterName).newInstance(); + } catch (Throwable t) { + t.printStackTrace(); + } + } + + public void encode(ByteBuffer out) { + out.putFloat(x); + out.putFloat(y); + out.putFloat(w); + out.putFloat(h); + out.putFloat(a); + out.putFloat(b); + out.putFloat(c); + String filterName = filter.getClass().getSimpleName(); + for(int i = 0; i < filterName.length(); i++) { + out.putChar(filterName.charAt(i)); + } + } +} diff --git a/src/com/jotuntech/sketcher/client/command/KickCommand.java b/src/com/jotuntech/sketcher/client/command/KickCommand.java new file mode 100755 index 0000000..c592085 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/command/KickCommand.java @@ -0,0 +1,58 @@ +package com.jotuntech.sketcher.client.command; + +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.client.Client; +import com.jotuntech.sketcher.client.Command; +import com.jotuntech.sketcher.client.Connection; +import com.jotuntech.sketcher.client.UserInterface; +import com.jotuntech.sketcher.common.User; + +public class KickCommand implements Command { + int targetKey; + String reason; + + public KickCommand() { + + } + + public KickCommand(int targetKey, String reason) { + this.targetKey = targetKey; + this.reason = reason; + } + + public int perform(Client client, User user) { + if(user != null && user.isViewer()) { + return Connection.SEND_NONE; + } + + User targetUser = client.getUserMap().remove(targetKey); + client.getUserInterface().println(targetUser.getName() + " was kicked out (" + reason + ")"); + + if(targetUser == client.getConnection().getUser()) { + client.getConnection().setTimeOfDeath(System.currentTimeMillis()); + } + + if(client.isSoundEnabled()) { + UserInterface.AUDIO_KICK.play(); + } + + return Connection.SEND_NONE; + } + + public void decode(ByteBuffer in) { + targetKey = in.getInt(); + StringBuffer reasonBuffer = new StringBuffer(); + while(in.remaining() >= 2) { + reasonBuffer.append(in.getChar()); + } + reason = reasonBuffer.toString(); + } + + public void encode(ByteBuffer out) { + out.putInt(targetKey); + for(int i = 0; i < reason.length(); i++) { + out.putChar(reason.charAt(i)); + } + } +} \ No newline at end of file diff --git a/src/com/jotuntech/sketcher/client/command/LayerDataCommand.java b/src/com/jotuntech/sketcher/client/command/LayerDataCommand.java new file mode 100755 index 0000000..1a791bd --- /dev/null +++ b/src/com/jotuntech/sketcher/client/command/LayerDataCommand.java @@ -0,0 +1,66 @@ +package com.jotuntech.sketcher.client.command; + +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.client.Client; +import com.jotuntech.sketcher.client.Command; +import com.jotuntech.sketcher.client.Connection; +import com.jotuntech.sketcher.common.Canvas; +import com.jotuntech.sketcher.common.Layer; +import com.jotuntech.sketcher.common.User; + +public class LayerDataCommand implements Command { + int layerKey; + boolean phantom; + byte[] data; + + public LayerDataCommand() { + } + + public LayerDataCommand(int layerKey, boolean phantom, byte[] data) { + this.layerKey = layerKey; + this.phantom = phantom; + this.data = data; + } + + public int perform(Client client, User user) { + Canvas canvas = client.getCanvas(); + + if(canvas == null) { + return Connection.SEND_NONE; + } + + Layer layer; + + if(phantom) { + if(user == null) { + return Connection.SEND_NONE; + } + + layer = user.getPhantomLayer(); + } else { + layer = canvas.getLayerMap().get(layerKey); + + if(layer == null) { + return Connection.SEND_NONE; + } + } + + layer.decode(data); + + return Connection.SEND_NONE; + } + + public void decode(ByteBuffer in) { + layerKey = in.getInt(); + phantom = in.get() != 0; + data = new byte[in.remaining()]; + in.get(data); + } + + public void encode(ByteBuffer out) { + out.putInt(layerKey); + out.put((byte) (phantom ? 0xFF : 0x00)); + out.put(data); + } +} diff --git a/src/com/jotuntech/sketcher/client/command/LineCommand.java b/src/com/jotuntech/sketcher/client/command/LineCommand.java new file mode 100755 index 0000000..37ab3a9 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/command/LineCommand.java @@ -0,0 +1,73 @@ +/** + * + */ +package com.jotuntech.sketcher.client.command; + +import java.awt.AlphaComposite; +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.client.Client; +import com.jotuntech.sketcher.client.Command; +import com.jotuntech.sketcher.client.Connection; +import com.jotuntech.sketcher.common.Input; +import com.jotuntech.sketcher.common.Layer; +import com.jotuntech.sketcher.common.User; + + +public class LineCommand implements Command { + protected Input input; + + /** Constructs a Daub command */ + public LineCommand() { + + } + + /** Constructs a Daub command. + * @param user Source user. + * @param input Position of new daub. */ + public LineCommand(Input input) { + this.input = input; + } + + public int perform(Client client, User user) { + if(user != null && user.getLayer() != null && user.getBrush() != null) { + if(user.isViewer()) { + return Connection.SEND_NONE; + } + + Layer l = user.getPhantomLayer(); + l.setOpacity(Math.abs(user.getBrush().getOpacity()) / 255f); + if(user.getBrush().isLockTransparency()) { + l.setAlphaRule(AlphaComposite.SRC_ATOP); + } else if(user.getBrush().getOpacity() >= 0) { + l.setAlphaRule(AlphaComposite.SRC_OVER); + } else { + l.setAlphaRule(AlphaComposite.DST_OUT); + } + + /** Draw line */ + Input end = user.getPhantomLayer().line(user.getCursor(), input, user.getColor(), user.getBrush(), user.getLayer()); + + if(end != null) { + /** Move cursor on success */ + user.setCursor(end); + + /** Return and broadcast */ + return Connection.SEND_OTHERS; + } + } + + /** Return silently */ + return Connection.SEND_NONE; + } + + public void decode(ByteBuffer in) { + input = new Input(); + input.decode(in); + } + + public void encode(ByteBuffer out) { + input.encode(out); + } +} + diff --git a/src/com/jotuntech/sketcher/client/command/MergeCommand.java b/src/com/jotuntech/sketcher/client/command/MergeCommand.java new file mode 100755 index 0000000..d98b8e7 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/command/MergeCommand.java @@ -0,0 +1,45 @@ +package com.jotuntech.sketcher.client.command; + +import java.nio.ByteBuffer; +import java.util.Deque; + +import com.jotuntech.sketcher.client.Client; +import com.jotuntech.sketcher.client.Command; +import com.jotuntech.sketcher.client.Connection; +import com.jotuntech.sketcher.common.UndoEntry; +import com.jotuntech.sketcher.common.User; + +public class MergeCommand implements Command { + public int perform(Client client, User user) { + if(user == null || user.getLayer() == null) { + return Connection.SEND_NONE; + } + + if(user.isViewer()) { + return Connection.SEND_NONE; + } + + if(user.getPhantomLayer().isEmpty()) { + return Connection.SEND_NONE; + } + + UndoEntry undoEntry = new UndoEntry(); + undoEntry.setLayer(user.getLayer()); + undoEntry.setUndoData(user.getLayer().merge(user.getPhantomLayer())); + Deque undoDeque = user.getUndoDeque(); + undoDeque.addFirst(undoEntry); + while(undoDeque.size() > 10) { + undoDeque.removeLast(); + } + + return Connection.SEND_OTHERS; + } + + public void decode(ByteBuffer in) { + + } + + public void encode(ByteBuffer out) { + + } +} diff --git a/src/com/jotuntech/sketcher/client/command/MoveCommand.java b/src/com/jotuntech/sketcher/client/command/MoveCommand.java new file mode 100755 index 0000000..db04bf6 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/command/MoveCommand.java @@ -0,0 +1,68 @@ +package com.jotuntech.sketcher.client.command; + +import java.awt.AlphaComposite; +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.client.Client; +import com.jotuntech.sketcher.client.Command; +import com.jotuntech.sketcher.client.Connection; +import com.jotuntech.sketcher.common.BitmapLayer; +import com.jotuntech.sketcher.common.Layer; +import com.jotuntech.sketcher.common.User; + +public class MoveCommand implements Command { + private float sx, sy, dx, dy, w, h; + + public MoveCommand() { + + } + + public MoveCommand(float sx, float sy, float dx, float dy, float w, float h) { + this.sx = sx; + this.sy = sy; + this.dx = dx; + this.dy = dy; + this.w = w; + this.h = h; + } + + public int perform(Client client, User user) { + if(user == null || user.getLayer() == null) { + return Connection.SEND_NONE; + } + + if(user.isViewer()) { + return Connection.SEND_NONE; + } + + if(user.getPhantomLayer().isEmpty()) { + Layer pl = (BitmapLayer) user.getPhantomLayer(); + pl.setAlphaRule(AlphaComposite.SRC_OVER); + pl.setOpacity(1); + user.getLayer().copyTo(pl, null, false, sx, sy, dx, dy, w, h); + } else { + user.getPhantomLayer().copyTo(user.getPhantomLayer(), user.getLayer().getImageObserver(), true, sx, sy, dx, dy, w, h); + user.getPhantomLayer().clean(); + } + + return Connection.SEND_OTHERS; + } + + public void decode(ByteBuffer in) { + sx = in.getFloat(); + sy = in.getFloat(); + dx = in.getFloat(); + dy = in.getFloat(); + w = in.getFloat(); + h = in.getFloat(); + } + + public void encode(ByteBuffer out) { + out.putFloat(sx); + out.putFloat(sy); + out.putFloat(dx); + out.putFloat(dy); + out.putFloat(w); + out.putFloat(h); + } +} diff --git a/src/com/jotuntech/sketcher/client/command/PingCommand.java b/src/com/jotuntech/sketcher/client/command/PingCommand.java new file mode 100755 index 0000000..1dbd192 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/command/PingCommand.java @@ -0,0 +1,32 @@ +package com.jotuntech.sketcher.client.command; + +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.client.Client; +import com.jotuntech.sketcher.client.Command; +import com.jotuntech.sketcher.client.Connection; +import com.jotuntech.sketcher.common.User; + +public class PingCommand implements Command { + private long timestamp; + + public PingCommand() { + this.timestamp = System.currentTimeMillis(); + } + + public PingCommand(long timestamp) { + this.timestamp = timestamp; + } + + public int perform(Client client, User user) { + return Connection.SEND_NONE; + } + + public void decode(ByteBuffer in) { + timestamp = in.getLong(); + } + + public void encode(ByteBuffer out) { + out.putLong(timestamp); + } +} diff --git a/src/com/jotuntech/sketcher/client/command/SayCommand.java b/src/com/jotuntech/sketcher/client/command/SayCommand.java new file mode 100755 index 0000000..0ac677b --- /dev/null +++ b/src/com/jotuntech/sketcher/client/command/SayCommand.java @@ -0,0 +1,62 @@ +package com.jotuntech.sketcher.client.command; + +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.client.Client; +import com.jotuntech.sketcher.client.Command; +import com.jotuntech.sketcher.client.Connection; +import com.jotuntech.sketcher.client.UserInterface; +import com.jotuntech.sketcher.common.User; + +public class SayCommand implements Command { + boolean isAction; + String text; + + public SayCommand() { + + } + + public SayCommand(String text, boolean isAction) { + this.isAction = isAction; + this.text = text; + } + + public int perform(Client client, User user) { + if(user == null) { + return Connection.SEND_NONE; + } + + if(user.isViewer()) { + return Connection.SEND_NONE; + } + + + if(isAction) { + client.getUserInterface().println("\u2022 " + user.getName() + text); + } else { + client.getUserInterface().println("<" + user.getName() + "> " + text); + } + + if(client.isSoundEnabled()) { + UserInterface.AUDIO_CHAT.play(); + } + + return Connection.SEND_NONE; + } + + public void decode(ByteBuffer in) { + isAction = in.get() != 0 ? true : false; + StringBuffer textBuffer = new StringBuffer(); + while(in.remaining() >= 2) { + textBuffer.append(in.getChar()); + } + text = textBuffer.toString(); + } + + public void encode(ByteBuffer out) { + out.put((byte) (isAction ? 0xFF : 0x00)); + for(int i = 0; i < text.length(); i++) { + out.putChar(text.charAt(i)); + } + } +} diff --git a/src/com/jotuntech/sketcher/client/command/ServerMessageCommand.java b/src/com/jotuntech/sketcher/client/command/ServerMessageCommand.java new file mode 100755 index 0000000..4424333 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/command/ServerMessageCommand.java @@ -0,0 +1,41 @@ +package com.jotuntech.sketcher.client.command; + +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.client.Client; +import com.jotuntech.sketcher.client.Command; +import com.jotuntech.sketcher.client.Connection; +import com.jotuntech.sketcher.common.User; + +public class ServerMessageCommand implements Command { + String text; + + public ServerMessageCommand() { + } + + public ServerMessageCommand(String text) { + this.text = text; + } + + public int perform(Client client, User user) { + /** Notify the user */ + client.getUserInterface().println(text); + + /** Return silently. */ + return Connection.SEND_NONE; + } + + public void decode(ByteBuffer in) { + StringBuffer textBuffer = new StringBuffer(); + while(in.remaining() >= 2) { + textBuffer.append(in.getChar()); + } + text = textBuffer.toString(); + } + + public void encode(ByteBuffer out) { + for(int i = 0; i < text.length(); i++) { + out.putChar(text.charAt(i)); + } + } +} diff --git a/src/com/jotuntech/sketcher/client/command/SetBrushCommand.java b/src/com/jotuntech/sketcher/client/command/SetBrushCommand.java new file mode 100755 index 0000000..ed65649 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/command/SetBrushCommand.java @@ -0,0 +1,57 @@ +package com.jotuntech.sketcher.client.command; + +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.client.Client; +import com.jotuntech.sketcher.client.Command; +import com.jotuntech.sketcher.client.Connection; +import com.jotuntech.sketcher.client.Editor; +import com.jotuntech.sketcher.common.Brush; +import com.jotuntech.sketcher.common.User; + +public class SetBrushCommand implements Command { + Brush brush; + + public SetBrushCommand() { + + } + + public SetBrushCommand(Brush brush) { + this.brush = brush; + } + + public int perform(Client client, User user) { + if(user == null) { + /** Signed in users only */ + return Connection.SEND_NONE; + } + + if(user.isViewer()) { + return Connection.SEND_NONE; + } + + Brush oldBrush = user.getBrush(); + user.setBrush(brush); + + if(user == client.getConnection().getUser()) { + /** Update the GUI */ + client.getUserInterface().updateBrushSliders(); + + if(oldBrush == null) { + /* Enable editor when first brush is selected */ + client.getUserInterface().getEditor().setState(Editor.State.DRAW_HOVER); + } + } + + return Connection.SEND_OTHERS; + } + + public void decode(ByteBuffer in) { + brush = new Brush(); + brush.decode(in); + } + + public void encode(ByteBuffer out) { + brush.encode(out); + } +} diff --git a/src/com/jotuntech/sketcher/client/command/SetColorCommand.java b/src/com/jotuntech/sketcher/client/command/SetColorCommand.java new file mode 100755 index 0000000..55d864b --- /dev/null +++ b/src/com/jotuntech/sketcher/client/command/SetColorCommand.java @@ -0,0 +1,53 @@ +package com.jotuntech.sketcher.client.command; + +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.client.Client; +import com.jotuntech.sketcher.client.Command; +import com.jotuntech.sketcher.client.Connection; +import com.jotuntech.sketcher.common.User; + +public class SetColorCommand implements Command { + static boolean firstWhite = true; + int color; + + public SetColorCommand() { + } + + public SetColorCommand(int color) { + this.color = color; + } + + public int perform(Client client, User user) { + if(color == 0xFFFFFF && user == client.getConnection().getUser() && client.getConnection().getUser().getBrush() != null && client.getConnection().getUser().getBrush().getOpacity() > 0 && firstWhite) { + client.getUserInterface().println("Use eraser unless drawing white. :-)"); + firstWhite = false; + } + + return perform(user) ? Connection.SEND_OTHERS : Connection.SEND_NONE; + } + + private boolean perform(User user) { + if(user == null) { + /** Signed in users only */ + return false; + } + + if(user.isViewer()) { + return false; + } + + user.setColor(color); + + return true; + } + + public void decode(ByteBuffer in) { + color = in.getInt(); + } + + public void encode(ByteBuffer out) { + out.putInt(color); + } + +} diff --git a/src/com/jotuntech/sketcher/client/command/SetLayerCommand.java b/src/com/jotuntech/sketcher/client/command/SetLayerCommand.java new file mode 100755 index 0000000..e7bc186 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/command/SetLayerCommand.java @@ -0,0 +1,64 @@ +package com.jotuntech.sketcher.client.command; + +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.client.Client; +import com.jotuntech.sketcher.client.Command; +import com.jotuntech.sketcher.client.Connection; +import com.jotuntech.sketcher.common.Canvas; +import com.jotuntech.sketcher.common.Layer; +import com.jotuntech.sketcher.common.User; + +public class SetLayerCommand implements Command { + Integer layerKey; + + public SetLayerCommand() { + } + + public SetLayerCommand(Integer layerKey) { + this.layerKey = layerKey; + } + + public int perform(Client client, User user) { + if(user == null) { + return Connection.SEND_NONE; + } + + if(user.isViewer()) { + return Connection.SEND_NONE; + } + + Canvas canvas = client.getCanvas(); + + if(canvas == null) { + return Connection.SEND_NONE; + } + + Layer layer = canvas.getLayerMap().get(layerKey); + + if(layer == null) { + return Connection.SEND_NONE; + } + + if(user.getLayer() == layer) { + return Connection.SEND_NONE; + } + + user.setLayer(layer); + + if(user == client.getConnection().getUser()) { + client.getUserInterface().updateLayer(); + } + + return Connection.SEND_OTHERS; + } + + + public void decode(ByteBuffer in) { + layerKey = in.getInt(); + } + + public void encode(ByteBuffer out) { + out.putInt(layerKey); + } +} diff --git a/src/com/jotuntech/sketcher/client/command/SignInCommand.java b/src/com/jotuntech/sketcher/client/command/SignInCommand.java new file mode 100755 index 0000000..390bfe6 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/command/SignInCommand.java @@ -0,0 +1,96 @@ +/** + * + */ +package com.jotuntech.sketcher.client.command; + +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.client.Client; +import com.jotuntech.sketcher.client.Command; +import com.jotuntech.sketcher.client.Connection; +import com.jotuntech.sketcher.client.UserInterface; +import com.jotuntech.sketcher.common.User; + +/** + * @author Thor Harald Johansen + * + */ +public class SignInCommand implements Command { + Integer peerKey; + String login; + String password; + boolean viewer; + + public SignInCommand() { + + } + + public SignInCommand(Integer peerKey, String login, String password, boolean viewer) { + this.peerKey = peerKey; + this.login = login; + this.password = password; + this.viewer = viewer; + } + + public void decode(ByteBuffer in) { + peerKey = in.getInt(); + viewer = in.get() != 0; + int loginLength = (in.get() & 0xFF) + 1; + StringBuffer loginBuffer = new StringBuffer(); + for(int i = 0; i < loginLength; i++) { + loginBuffer.append(in.getChar()); + } + login = loginBuffer.toString(); + + StringBuffer passwordBuffer = new StringBuffer(); + while(in.remaining() >= 2) { + passwordBuffer.append(in.getChar()); + } + password = passwordBuffer.toString(); + } + + public void encode(ByteBuffer out) { + out.putInt(peerKey); + out.put((byte) (viewer ? 0xFF : 0x00)); + out.put((byte) (login.length() - 1)); + for(int i = 0; i < login.length(); i++) { + out.putChar(login.charAt(i)); + } + for(int i = 0; i < password.length(); i++) { + out.putChar(password.charAt(i)); + } + } + + public int perform(Client client, User user) { + /** Create user */ + user = new User(login); + + /** Set viewer flag */ + user.setViewer(viewer); + + /** Put user in client user map */ + client.getUserMap().put(peerKey, user); + + if(client.getConnection().getUser() == null) { + /** Assign user to client */ + client.getConnection().setUser(user); + + /** Select a default brush */ + client.getUserInterface().getBrushButtons()[0].doClick(); + } + + /** Add canvas as phantom layer listener */ + user.getPhantomLayer().addImageObserver(client.getCanvas()); + + /** Print message */ + client.getUserInterface().println(user.getName() + " has signed in"); + + /** Play audio */ + if(client.isSoundEnabled()) { + UserInterface.AUDIO_SIGN_IN.play(); + } + + /** Return silently */ + return Connection.SEND_NONE; + } +} diff --git a/src/com/jotuntech/sketcher/client/command/SignOutCommand.java b/src/com/jotuntech/sketcher/client/command/SignOutCommand.java new file mode 100755 index 0000000..a4eda46 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/command/SignOutCommand.java @@ -0,0 +1,49 @@ +package com.jotuntech.sketcher.client.command; + +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.client.Client; +import com.jotuntech.sketcher.client.Command; +import com.jotuntech.sketcher.client.Connection; +import com.jotuntech.sketcher.client.UserInterface; +import com.jotuntech.sketcher.common.User; + +public class SignOutCommand implements Command { + String message; + + public SignOutCommand() { + message = new String(); + } + + public SignOutCommand(String message) { + this.message = message; + } + + public int perform(Client client, User user) { + client.getUserMap().removeByValue(user); + + if(user != null) { + client.getUserInterface().println(user.getName() + " has disconnected (" + message + ")"); + } + + if(client.isSoundEnabled()) { + UserInterface.AUDIO_SIGN_OUT.play(); + } + + return Connection.SEND_NONE; + } + + public void decode(ByteBuffer in) { + StringBuffer messageBuffer = new StringBuffer(); + while(in.remaining() >= 2) { + messageBuffer.append(in.getChar()); + } + message = messageBuffer.toString(); + } + + public void encode(ByteBuffer out) { + for(int i = 0; i < message.length(); i++) { + out.putChar(message.charAt(i)); + } + } +} diff --git a/src/com/jotuntech/sketcher/client/command/UndoCommand.java b/src/com/jotuntech/sketcher/client/command/UndoCommand.java new file mode 100755 index 0000000..e83abca --- /dev/null +++ b/src/com/jotuntech/sketcher/client/command/UndoCommand.java @@ -0,0 +1,35 @@ +package com.jotuntech.sketcher.client.command; + +import java.nio.ByteBuffer; +import java.util.Deque; + +import com.jotuntech.sketcher.client.Client; +import com.jotuntech.sketcher.client.Command; +import com.jotuntech.sketcher.client.Connection; +import com.jotuntech.sketcher.common.UndoEntry; +import com.jotuntech.sketcher.common.User; + +public class UndoCommand implements Command { + + public int perform(Client client, User user) { + if(user.isViewer()) { + return Connection.SEND_NONE; + } + + Deque undoDeque = user.getUndoDeque(); + if(undoDeque.size() == 0) { + return Connection.SEND_NONE; + } + UndoEntry undoEntry = undoDeque.removeFirst(); + undoEntry.getLayer().undo(undoEntry.getUndoData()); + return Connection.SEND_OTHERS; + } + + public void decode(ByteBuffer in) { + + } + + public void encode(ByteBuffer out) { + + } +} diff --git a/src/com/jotuntech/sketcher/client/command/UndoDataCommand.java b/src/com/jotuntech/sketcher/client/command/UndoDataCommand.java new file mode 100755 index 0000000..e483154 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/command/UndoDataCommand.java @@ -0,0 +1,45 @@ +package com.jotuntech.sketcher.client.command; + +import java.nio.ByteBuffer; +import java.util.Deque; + +import com.jotuntech.sketcher.client.Client; +import com.jotuntech.sketcher.client.Command; +import com.jotuntech.sketcher.client.Connection; +import com.jotuntech.sketcher.common.BitmapUndoData; +import com.jotuntech.sketcher.common.UndoEntry; +import com.jotuntech.sketcher.common.User; + +public class UndoDataCommand implements Command { + byte[] data; + + public int perform(Client client, User user) { + if(user == null) { + return Connection.SEND_NONE; + } + + if(user.isViewer()) { + return Connection.SEND_NONE; + } + + Deque undoDeque = user.getUndoDeque(); + + if(undoDeque.size() == 0) { + return Connection.SEND_NONE; + } + + BitmapUndoData bud = (BitmapUndoData) undoDeque.getLast().getUndoData(); + bud.decode(data); + + return Connection.SEND_NONE; + } + + public void decode(ByteBuffer in) { + data = new byte[in.remaining()]; + in.get(data); + } + + public void encode(ByteBuffer out) { + out.put(data); + } +} diff --git a/src/com/jotuntech/sketcher/client/command/UndoEntryCommand.java b/src/com/jotuntech/sketcher/client/command/UndoEntryCommand.java new file mode 100755 index 0000000..71e5969 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/command/UndoEntryCommand.java @@ -0,0 +1,46 @@ +package com.jotuntech.sketcher.client.command; + +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.client.Client; +import com.jotuntech.sketcher.client.Command; +import com.jotuntech.sketcher.common.BitmapUndoData; +import com.jotuntech.sketcher.common.Layer; +import com.jotuntech.sketcher.common.UndoEntry; +import com.jotuntech.sketcher.common.User; +import com.jotuntech.sketcher.server.Connection; + +public class UndoEntryCommand implements Command { + Integer layerKey; + + public int perform(Client client, User user) { + if(user == null) { + return Connection.SEND_NONE; + } + + if(user.isViewer()) { + return Connection.SEND_NONE; + } + + Layer layer = client.getCanvas().getLayerMap().get(layerKey); + + if(layer == null) { + return Connection.SEND_NONE; + } + + UndoEntry undoEntry = new UndoEntry(); + undoEntry.setLayer(layer); + undoEntry.setUndoData(new BitmapUndoData()); + user.getUndoDeque().addLast(undoEntry); + + return Connection.SEND_NONE; + } + + public void decode(ByteBuffer in) { + layerKey = in.getInt(); + } + + public void encode(ByteBuffer out) { + out.putInt(layerKey); + } +} diff --git a/src/com/jotuntech/sketcher/client/command/VoiceCommand.java b/src/com/jotuntech/sketcher/client/command/VoiceCommand.java new file mode 100755 index 0000000..b00d244 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/command/VoiceCommand.java @@ -0,0 +1,37 @@ +package com.jotuntech.sketcher.client.command; + +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.client.Client; +import com.jotuntech.sketcher.client.Command; +import com.jotuntech.sketcher.client.Connection; +import com.jotuntech.sketcher.common.User; + +public class VoiceCommand implements Command { + boolean voiceEnabled; + + public VoiceCommand() { + + } + + public VoiceCommand(boolean voiceEnabled) { + this.voiceEnabled = voiceEnabled; + } + + public int perform(Client client, User user) { + if(user.isViewer()) { + return Connection.SEND_NONE; + } + + client.setVoiceEnabled(voiceEnabled); + return Connection.SEND_OTHERS; + } + + public void decode(ByteBuffer in) { + voiceEnabled = in.get() != 0; + } + + public void encode(ByteBuffer out) { + out.put((byte) (voiceEnabled ? 0xFF : 0x00)); + } +} diff --git a/src/com/jotuntech/sketcher/client/images/about.png b/src/com/jotuntech/sketcher/client/images/about.png new file mode 100755 index 0000000..2e0c2a6 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/about.png differ diff --git a/src/com/jotuntech/sketcher/client/images/ad1.png b/src/com/jotuntech/sketcher/client/images/ad1.png new file mode 100755 index 0000000..e58a25e Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/ad1.png differ diff --git a/src/com/jotuntech/sketcher/client/images/bezier.png b/src/com/jotuntech/sketcher/client/images/bezier.png new file mode 100755 index 0000000..231a38b Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/bezier.png differ diff --git a/src/com/jotuntech/sketcher/client/images/blue.png b/src/com/jotuntech/sketcher/client/images/blue.png new file mode 100755 index 0000000..e69acfe Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/blue.png differ diff --git a/src/com/jotuntech/sketcher/client/images/brightness.png b/src/com/jotuntech/sketcher/client/images/brightness.png new file mode 100755 index 0000000..e65526c Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/brightness.png differ diff --git a/src/com/jotuntech/sketcher/client/images/brush.png b/src/com/jotuntech/sketcher/client/images/brush.png new file mode 100755 index 0000000..83a1493 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/brush.png differ diff --git a/src/com/jotuntech/sketcher/client/images/cmy.png b/src/com/jotuntech/sketcher/client/images/cmy.png new file mode 100755 index 0000000..88366a9 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/cmy.png differ diff --git a/src/com/jotuntech/sketcher/client/images/color.png b/src/com/jotuntech/sketcher/client/images/color.png new file mode 100755 index 0000000..5888349 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/color.png differ diff --git a/src/com/jotuntech/sketcher/client/images/crosshair.gif b/src/com/jotuntech/sketcher/client/images/crosshair.gif new file mode 100755 index 0000000..61ced1d Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/crosshair.gif differ diff --git a/src/com/jotuntech/sketcher/client/images/cyan.png b/src/com/jotuntech/sketcher/client/images/cyan.png new file mode 100755 index 0000000..ac66843 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/cyan.png differ diff --git a/src/com/jotuntech/sketcher/client/images/dummybanner.png b/src/com/jotuntech/sketcher/client/images/dummybanner.png new file mode 100755 index 0000000..e19af5b Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/dummybanner.png differ diff --git a/src/com/jotuntech/sketcher/client/images/eraser.png b/src/com/jotuntech/sketcher/client/images/eraser.png new file mode 100755 index 0000000..28e4e8f Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/eraser.png differ diff --git a/src/com/jotuntech/sketcher/client/images/eyedropper.gif b/src/com/jotuntech/sketcher/client/images/eyedropper.gif new file mode 100755 index 0000000..5d189a3 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/eyedropper.gif differ diff --git a/src/com/jotuntech/sketcher/client/images/flow.png b/src/com/jotuntech/sketcher/client/images/flow.png new file mode 100755 index 0000000..99c925d Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/flow.png differ diff --git a/src/com/jotuntech/sketcher/client/images/freehand.png b/src/com/jotuntech/sketcher/client/images/freehand.png new file mode 100755 index 0000000..dab3170 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/freehand.png differ diff --git a/src/com/jotuntech/sketcher/client/images/green.png b/src/com/jotuntech/sketcher/client/images/green.png new file mode 100755 index 0000000..7c3e368 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/green.png differ diff --git a/src/com/jotuntech/sketcher/client/images/hand.gif b/src/com/jotuntech/sketcher/client/images/hand.gif new file mode 100755 index 0000000..5569dc3 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/hand.gif differ diff --git a/src/com/jotuntech/sketcher/client/images/hardness.png b/src/com/jotuntech/sketcher/client/images/hardness.png new file mode 100755 index 0000000..6d2d690 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/hardness.png differ diff --git a/src/com/jotuntech/sketcher/client/images/hsb.png b/src/com/jotuntech/sketcher/client/images/hsb.png new file mode 100755 index 0000000..8ed37e8 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/hsb.png differ diff --git a/src/com/jotuntech/sketcher/client/images/hue.png b/src/com/jotuntech/sketcher/client/images/hue.png new file mode 100755 index 0000000..20fdddb Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/hue.png differ diff --git a/src/com/jotuntech/sketcher/client/images/ink.png b/src/com/jotuntech/sketcher/client/images/ink.png new file mode 100755 index 0000000..4b505bf Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/ink.png differ diff --git a/src/com/jotuntech/sketcher/client/images/jitter.png b/src/com/jotuntech/sketcher/client/images/jitter.png new file mode 100755 index 0000000..82da503 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/jitter.png differ diff --git a/src/com/jotuntech/sketcher/client/images/layers.png b/src/com/jotuntech/sketcher/client/images/layers.png new file mode 100755 index 0000000..81989de Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/layers.png differ diff --git a/src/com/jotuntech/sketcher/client/images/line.png b/src/com/jotuntech/sketcher/client/images/line.png new file mode 100755 index 0000000..70b2f45 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/line.png differ diff --git a/src/com/jotuntech/sketcher/client/images/madeinsketcher.png b/src/com/jotuntech/sketcher/client/images/madeinsketcher.png new file mode 100755 index 0000000..00f0671 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/madeinsketcher.png differ diff --git a/src/com/jotuntech/sketcher/client/images/magenta.png b/src/com/jotuntech/sketcher/client/images/magenta.png new file mode 100755 index 0000000..751ea55 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/magenta.png differ diff --git a/src/com/jotuntech/sketcher/client/images/move.png b/src/com/jotuntech/sketcher/client/images/move.png new file mode 100755 index 0000000..cfe5840 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/move.png differ diff --git a/src/com/jotuntech/sketcher/client/images/noise.png b/src/com/jotuntech/sketcher/client/images/noise.png new file mode 100755 index 0000000..7ae4ffe Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/noise.png differ diff --git a/src/com/jotuntech/sketcher/client/images/opacity.png b/src/com/jotuntech/sketcher/client/images/opacity.png new file mode 100755 index 0000000..9588b45 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/opacity.png differ diff --git a/src/com/jotuntech/sketcher/client/images/oval.png b/src/com/jotuntech/sketcher/client/images/oval.png new file mode 100755 index 0000000..9eb5406 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/oval.png differ diff --git a/src/com/jotuntech/sketcher/client/images/pen.png b/src/com/jotuntech/sketcher/client/images/pen.png new file mode 100755 index 0000000..f5fe9ba Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/pen.png differ diff --git a/src/com/jotuntech/sketcher/client/images/pencil.png b/src/com/jotuntech/sketcher/client/images/pencil.png new file mode 100755 index 0000000..1011954 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/pencil.png differ diff --git a/src/com/jotuntech/sketcher/client/images/pin_normal.png b/src/com/jotuntech/sketcher/client/images/pin_normal.png new file mode 100755 index 0000000..65247d8 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/pin_normal.png differ diff --git a/src/com/jotuntech/sketcher/client/images/pin_stuck.png b/src/com/jotuntech/sketcher/client/images/pin_stuck.png new file mode 100755 index 0000000..3a914be Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/pin_stuck.png differ diff --git a/src/com/jotuntech/sketcher/client/images/rectangle.png b/src/com/jotuntech/sketcher/client/images/rectangle.png new file mode 100755 index 0000000..15e8c9c Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/rectangle.png differ diff --git a/src/com/jotuntech/sketcher/client/images/red.png b/src/com/jotuntech/sketcher/client/images/red.png new file mode 100755 index 0000000..86c745d Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/red.png differ diff --git a/src/com/jotuntech/sketcher/client/images/rgb.png b/src/com/jotuntech/sketcher/client/images/rgb.png new file mode 100755 index 0000000..d5fb7d1 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/rgb.png differ diff --git a/src/com/jotuntech/sketcher/client/images/saturation.png b/src/com/jotuntech/sketcher/client/images/saturation.png new file mode 100755 index 0000000..4c74aec Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/saturation.png differ diff --git a/src/com/jotuntech/sketcher/client/images/select.png b/src/com/jotuntech/sketcher/client/images/select.png new file mode 100755 index 0000000..de9c47e Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/select.png differ diff --git a/src/com/jotuntech/sketcher/client/images/size.png b/src/com/jotuntech/sketcher/client/images/size.png new file mode 100755 index 0000000..76ba2fa Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/size.png differ diff --git a/src/com/jotuntech/sketcher/client/images/sketcherlogo.png b/src/com/jotuntech/sketcher/client/images/sketcherlogo.png new file mode 100755 index 0000000..8b984c2 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/sketcherlogo.png differ diff --git a/src/com/jotuntech/sketcher/client/images/smoothing.png b/src/com/jotuntech/sketcher/client/images/smoothing.png new file mode 100755 index 0000000..805b8fe Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/smoothing.png differ diff --git a/src/com/jotuntech/sketcher/client/images/spacing.png b/src/com/jotuntech/sketcher/client/images/spacing.png new file mode 100755 index 0000000..ac9920e Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/spacing.png differ diff --git a/src/com/jotuntech/sketcher/client/images/strength.png b/src/com/jotuntech/sketcher/client/images/strength.png new file mode 100755 index 0000000..9588b45 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/strength.png differ diff --git a/src/com/jotuntech/sketcher/client/images/tools.png b/src/com/jotuntech/sketcher/client/images/tools.png new file mode 100755 index 0000000..6d2d690 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/tools.png differ diff --git a/src/com/jotuntech/sketcher/client/images/trailing.png b/src/com/jotuntech/sketcher/client/images/trailing.png new file mode 100755 index 0000000..bc93d82 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/trailing.png differ diff --git a/src/com/jotuntech/sketcher/client/images/water.png b/src/com/jotuntech/sketcher/client/images/water.png new file mode 100755 index 0000000..20106a3 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/water.png differ diff --git a/src/com/jotuntech/sketcher/client/images/water2.png b/src/com/jotuntech/sketcher/client/images/water2.png new file mode 100755 index 0000000..3e6c983 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/water2.png differ diff --git a/src/com/jotuntech/sketcher/client/images/waterarea.png b/src/com/jotuntech/sketcher/client/images/waterarea.png new file mode 100755 index 0000000..cb85ebf Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/waterarea.png differ diff --git a/src/com/jotuntech/sketcher/client/images/wipe.png b/src/com/jotuntech/sketcher/client/images/wipe.png new file mode 100755 index 0000000..a1c6926 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/wipe.png differ diff --git a/src/com/jotuntech/sketcher/client/images/yellow.png b/src/com/jotuntech/sketcher/client/images/yellow.png new file mode 100755 index 0000000..fc635e0 Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/yellow.png differ diff --git a/src/com/jotuntech/sketcher/client/images/zoom.png b/src/com/jotuntech/sketcher/client/images/zoom.png new file mode 100755 index 0000000..8f1f91f Binary files /dev/null and b/src/com/jotuntech/sketcher/client/images/zoom.png differ diff --git a/src/com/jotuntech/sketcher/client/voice/Highpass24.java b/src/com/jotuntech/sketcher/client/voice/Highpass24.java new file mode 100755 index 0000000..d7efa37 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/voice/Highpass24.java @@ -0,0 +1,34 @@ +package com.jotuntech.sketcher.client.voice; + +/** 4-pole cascaded Butterworth highpass filter */ + +public class Highpass24 { + float li1, li2, li3, li4, lo1, lo2, lo3, lo4; + + public Highpass24() { + li1 = 0; li2 = 0; li3 = 0; li4 = 0; + lo1 = 0; lo2 = 0; lo3 = 0; lo4 = 0; + } + + public float process(float x, float c) { + float a = 1 - c; + + /** 1st pole */ + lo1 = a * (lo1 + x - li1); + li1 = x; + + /** 2nd pole */ + lo2 = a * (lo2 + lo1 - li2); + li2 = lo1; + + /** 3rd pole */ + lo3 = a * (lo3 + lo2 - li3); + li3 = lo2; + + /** 4th pole */ + lo4 = a * (lo4 + lo3 - li4); + li4 = lo3; + + return lo4; + } +} diff --git a/src/com/jotuntech/sketcher/client/voice/VoiceChannel.java b/src/com/jotuntech/sketcher/client/voice/VoiceChannel.java new file mode 100755 index 0000000..860d354 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/voice/VoiceChannel.java @@ -0,0 +1,197 @@ +package com.jotuntech.sketcher.client.voice; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import org.xiph.speex.SpeexDecoder; + + +public class VoiceChannel { + public final static int SPEEX_FRAME_BYTES = 640; + + private SpeexDecoder decoder; + private ByteBuffer[] packetArray; + private int expectedPacketNumber; + private int newestPacketNumber; + private float qualityPercent; + private ByteBuffer audioBuffer; + private VoiceMixer voiceMixer; + private Integer channelKey; + private boolean buffering; + private long lastPlayTime; + private boolean dead; + private VoiceListener listener; + + public VoiceChannel(VoiceMixer voiceMixer, Integer channelKey) { + this.voiceMixer = voiceMixer; + this.channelKey = channelKey; + + packetArray = new ByteBuffer[voiceMixer.getBufferSize() / SPEEX_FRAME_BYTES]; + expectedPacketNumber = -1; + newestPacketNumber = 0; + qualityPercent = 50f; + + decoder = new SpeexDecoder(); + decoder.init(1, 16000, 1, true); + + buffering = true; + + lastPlayTime = System.currentTimeMillis(); + dead = false; + } + + public void packet(ByteBuffer packetBuffer) throws IOException { + if(lastPlayTime < System.currentTimeMillis() - 10000) { + dead = true; + return; + } + + if(packetBuffer.remaining() < 4) { + /** Packet too short to even contain its own number */ + return; + } + + int packetNumber = packetBuffer.getInt(); + if(expectedPacketNumber == -1) { + /** Lock on first packet in sequence */ + expectedPacketNumber = packetNumber; + } + if(packetNumber - expectedPacketNumber > packetArray.length - 1) { + /** Packet overflow */ + + /** Decode the N packets up to, and including, the overflowed packet */ + expectedPacketNumber = packetNumber - packetArray.length + 1; + newestPacketNumber = packetNumber; + + /** Place overflowing packet at head of the queue */ + packetArray[packetNumber % packetArray.length] = packetBuffer; + + if(listener != null) { + listener.voiceEvent(new VoiceEvent(VoiceEvent.TYPE_PACKET_OVERFLOW, channelKey)); + } + } else if(packetNumber < expectedPacketNumber) { + /** Packet arrived too late and is dropped */ + } else { + /** Packet has arrived in order. */ + packetArray[packetNumber % packetArray.length] = packetBuffer; + if(packetNumber > newestPacketNumber) { + /** Tell decoder to expect newer packets. */ + newestPacketNumber = packetNumber; + } + } + } + + public void process() throws IOException { + if(lastPlayTime < System.currentTimeMillis() - 10000) { + dead = true; + return; + } + + /** Hold off playback until we are buffered up */ + if(expectedPacketNumber == -1 || (buffering && newestPacketNumber - expectedPacketNumber < packetArray.length - 1)) { + if(listener != null) { + int bufferPercent = expectedPacketNumber == -1 ? 0 : Math.max(0, ((newestPacketNumber - expectedPacketNumber) * 200) / packetArray.length); + listener.voiceEvent(new VoiceEvent(VoiceEvent.TYPE_PACKET_BUFFERING, channelKey, bufferPercent)); + } + return; + } + + buffering = false; + + /** Attempt to decode packets if there is room for them in the voice mixer buffer. */ + while(voiceMixer.available(channelKey) >= SPEEX_FRAME_BYTES) { + /** Look for the next expected packet */ + ByteBuffer packetBuffer = packetArray[expectedPacketNumber % packetArray.length]; + if(packetBuffer == null) { + /** We have no packet. What to do? */ + if(voiceMixer.available(channelKey) >= voiceMixer.getBufferSize() / 2) { + /** Playback buffer is half empty, so we "decode" a lost packet. */ + decoder.processData(true); + + /** Reduce quality average for missing packets */ + qualityPercent -= qualityPercent * 0.1f; + + if(expectedPacketNumber > newestPacketNumber) { + /** Packet underflow */ + buffering = true; + expectedPacketNumber = -1; + if(listener != null) { + int bufferPercent = Math.max(0, ((newestPacketNumber - expectedPacketNumber) * 200) / packetArray.length); + listener.voiceEvent(new VoiceEvent(VoiceEvent.TYPE_PACKET_UNDERFLOW, channelKey, bufferPercent, (int) qualityPercent)); + } + break; + } else { + /** Packet missing */ + ++expectedPacketNumber; + if(listener != null) { + int bufferPercent = Math.max(0, ((newestPacketNumber - expectedPacketNumber) * 200) / packetArray.length); + listener.voiceEvent(new VoiceEvent(VoiceEvent.TYPE_PACKET_LOST, channelKey, bufferPercent, (int) qualityPercent)); + } + } + } else { + /** Playback buffer is good, so we wait for the packet. */ + break; + } + } else { + /** We have a packet and we retrieve it. */ + byte[] packetBufferArray = packetBuffer.array(); + int dataOffset = packetBuffer.arrayOffset() + packetBuffer.position(); + int dataLength = packetBuffer.remaining(); + + /** Decode the packet. */ + decoder.processData(packetBufferArray, dataOffset, dataLength); + + /** Erase packet and increment counter */ + packetArray[expectedPacketNumber++ % packetArray.length] = null; + + /** Update last play time */ + lastPlayTime = System.currentTimeMillis(); + + if(listener != null) { + int bufferPercent = ((newestPacketNumber - expectedPacketNumber) * 200) / packetArray.length; + qualityPercent += (100 - qualityPercent) * 0.1f; + listener.voiceEvent(new VoiceEvent(VoiceEvent.TYPE_PACKET_RECEIVED, channelKey, bufferPercent, (int) qualityPercent)); + } + } + + /** Read the decoded packet into audio buffer */ + int audioByteLength = decoder.getProcessedDataByteSize(); + if(audioBuffer == null || audioByteLength > audioBuffer.capacity()) { + audioBuffer = ByteBuffer.allocate(audioByteLength); + audioBuffer.order(ByteOrder.LITTLE_ENDIAN); + System.err.println("Created " + audioByteLength + " byte audio buffer."); + } + audioBuffer.clear(); + decoder.getProcessedData(audioBuffer.array(), audioBuffer.arrayOffset() + audioBuffer.position()); + audioBuffer.position(audioBuffer.position() + audioByteLength); + audioBuffer.flip(); + + if(listener != null) { + /** Measure peak volume of audio buffer */ + int peak = 0; + for(int i = audioBuffer.position(); i < audioBuffer.limit(); i += 2) { + int absSample = Math.abs(audioBuffer.getShort(i)); + if(absSample > peak) { + peak = absSample; + } + } + + /** Notify listener of peak volume */ + listener.voiceEvent(new VoiceEvent(VoiceEvent.TYPE_PACKET_VOLUME, channelKey, peak)); + } + + + /** Write audio buffer to voice mixer. */ + voiceMixer.write(channelKey, audioBuffer); + } + } + + public boolean isDead() { + return dead; + } + + public void setListener(VoiceListener listener) { + this.listener = listener; + } +} \ No newline at end of file diff --git a/src/com/jotuntech/sketcher/client/voice/VoiceClient.java b/src/com/jotuntech/sketcher/client/voice/VoiceClient.java new file mode 100755 index 0000000..0df3c45 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/voice/VoiceClient.java @@ -0,0 +1,310 @@ +package com.jotuntech.sketcher.client.voice; + +import java.io.IOException; +import java.net.DatagramSocket; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.DatagramChannel; +import java.util.HashMap; +import java.util.Map; + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.LineUnavailableException; +import javax.sound.sampled.TargetDataLine; + +import org.xiph.speex.SpeexEncoder; + + +public class VoiceClient extends Thread { + private static final int bufferSize = VoiceChannel.SPEEX_FRAME_BYTES * 4; + private DatagramChannel channel; + private VoiceMixer voiceMixer; + private Map voiceChannels; + private Integer peerKey; + private AudioFormat targetFormat; + private TargetDataLine target; + private SpeexEncoder encoder; + private ByteBuffer recordBuffer; + private InetSocketAddress serverAddress; + private int packetNumber; + private boolean bigEndian = false; + private float gain, gain2; + private VoiceListener listener; + private Highpass24 highpass; + private VoiceDetector detector; + + public VoiceClient(InetSocketAddress serverAddress, Integer peerKey) { + super("VoiceClient"); + setPriority(Thread.MAX_PRIORITY); + voiceChannels = new HashMap(); + this.serverAddress = serverAddress; + this.peerKey = peerKey; + packetNumber = 0; + bigEndian = false; + targetFormat = new AudioFormat(16000, 16, 1, true, false); + gain = 1f; + gain2 = 1f; + } + + public void run() { + try { + channel = DatagramChannel.open(); + channel.configureBlocking(true); + DatagramSocket socket = channel.socket(); + socket.bind(new InetSocketAddress(0)); + System.err.println("Bound to address " + socket.getLocalAddress().toString() + ":" + socket.getLocalPort()); + channel.configureBlocking(false); + + voiceMixer = new VoiceMixer(VoiceChannel.SPEEX_FRAME_BYTES * 16); + + System.err.println("Voice client ready."); + + while(!interrupted()) { + sleep(10); + + if(target == null) { + try { + target = AudioSystem.getTargetDataLine(targetFormat); + } catch(IllegalArgumentException e) { + System.err.println("Mono 16-bit 16000 Hz little-endian target data line not supported."); + System.err.println("Java exception:"); + e.printStackTrace(); + System.err.println("Attempting to open big-endian target data line instead."); + targetFormat = new AudioFormat(16000, 16, 1, true, true); + try { + target = AudioSystem.getTargetDataLine(targetFormat); + bigEndian = true; + } catch(IllegalArgumentException e2) { + System.err.println("Big-endian target data line not supported either."); + System.err.println("Java exception:"); + e.printStackTrace(); + System.err.println("Continuing in playback-only mode!"); + if(listener != null) { + listener.voiceEvent(new VoiceEvent(VoiceEvent.TYPE_WARN, "The sound device does not support the required format.")); + } + } + } + + if(target != null) { + target.open(targetFormat, VoiceChannel.SPEEX_FRAME_BYTES * 4); + target.start(); + System.err.println("Target data line successfully opened and started."); + } + } + + if(target != null) { + if(recordBuffer == null) { + recordBuffer = ByteBuffer.allocate(bufferSize); + if(bigEndian) { + recordBuffer.order(ByteOrder.BIG_ENDIAN); + System.err.println("Allocated " + bufferSize + " byte big-endian recording buffer."); + } else { + recordBuffer.order(ByteOrder.LITTLE_ENDIAN); + System.err.println("Allocated " + bufferSize + " byte little-endian recording buffer."); + } + highpass = new Highpass24(); + detector = new VoiceDetector(16000f, 0.1f); + } + + recordBuffer.clear(); + + while(target.available() >= VoiceChannel.SPEEX_FRAME_BYTES && recordBuffer.remaining() >= VoiceChannel.SPEEX_FRAME_BYTES) { + int bytesRead = target.read(recordBuffer.array(), recordBuffer.arrayOffset() + recordBuffer.position(), VoiceChannel.SPEEX_FRAME_BYTES); + recordBuffer.position(recordBuffer.position() + bytesRead); + } + + recordBuffer.flip(); + + if(recordBuffer.remaining() >= VoiceChannel.SPEEX_FRAME_BYTES) { + if(bigEndian) { + for(int i = recordBuffer.position(); i < recordBuffer.limit(); i += 2) { + byte a = recordBuffer.get(i); + byte b = recordBuffer.get(i + 1); + recordBuffer.put(i, b); + recordBuffer.put(i + 1, a); + } + } + + float minimumChange = 0.01f; + //float avgChange = 0f; + //float numSamples = recordBuffer.remaining() / 2f; + + int peakSample = 0; + + for(int i = recordBuffer.position(); i < recordBuffer.limit(); i += 2) { + int rawSample = recordBuffer.getShort(i); + int absSample = Math.abs(rawSample); + + if(absSample > peakSample) { + peakSample = absSample; + } + + /** Get sample and perform 100 Hz highpass */ + float sample = highpass.process(rawSample / 32767f, 0.0125f); + + /** Detect voice activity */ + float change = detector.process(sample); + //avgChange += change; + + /** Apply automatic gain */ + float gainSample = sample * gain; + + /** Absolute sample with gain applied */ + float absGainSample = Math.abs(gainSample); + + if(gain > 1f && absGainSample >= 1f) { + /** Brickwall limit output to 0dB */ + gain /= absGainSample; + gainSample = sample * gain; + } else if(gain > 1f && absGainSample > 1f) { + /** 2 second release */ + gain -= gain * 0.00003125f; + } else if(gain < 8f && absGainSample > 0 && absGainSample < 1f) { + /** 2 second attack */ + gain += gain * 0.00003125f; + } + + if(gain2 < 1f && change > minimumChange) { + /** 1/16th second attack */ + gain2 += (1f - gain2) * 0.001f; + } else if(gain2 > 0.03125f && change < minimumChange) { + /** 1 second release */ + gain2 -= gain2 * 0.000125f; + } + + /** Apply voice activity gain */ + gainSample *= gain2; + + /** Clip sample */ + if(gainSample < -1f) { + gainSample = -1f; + } else if(gainSample > 1f) { + gainSample = 1f; + } + + recordBuffer.putShort(i, (short) (gainSample * 32767f)); + } + + //avgChange /= numSamples; + + if(listener != null) { + listener.voiceEvent(new VoiceEvent(VoiceEvent.TYPE_PACKET_VOLUME, peerKey, peakSample)); + } + + //System.err.println("Gain = " + gain + ", Gain2 = " + gain2 + ", µChange = " + (long) (avgChange * 1000000f)); + + while(recordBuffer.remaining() >= VoiceChannel.SPEEX_FRAME_BYTES) { + if(encoder == null) { + /** Start encoder */ + encoder = new SpeexEncoder(); + encoder.init(1, 4, 16000, 1); + if(listener != null) { + listener.voiceEvent(new VoiceEvent(VoiceEvent.TYPE_START, null, target.getBufferSize())); + } + System.err.println("Speex encoder initialized and started."); + } + + if(encoder.processData(recordBuffer.array(), recordBuffer.arrayOffset() + recordBuffer.position(), VoiceChannel.SPEEX_FRAME_BYTES)) { + recordBuffer.position(recordBuffer.position() + VoiceChannel.SPEEX_FRAME_BYTES); + int bytesProcessed = encoder.getProcessedDataByteSize(); + ByteBuffer packetBuffer = ByteBuffer.allocate(bytesProcessed + 8); + packetBuffer.putInt(peerKey); + packetBuffer.putInt(packetNumber); + encoder.getProcessedData(packetBuffer.array(), packetBuffer.arrayOffset() + packetBuffer.position()); + packetBuffer.position(packetBuffer.position() + bytesProcessed); + packetBuffer.flip(); + int bytesSent = channel.send(packetBuffer, serverAddress); + if(bytesSent > 0) { + ++packetNumber; + } + } else { + if(listener != null) { + listener.voiceEvent(new VoiceEvent(VoiceEvent.TYPE_ERROR, "An unknown error occurred while encoding a voice packet.")); + } + System.err.println("An unknown error occurred while encoding a voice packet."); + return; + } + } + } + } /** End of recording specific section */ + + ByteBuffer packetBuffer = ByteBuffer.allocate(VoiceChannel.SPEEX_FRAME_BYTES + 8); + for(SocketAddress address = channel.receive(packetBuffer); address != null; address = channel.receive(packetBuffer)) { + packetBuffer.flip(); + if(packetBuffer.remaining() > 4) { + Integer voiceChannelKey = packetBuffer.getInt(); + VoiceChannel voiceChannel = voiceChannels.get(voiceChannelKey); + if(voiceChannel == null) { + voiceChannel = new VoiceChannel(voiceMixer, voiceChannelKey); + voiceChannels.put(voiceChannelKey, voiceChannel); + System.err.println("Creating new voice channel #" + voiceChannelKey); + if(listener != null) { + voiceChannel.setListener(listener); + listener.voiceEvent(new VoiceEvent(VoiceEvent.TYPE_CHANNEL_NEW, voiceChannelKey)); + } + } + + voiceChannel.packet(packetBuffer); + } + packetBuffer = ByteBuffer.allocate(VoiceChannel.SPEEX_FRAME_BYTES + 8); + } + + for(VoiceChannel voiceChannel : voiceChannels.values()) { + voiceChannel.process(); + } + + /** Remove dead voice channels, one at a time, to avoid ConcurrentModificationException */ + for(Map.Entry e : voiceChannels.entrySet()) { + if(e.getValue().isDead()) { + System.err.println("Voice channel #" + e.getKey() + " is dead and was dropped."); + voiceMixer.drop(e.getKey()); + voiceChannels.remove(e.getKey()); + if(listener != null) { + listener.voiceEvent(new VoiceEvent(VoiceEvent.TYPE_CHANNEL_DEAD, e.getKey())); + } + break; + } + } + + if(!voiceMixer.mix()) { + break; + } + } + } catch (IOException e) { + if(listener != null) { + listener.voiceEvent(new VoiceEvent(VoiceEvent.TYPE_ERROR, e.getMessage())); + } + } catch (InterruptedException e) { + + } catch (LineUnavailableException e) { + if(listener != null) { + listener.voiceEvent(new VoiceEvent(VoiceEvent.TYPE_ERROR, "The sound device is busy.")); + } + } + + voiceMixer.close(); + + if(target != null) { + target.stop(); + target.close(); + } + + try { channel.close(); } catch(IOException e) { } + + if(listener != null) { + listener.voiceEvent(new VoiceEvent(VoiceEvent.TYPE_STOP)); + } + + System.err.println("Voice client terminated."); + } + + public void setListener(VoiceListener listener) { + this.listener = listener; + for(VoiceChannel channel : voiceChannels.values()) { + channel.setListener(listener); + } + } +} diff --git a/src/com/jotuntech/sketcher/client/voice/VoiceDetector.java b/src/com/jotuntech/sketcher/client/voice/VoiceDetector.java new file mode 100755 index 0000000..072ced7 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/voice/VoiceDetector.java @@ -0,0 +1,39 @@ +package com.jotuntech.sketcher.client.voice; + +import java.util.Arrays; + +/** Class for detecting voice activity. Calculates a moving root-mean-square + * and stores measurements in a delay line. Returns the absolute difference + * between the current and oldest RMS measurements. The length of the + * delay line and the moving average is determined by the 'windows' variable, + * specified in seconds (fraction of the sample rate). + */ + +public class VoiceDetector { + private float delayLine[]; + private float alpha; + private float mean; + + public VoiceDetector(float sampleRate, float window) { + delayLine = new float[(int) (sampleRate * window)]; + Arrays.fill(delayLine, 0f); + alpha = 1f / delayLine.length; + mean = 0f; + } + + public float process(float x) { + /** Shift delay line backwards by one sample */ + for(int i = 1; i < delayLine.length; i++) { + delayLine[i - 1] = delayLine[i]; + } + + /** Calculate moving Root-Mean */ + mean += (Math.sqrt(Math.abs(x)) - mean) * alpha; + + /** Place at head of delay line */ + delayLine[delayLine.length - 1] = mean; + + /** Return difference of Root-Means */ + return Math.abs(mean - delayLine[0]); + } +} diff --git a/src/com/jotuntech/sketcher/client/voice/VoiceEvent.java b/src/com/jotuntech/sketcher/client/voice/VoiceEvent.java new file mode 100755 index 0000000..e7502f3 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/voice/VoiceEvent.java @@ -0,0 +1,120 @@ +package com.jotuntech.sketcher.client.voice; + +public class VoiceEvent { + /** No-operation type */ + public final static int TYPE_NONE = 0; + + /** Sent after voice client and recording line is running */ + public final static int TYPE_START = 10; + + /** Sent after voice client has shut down completely */ + public final static int TYPE_STOP = 20; + + /** Sent if there was an error starting the recording or playback lines */ + public final static int TYPE_ERROR = 30; + + /** Sent if there was a non-critical error starting the recording or playback lines */ + public final static int TYPE_WARN = 31; + + /** Sent when a channel is receiving too many packets (non-critical) */ + public final static int TYPE_PACKET_OVERFLOW = 40; + + /** Sent when a channel is receiving too few packets (non-critical) */ + public final static int TYPE_PACKET_UNDERFLOW = 50; + + /** Sent when a channel packet is received normally */ + public final static int TYPE_PACKET_RECEIVED = 60; + + /** Sent when a channel packet is lost (non-critical) */ + public final static int TYPE_PACKET_LOST = 70; + + /** Sent when a channel is receiving packets for buffering */ + public final static int TYPE_PACKET_BUFFERING = 80; + + /** Sent when a new voice channel is added */ + public final static int TYPE_CHANNEL_NEW = 90; + + /** Sent when a voice channel dies */ + public final static int TYPE_CHANNEL_DEAD = 100; + + /** Volume, sent when a packet is decoded */ + public final static int TYPE_PACKET_VOLUME = 110; + + private int type; + private Integer channel; + private int data1; + private int data2; + private String errorMessage; + + public VoiceEvent() { + type = TYPE_NONE; + } + + public VoiceEvent(int type) { + this.type = type; + } + + public VoiceEvent(int type, String errorMessage) { + this.type = type; + this.errorMessage = errorMessage; + } + + public VoiceEvent(int type, Integer channel) { + this.type = type; + this.channel = channel; + } + + public VoiceEvent(int type, Integer channel, int data1) { + this.type = type; + this.channel = channel; + this.data1 = data1; + } + + public VoiceEvent(int type, Integer channel, int data1, int data2) { + this.type = type; + this.channel = channel; + this.data1 = data1; + this.data2 = data2; + } + + public VoiceEvent(int type, Integer channel, String errorMessage) { + this.type = type; + this.channel = channel; + this.errorMessage = errorMessage; + } + + /** Returns a type from list of constants above */ + public int getType() { + return type; + } + + /** Channel number the event is coming from or null if none */ + public Integer getChannel() { + return channel; + } + + /** Error message associated with event or null if none */ + public String getErrorMessage() { + return errorMessage; + } + + /** Buffer size percentage or 0 if none */ + public int getBufferPercent() { + return data1; + } + + /** Recording buffer size in bytes, or 0 if none */ + public int getBufferSize() { + return data1; + } + + /** Moving average of quality (packet received vs packet lost) percentage or 0 if none */ + public int getQualityPercent() { + return data2; + } + + /** Peak volume of this packet */ + public int getVolume() { + return data1; + } +} diff --git a/src/com/jotuntech/sketcher/client/voice/VoiceListener.java b/src/com/jotuntech/sketcher/client/voice/VoiceListener.java new file mode 100755 index 0000000..704c3fd --- /dev/null +++ b/src/com/jotuntech/sketcher/client/voice/VoiceListener.java @@ -0,0 +1,6 @@ +package com.jotuntech.sketcher.client.voice; + + +public interface VoiceListener { + void voiceEvent(VoiceEvent e); +} diff --git a/src/com/jotuntech/sketcher/client/voice/VoiceMixer.java b/src/com/jotuntech/sketcher/client/voice/VoiceMixer.java new file mode 100755 index 0000000..50cfb9c --- /dev/null +++ b/src/com/jotuntech/sketcher/client/voice/VoiceMixer.java @@ -0,0 +1,172 @@ +package com.jotuntech.sketcher.client.voice; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.HashMap; +import java.util.Map; + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.LineUnavailableException; +import javax.sound.sampled.SourceDataLine; + +public class VoiceMixer { + private SourceDataLine source; + private AudioFormat sourceFormat; + private int bufferSize; + private Map channelMap; + private ByteBuffer mixBuffer; + + public VoiceMixer(int bufferSize) { + sourceFormat = new AudioFormat(16000, 16, 1, true, false); + channelMap = new HashMap(); + this.bufferSize = bufferSize; + } + + public void write(Integer channelKey, ByteBuffer data) { + ByteBuffer channel = channelMap.get(channelKey); + if(channel == null) { + /** Open new channel */ + channel = ByteBuffer.allocate(bufferSize); + channel.order(ByteOrder.LITTLE_ENDIAN); + channelMap.put(channelKey, channel); + } + channel.put(data); + } + + public int available(Integer channelKey) { + ByteBuffer channel = channelMap.get(channelKey); + if(channel == null) { + return bufferSize; + } + + return channel.remaining(); + } + + public void drop(Integer channelKey) { + channelMap.remove(channelKey); + } + + public boolean mix() { + if(channelMap.size() == 0) { + /** No mixing or playback takes place unless we have channels */ + return true; + } + + /** Attempt to mix when playback buffer is half empty */ + if(source == null || source.available() >= source.getBufferSize() / 2) { + /** We can not mix more audio than we can write to the source */ + int maximumWrite = source == null ? bufferSize : source.available(); + int underflows = 0; + + /** Scan available channels */ + for(Map.Entry e : channelMap.entrySet()) { + ByteBuffer channel = e.getValue(); + + /** Flip channel for reading */ + channel.flip(); + + /** Shorten mix to shortest individual channel, excluding too short ones */ + if(channel.remaining() <= bufferSize / 2) { + /** Channel is underflowing and we will not include it in the mix */ + //System.err.println("Channel " + e.getKey() + " underflowing."); + ++underflows; + } else if(channel.remaining() < maximumWrite) { + /** Channel has the least remaining audio and we must shorten the mix */ + maximumWrite = channel.remaining(); + } + } + + if(underflows == channelMap.size()) { + /** Do not write anything if all channels are empty */ + maximumWrite = 0; + } + + if(maximumWrite > 0) { + /** Prepare the mix buffer */ + if(mixBuffer == null) { + mixBuffer = ByteBuffer.allocate(bufferSize); + mixBuffer.order(ByteOrder.LITTLE_ENDIAN); // Speex uses 16-bit little-endian samples + } + mixBuffer.clear(); + mixBuffer.limit(maximumWrite); + + boolean firstChannel = true; + for(ByteBuffer channel : channelMap.values()) { + /** Ignore underflowing channels */ + if(channel.remaining() > bufferSize / 2) { + /** Position buffer for mixing a new channel */ + mixBuffer.position(0); + + for(int i = 0; i < maximumWrite; i += 2) { + /** Mix samples additively */ + int sample = channel.getShort(); + if(firstChannel) { + mixBuffer.putShort((short) sample); + } else { + sample += mixBuffer.getShort(i); + /** Handle clipping */ + if(sample > 32767) { + mixBuffer.putShort((short) 32767); + } else if(sample < -32768) { + mixBuffer.putShort((short) -32768); + } else { + /** Write mixed sample to mix buffer */ + mixBuffer.putShort((short) sample); + } + } + } + + firstChannel = false; + } + } + + /** Flip mix buffer for reading (i.e. writing to source) */ + mixBuffer.flip(); + + if(mixBuffer.remaining() > 0) { + if(source == null) { + try { + /** Start source data line */ + System.err.println("Voice mixer opening source data line for playback."); + source = AudioSystem.getSourceDataLine(sourceFormat); + source.open(sourceFormat, bufferSize); + source.start(); + bufferSize = source.getBufferSize(); + } catch (LineUnavailableException e) { + System.err.println("Voice mixer unable to open target data line."); + System.err.println("Java exception:"); + e.printStackTrace(); + return false; + } + } + + /** Write mix to source */ + source.write(mixBuffer.array(), mixBuffer.arrayOffset(), mixBuffer.limit()); + } + } + + /** Prepare channels, previously flipped under scan, for writing again */ + for(ByteBuffer channel : channelMap.values()) { + channel.compact(); + } + } + + return true; + } + + public void close() { + if(source != null) { + source.stop(); + source.close(); + } + } + + public int getBufferSize() { + if(source == null) { + return bufferSize; + } else { + return source.getBufferSize(); + } + } +} diff --git a/src/com/jotuntech/sketcher/client/voice/aec/AEC.java b/src/com/jotuntech/sketcher/client/voice/aec/AEC.java new file mode 100755 index 0000000..36efa95 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/voice/aec/AEC.java @@ -0,0 +1,237 @@ +package com.jotuntech.sketcher.client.voice.aec; + +public class AEC { + /* dB Values */ + final static float M0dB = 1.0f; + final static float M3dB = 0.71f; + final static float M6dB = 0.50f; + final static float M9dB = 0.35f; + final static float M12dB = 0.25f; + final static float M18dB = 0.125f; + final static float M24dB = 0.063f; + + /* dB values for 16bit PCM */ + /* MxdB_PCM = 32767 * 10 ^(x / 20) */ + final static float M10dB_PCM = 10362.0f; + final static float M20dB_PCM = 3277.0f; + final static float M25dB_PCM = 1843.0f; + final static float M30dB_PCM = 1026.0f; + final static float M35dB_PCM = 583.0f; + final static float M40dB_PCM = 328.0f; + final static float M45dB_PCM = 184.0f; + final static float M50dB_PCM = 104.0f; + final static float M55dB_PCM = 58.0f; + final static float M60dB_PCM = 33.0f; + final static float M65dB_PCM = 18.0f; + final static float M70dB_PCM = 10.0f; + final static float M75dB_PCM = 6.0f; + final static float M80dB_PCM = 3.0f; + final static float M85dB_PCM = 2.0f; + final static float M90dB_PCM = 1.0f; + + final static float MAXPCM = 32767.0f; + + /* Design final staticants (Change to fine tune the algorithms */ + + /* The following values are for hardware AEC and studio quality + * microphone */ + + /* maximum NLMS filter length in taps. A longer filter length gives + * better Echo Cancellation, but slower convergence speed and + * needs more CPU power (Order of NLMS is linear) */ + final static int NLMS_LEN = 80 * 8; + + /* convergence speed. Range: >0 to <1 (0.2 to 0.7). Larger values give + * more AEC in lower frequencies, but less AEC in higher frequencies. */ + final static float Stepsize = 0.7f; + + /* minimum energy in xf. Range: M70dB_PCM to M50dB_PCM. Should be equal + * to microphone ambient Noise level */ + final static float Min_xf = M75dB_PCM; + + /* Double Talk Detector Speaker/Microphone Threshold. Range <=1 + * Large value (M0dB) is good for Single-Talk Echo cancellation, + * small value (M12dB) is good for Doulbe-Talk AEC */ + final static float GeigelThreshold = M6dB; + + /* Double Talk Detector hangover in taps. Not relevant for Single-Talk + * AEC */ + final static int Thold = 30 * 8; + + /* for Non Linear Processor. Range >0 to 1. Large value (M0dB) is good + * for Double-Talk, small value (M12dB) is good for Single-Talk */ + final static float NLPAttenuation = M12dB; + + // Extention in taps to reduce mem copies + final static int NLMS_EXT = 10 * 8; + + // block size in taps to optimize DTD calculation + final static int DTD_LEN = 16; + + // Time domain Filters + IIRHP hp00, hp1; // DC-level remove Highpass) + FIRHP13 hp0; // 300Hz cut-off Highpass + IIR1 Fx, Fe; // pre-whitening Highpass for x, e + + // Geigel DTD (Double Talk Detector) + float max_max_x; // max(|x[0]|, .. |x[L-1]|) + int hangover; + // optimize: less calculations for max() + float max_x[]; + int dtdCnt; + int dtdNdx; + + // NLMS-pw + float x[]; // tap delayed loudspeaker signal + float xf[]; // pre-whitening tap delayed signal + float w[]; // tap weights + int j; // optimize: less memory copies + int lastupdate; // optimize: iterative dotp(x,x) + double dotp_xf_xf; // double to avoid loss of precision + double Min_dotp_xf_xf; + float s0avg; + + /* Vector Dot Product */ + private float dotp(float a[], float b[], int boffset) { + float sum0 = 0.0f, sum1 = 0.0f; + int j; + + for (j = 0; j < NLMS_LEN; j += 2) { + // optimize: partial loop unrolling + sum0 += a[j] * b[j + boffset]; + sum1 += a[j + 1] * b[j + 1 + boffset]; + } + return sum0 + sum1; + } + + public float getAmbient() { + return s0avg; + }; + + public void setAmbient(float Min_xf) { + dotp_xf_xf = Min_dotp_xf_xf = NLMS_LEN * Min_xf * Min_xf; + }; + + public AEC() { + hp00 = new IIRHP(); + hp1 = new IIRHP(); + hp0 = new FIRHP13(); + Fx = new IIR1(); + Fe = new IIR1(); + + max_x = new float[NLMS_LEN / DTD_LEN]; + + x = new float[NLMS_LEN + NLMS_EXT]; + xf = new float[NLMS_LEN + NLMS_EXT]; + w = new float[NLMS_LEN]; + + max_max_x = 0.0f; + hangover = 0; + dtdCnt = dtdNdx = 0; + + j = NLMS_EXT; + lastupdate = 0; + s0avg = M80dB_PCM; + setAmbient(Min_xf); + } + + public float nlms_pw(float mic, float spk, boolean update) { + float d = mic; // desired signal + x[j] = spk; + xf[j] = Fx.highpass(spk); // pre-whitening of x + + // calculate error value + // (mic signal - estimated mic signal from spk signal) + float e = d - dotp(w, x, j); + float ef = Fe.highpass(e); // pre-whitening of e + // optimize: iterative dotp(xf, xf) + dotp_xf_xf += (xf[j] * xf[j] - xf[j + NLMS_LEN - 1] * xf[j + NLMS_LEN - 1]); + if (update) { + // calculate variable step size + float mikro_ef = (float) (Stepsize * ef / dotp_xf_xf); + + // update tap weights (filter learning) + int i; + for (i = 0; i < NLMS_LEN; i += 2) { + // optimize: partial loop unrolling + w[i] += mikro_ef * xf[i + j]; + w[i+1] += mikro_ef * xf[i + j + 1]; + } + } + + if (--j < 0) { + // optimize: decrease number of memory copies + j = NLMS_EXT; + System.arraycopy(x, 0, x, j + 1, NLMS_LEN - 1); + System.arraycopy(xf, 0, xf, j + 1, NLMS_LEN - 1); + } + + return e; + } + + public boolean dtd(float d, float x) { + // optimized implementation of max(|x[0]|, |x[1]|, .., |x[L-1]|): + // calculate max of block (DTD_LEN values) + x = Math.abs(x); + if (x > max_x[dtdNdx]) { + max_x[dtdNdx] = x; + if (x > max_max_x) { + max_max_x = x; + } + } + if (++dtdCnt >= DTD_LEN) { + dtdCnt = 0; + // calculate max of max + max_max_x = 0.0f; + for (int i = 0; i < NLMS_LEN/DTD_LEN; ++i) { + if (max_x[i] > max_max_x) { + max_max_x = max_x[i]; + } + } + // rotate Ndx + if (++dtdNdx >= NLMS_LEN/DTD_LEN) dtdNdx = 0; + max_x[dtdNdx] = 0.0f; + } + + // The Geigel DTD algorithm with Hangover timer Thold + if (Math.abs(d) >= GeigelThreshold * max_max_x) { + hangover = Thold; + } + + if(hangover != 0) --hangover; + + return (hangover > 0); + } + + + public float doAEC(int d, int x) { + float s0 = (float)d; + float s1 = (float)x; + + // Mic Highpass Filter - to remove DC + s0 = hp00.highpass(s0); + + // Mic Highpass Filter - telephone users are used to 300Hz cut-off + s0 = hp0.highpass(s0); + + // ambient mic level estimation + s0avg += 1e-4f * (Math.abs(s0) - s0avg); + + // Spk Highpass Filter - to remove DC + s1 = hp1.highpass(s1); + + // Double Talk Detector + boolean update = !dtd(s0, s1); + + // Acoustic Echo Cancellation + s0 = nlms_pw(s0, s1, update); + + // Acoustic Echo Suppression + if (update) { + // Non Linear Processor (NLP): attenuate low volumes + s0 *= NLPAttenuation; + } + + return s0; + } +} diff --git a/src/com/jotuntech/sketcher/client/voice/aec/FIRHP13.java b/src/com/jotuntech/sketcher/client/voice/aec/FIRHP13.java new file mode 100755 index 0000000..dd6cdf8 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/voice/aec/FIRHP13.java @@ -0,0 +1,42 @@ +package com.jotuntech.sketcher.client.voice.aec; + +/* 13 taps FIR Finite Impulse Response filter + * Coefficients calculated with + * www.dsptutor.freeuk.com/KaiserFilterDesign/KaiserFilterDesign.html + */ + +public class FIRHP13 { + private final static float a[] = { + // Kaiser Window FIR Filter, Filter type: High pass + // Passband: 300.0 - 4000.0 Hz, Order: 12 + // Transition band: 100.0 Hz, Stopband attenuation: 10.0 dB + -0.043183226f, -0.046636667f, -0.049576525f, -0.051936015f, + -0.053661242f, -0.054712527f, 0.82598513f, -0.054712527f, + -0.053661242f, -0.051936015f, -0.049576525f, -0.046636667f, + -0.043183226f, 0.0f + }; + + private float z[]; + + public FIRHP13() { + z = new float[14]; + } + + float highpass(float in) { + System.arraycopy(z, 0, z, 1, z.length - 1); + for(int i = a.length - 2; i >= 0; i--) { + z[i + 1] = z[i]; + } + z[0] = in; + + float sum0 = 0.0f, sum1 = 0.0f; + int j; + for (j = 0; j < a.length; j += 2) { + // optimize: partial loop unrolling + sum0 += a[j] * z[j]; + sum1 += a[j + 1] * z[j + 1]; + } + + return sum0 + sum1; + } +} diff --git a/src/com/jotuntech/sketcher/client/voice/aec/IIR1.java b/src/com/jotuntech/sketcher/client/voice/aec/IIR1.java new file mode 100755 index 0000000..78ded82 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/voice/aec/IIR1.java @@ -0,0 +1,24 @@ +package com.jotuntech.sketcher.client.voice.aec; + +/* Recursive single pole IIR Infinite Impulse response filter + * Coefficients calculated with + * http://www.dsptutor.freeuk.com/IIRFilterDesign/IIRFiltDes102.html + */ + +public class IIR1 { + private final static float a0 = 0.105831884f; + private final static float a1 = -0.105831884f; + private final static float b1 = 0.78833646f; + + private float x, y; + + float highpass(float in) { + // Chebyshev IIR filter, Filter type: HP + // Passband: 3700 - 4000.0 Hz + // Passband ripple: 1.5 dB, Order: 1 + float out = a0 * in + a1 * x + b1 * y; + x = in; + y = out; + return out; + } +} \ No newline at end of file diff --git a/src/com/jotuntech/sketcher/client/voice/aec/IIR2.java b/src/com/jotuntech/sketcher/client/voice/aec/IIR2.java new file mode 100755 index 0000000..a55d286 --- /dev/null +++ b/src/com/jotuntech/sketcher/client/voice/aec/IIR2.java @@ -0,0 +1,36 @@ +package com.jotuntech.sketcher.client.voice.aec; + +/* Recursive two pole IIR Infinite Impulse Response filter + * Coefficients calculated with + * http://www.dsptutor.freeuk.com/IIRFilterDesign/IIRFiltDes102.html + */ + +public class IIR2 { + private final static float a[] = { 0.29289323f, -0.58578646f, 0.29289323f }; + private final static float b[] = { 1.3007072E-16f, 0.17157288f }; + + private float x[], y[]; + + public IIR2() { + x = new float[2]; + y = new float[2]; + } + + float highpass(float in) { + // Butterworth IIR filter, Filter type: HP + // Passband: 2000 - 4000.0 Hz, Order: 2 + float out = + a[0] * in + + a[1] * x[0] + + a[2] * x[1] - + b[0] * y[0] - + b[1] * y[1]; + + x[1] = x[0]; + x[0] = in; + y[1] = y[0]; + y[0] = out; + + return out; + } +} \ No newline at end of file diff --git a/src/com/jotuntech/sketcher/client/voice/aec/IIRHP.java b/src/com/jotuntech/sketcher/client/voice/aec/IIRHP.java new file mode 100755 index 0000000..bbc7e3c --- /dev/null +++ b/src/com/jotuntech/sketcher/client/voice/aec/IIRHP.java @@ -0,0 +1,16 @@ +package com.jotuntech.sketcher.client.voice.aec; + +/* Exponential Smoothing or IIR Infinite Impulse Response Filter */ + +public class IIRHP { + /* Transfer Frequency */ + private final static float a0 = 0.01f; + + private float x; + + float highpass(float in) { + /* Highpass = Signal - Lowpass. Lowpass = Exponential Smoothing */ + x += a0 * (in - x); + return in - x; + } +} \ No newline at end of file diff --git a/src/com/jotuntech/sketcher/common/BitmapLayer.java b/src/com/jotuntech/sketcher/common/BitmapLayer.java new file mode 100755 index 0000000..d2bc00e --- /dev/null +++ b/src/com/jotuntech/sketcher/common/BitmapLayer.java @@ -0,0 +1,962 @@ +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); + } + } +} diff --git a/src/com/jotuntech/sketcher/common/BitmapTile.java b/src/com/jotuntech/sketcher/common/BitmapTile.java new file mode 100755 index 0000000..ae1fdfe --- /dev/null +++ b/src/com/jotuntech/sketcher/common/BitmapTile.java @@ -0,0 +1,232 @@ +/** + * + */ +package com.jotuntech.sketcher.common; + +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.io.IOException; +import java.io.Serializable; + +/** + * @author Thor Harald Johansen + * + */ +public class BitmapTile implements Serializable { + static final long serialVersionUID = 3265680755614445636L; + + public final static int SIZE = 32; + public final static int SIZE_2 = 5; + public final static int SIZE_MASK = 31; + + private BufferedImage image; + private int[] pixels; + private byte[] encoded; + + /** + * Creates a bitmap tile + */ + public BitmapTile() { + image = new BufferedImage(SIZE, SIZE, BufferedImage.TYPE_INT_ARGB); + pixels = ((DataBufferInt)image.getRaster().getDataBuffer()).getData(); + } + + public BitmapTile(BufferedImage imagex) { + image = new BufferedImage(SIZE, SIZE, BufferedImage.TYPE_INT_ARGB); + pixels = ((DataBufferInt)image.getRaster().getDataBuffer()).getData(); + + image.getGraphics().drawImage(imagex, 0, 0, null); + } + + public BitmapTile(BitmapTile tile) { + image = new BufferedImage(SIZE, SIZE, BufferedImage.TYPE_INT_ARGB); + tile.image.copyData(image.getRaster()); + pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + } + + /* + public void blendPixel(int x, int y, int sourceColor, int sourceAlpha65025) { + // Offset in destination pixel array + int offset = (y << SIZE_2) + x; + + // Fetch destionation pixel + int destinationPixel = pixels[offset]; + + // Get destination's alpha + int destinationAlpha = Pixels.getChannel0(destinationPixel); + + // Divide source alpha + int sourceAlpha = sourceAlpha65025 / 255; + + if(sourceAlpha > 0) { + if(sourceAlpha == 255) { + pixels[offset] = Pixels.pack(255, sourceColor); + } else { + if(destinationAlpha == 0) { + int dither = sourceAlpha < 255 && sourceAlpha65025 % 255 > Pixels.random() ? 1 : 0; + pixels[offset] = Pixels.pack(sourceAlpha + dither, sourceColor); + } else { + // Destination alpha result + int destinationAlphaResult, destinationAlphaResult65025; + + if(destinationAlpha == 255) { + destinationAlphaResult = 255; + destinationAlphaResult65025 = 65025; + } else { + destinationAlphaResult65025 = Math.min(65205, destinationAlpha * 255 + sourceAlpha65025); + destinationAlphaResult = destinationAlphaResult65025 / 255; + destinationAlphaResult += destinationAlphaResult < 255 && destinationAlphaResult65025 % 255 > Pixels.random() ? 1 : 0; + } + + if(sourceColor == Pixels.stripChannel0(destinationPixel)) { + pixels[offset] = Pixels.pack(destinationAlphaResult, sourceColor); + } else { + // Destination colors + int destinationRed = Pixels.getChannel1(destinationPixel); + int destinationGreen = Pixels.getChannel2(destinationPixel); + int destinationBlue = Pixels.getChannel3(destinationPixel); + + // Source colors + int sourceRed = Pixels.getChannel1(sourceColor); + int sourceGreen = Pixels.getChannel2(sourceColor); + int sourceBlue = Pixels.getChannel3(sourceColor); + + if(destinationAlphaResult == 255) { + int outputRed = destinationRed + ((sourceRed - destinationRed) * sourceAlpha65025) / 65025; + int outputGreen = destinationGreen + ((sourceGreen - destinationGreen) * sourceAlpha65025) / 65025; + int outputBlue = destinationBlue + ((sourceBlue - destinationBlue) * sourceAlpha65025) / 65025; + + // Blend, and store to destination + pixels[offset] = Pixels.pack(255, + outputRed, + outputGreen, + outputBlue); + } else { + // Blend, and store to destination + pixels[offset] = Pixels.pack(destinationAlphaResult, + destinationRed + ((sourceRed - destinationRed) * sourceAlpha65025) / destinationAlphaResult65025, + destinationGreen + ((sourceGreen - destinationGreen) * sourceAlpha65025) / destinationAlphaResult65025, + destinationBlue + ((sourceBlue - destinationBlue) * sourceAlpha65025) / destinationAlphaResult65025); + } + } + } + } + } + } + */ + + public void blendPixel(int x, int y, int sourceColor, int sourceAlpha65025, int maxAlpha) { + // Offset in destination pixel array + int offset = (y << SIZE_2) + x; + + // Fetch destionation pixel + int destinationPixel = pixels[offset]; + + // Get destination's alpha + int destinationAlpha = Pixels.getChannel0(destinationPixel); + + // Divide source alpha + int sourceAlpha = sourceAlpha65025 / 255; + + if(sourceAlpha > 0) { + if(sourceAlpha == 255) { + pixels[offset] = Pixels.pack(255, sourceColor); + } else { + if(destinationAlpha == 0) { + int dither = sourceAlpha65025 % 255 > Pixels.random() ? 1 : 0; + pixels[offset] = Pixels.pack((sourceAlpha & 0xFFFFFFFE) | dither, sourceColor); + } else { + // Destination alpha result + int destinationAlphaResult, destinationAlphaResult65025; + + if(destinationAlpha == 255) { + destinationAlphaResult = 255; + destinationAlphaResult65025 = 65025; + } else { + // Destination multiplied by inverted source alpha. + int invSrcAlphaMulDest65025 = (destinationAlpha * (65025 - sourceAlpha65025)) / 255; + + // Set destination alpha result + destinationAlphaResult65025 = Math.max(destinationAlpha * 255, Math.min(maxAlpha * 255, sourceAlpha65025 + invSrcAlphaMulDest65025)); + destinationAlphaResult = destinationAlphaResult65025 / 255; + destinationAlphaResult = (destinationAlphaResult & 0xFFFFFFFE) | (destinationAlphaResult65025 % 255 > Pixels.random() ? 1 : 0); + } + + if(sourceColor == Pixels.stripChannel0(destinationPixel)) { + if(destinationAlphaResult == 255) { + pixels[offset] = Pixels.pack(255, sourceColor); + } else { + pixels[offset] = Pixels.pack(destinationAlphaResult, sourceColor); + } + } else { + // Destination colors + int destinationRed = Pixels.getChannel1(destinationPixel); + int destinationGreen = Pixels.getChannel2(destinationPixel); + int destinationBlue = Pixels.getChannel3(destinationPixel); + + // Source colors + int sourceRed = Pixels.getChannel1(sourceColor); + int sourceGreen = Pixels.getChannel2(sourceColor); + int sourceBlue = Pixels.getChannel3(sourceColor); + + if(destinationAlphaResult == 255) { + int outputRed = destinationRed + ((sourceRed - destinationRed) * sourceAlpha65025) / 65025; + int outputGreen = destinationGreen + ((sourceGreen - destinationGreen) * sourceAlpha65025) / 65025; + int outputBlue = destinationBlue + ((sourceBlue - destinationBlue) * sourceAlpha65025) / 65025; + + // Blend, and store to destination + pixels[offset] = Pixels.pack(255, + outputRed, + outputGreen, + outputBlue); + } else { + // Blend, and store to destination + pixels[offset] = Pixels.pack(destinationAlphaResult, + destinationRed + ((sourceRed - destinationRed) * sourceAlpha65025) / destinationAlphaResult65025, + destinationGreen + ((sourceGreen - destinationGreen) * sourceAlpha65025) / destinationAlphaResult65025, + destinationBlue + ((sourceBlue - destinationBlue) * sourceAlpha65025) / destinationAlphaResult65025); + } + } + } + } + } + } + + private void writeObject(java.io.ObjectOutputStream out) throws IOException { + out.writeObject(pixels); + } + + private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { + this.image = new BufferedImage(SIZE, SIZE, BufferedImage.TYPE_INT_ARGB); + this.pixels = ((DataBufferInt)image.getRaster().getDataBuffer()).getData(); + + int[] pixels = (int[])in.readObject(); + for(int i = 0; i < pixels.length; i++) { + this.pixels[i] = pixels[i]; + } + } + + public void setImage(BufferedImage image) { + this.image = image; + } + + public BufferedImage getImage() { + return image; + } + + public void setPixels(int[] pixels) { + this.pixels = pixels; + } + + public int[] getPixels() { + return pixels; + } + + public void setEncoded(byte[] encoded) { + this.encoded = encoded; + } + + public byte[] getEncoded() { + return encoded; + } +} diff --git a/src/com/jotuntech/sketcher/common/BitmapTileDiff.java b/src/com/jotuntech/sketcher/common/BitmapTileDiff.java new file mode 100755 index 0000000..68187d6 --- /dev/null +++ b/src/com/jotuntech/sketcher/common/BitmapTileDiff.java @@ -0,0 +1,15 @@ +package com.jotuntech.sketcher.common; + +public class BitmapTileDiff { + public short diffAlpha[]; + public short diffRed[]; + public short diffGreen[]; + public short diffBlue[]; + + public BitmapTileDiff() { + diffAlpha = new short[BitmapTile.SIZE * BitmapTile.SIZE]; + diffRed = new short[BitmapTile.SIZE * BitmapTile.SIZE]; + diffGreen = new short[BitmapTile.SIZE * BitmapTile.SIZE]; + diffBlue = new short[BitmapTile.SIZE * BitmapTile.SIZE]; + } +} diff --git a/src/com/jotuntech/sketcher/common/BitmapUndoData.java b/src/com/jotuntech/sketcher/common/BitmapUndoData.java new file mode 100755 index 0000000..df33c51 --- /dev/null +++ b/src/com/jotuntech/sketcher/common/BitmapUndoData.java @@ -0,0 +1,56 @@ +package com.jotuntech.sketcher.common; + +import java.awt.Point; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map.Entry; + +public class BitmapUndoData extends HashMap implements UndoData { + protected Iterator> tileIterator = null; + protected static DiffPacker packer; + protected static DiffUnpacker unpacker; + + public void reset() { + tileIterator = null; + } + + public byte[] encode() { + if(tileIterator == null) { + tileIterator = entrySet().iterator(); + } + + if(tileIterator.hasNext()) { + Entry e = tileIterator.next(); + Point p = e.getKey(); + BitmapTileDiff btd = e.getValue(); + byte[] encoded = new byte[BitmapTile.SIZE * BitmapTile.SIZE * 8 + 2]; + int offset = 0; + encoded[offset++] = (byte) p.x; + encoded[offset++] = (byte) p.y; + + if(packer == null) { + packer = new DiffPacker(); + } + + packer.pack(btd.diffAlpha, btd.diffRed, btd.diffGreen, btd.diffBlue, encoded, offset, encoded.length - offset); + + return encoded; + } else { + tileIterator = null; + return null; + } + } + + public void decode(byte[] in) { + int offset = 0; + int x = 0xFF & in[offset++]; + int y = 0xFF & in[offset++]; + Point p = new Point(x, y); + BitmapTileDiff btd = new BitmapTileDiff(); + if(unpacker == null) { + unpacker = new DiffUnpacker(); + } + unpacker.unpack(in, offset, in.length - offset, btd.diffAlpha, btd.diffRed, btd.diffGreen, btd.diffBlue); + put(p, btd); + } +} diff --git a/src/com/jotuntech/sketcher/common/Brush.java b/src/com/jotuntech/sketcher/common/Brush.java new file mode 100755 index 0000000..010c460 --- /dev/null +++ b/src/com/jotuntech/sketcher/common/Brush.java @@ -0,0 +1,269 @@ +package com.jotuntech.sketcher.common; + +import java.nio.ByteBuffer; + +/** + * Class for containting brush parameters for painting. + * + * @author Thor Harald Johansen + */ +public class Brush implements Streamable { + /** Display friendly name of brush. */ + private String name; + + /** Opacity of brush daub */ + private int opacity = 0xFF; + + /** Flow of brush daub. */ + private int flow = 0x20; + + /** Radius of brush daub. */ + private float radius = 32f; + + /** Hardness of brush daub. */ + private int hardness = 1; + + /** Spacing of brush daubs. */ + private float spacing = 0.0f; + + /** Route pressure to opacity of brush stroke. */ + private boolean pressureToOpacity = true; + + /** Route pressure to opacity of brush daub. */ + private boolean pressureToFlow = false; + + /** Route pressure to radius of brush daub. */ + private boolean pressureToRadius = true; + + /** Jitter of brush daub position. */ + private float jitter = 0; + + /** Amount of pixel noise applied to brush daub. */ + private int noise = 0; + + /** Degree of watercolor effect to apply to brush */ + private int water = 0; + + /** Sampling area for watercolor effect */ + private float waterArea = 1f; + + /** Lock transparency */ + private boolean lockTransparency = false; + + /** + * Constructs a new brush based on parameters. + * + * @param opacity A value from -0xFF to -0x01 (for erasers) and 0x01 to 0xFF + * (for brushes) specifying the opacity of the brush daub. + * @param flow A value from 0 to 0xFF specifying the flow of the brush daub. + * @param radius A value from 1.5 to 126 specifying the radius of the brush + * daub. + * @param hardness hardness A HARDNESS_xxx... constant specifying + * the hardness of the brush daub. + * @param spacing A value from 0.0 to 1.0 specifying the spacing of brush + * daubs as a fraction of the brush diameter. + * @param pressureToFlow Enables/disables the routing of tablet pressure + * to brush daub opacity. + * @param pressureToRadius Enables/disables the routing of tables pressure + * to brush daub radius. + * @param jitter A value from 0 to infinity specifying the brush position + * jitter as the fraction of the brush daub diameter. + * @param noise A value from 0x00 to 0xFF specifying the amount of pixel + * noise to add to the brush daub. + * @param water A value from 0x00 to 0xFF specifying the amount of environmental + * pickup done by the brush daub. + */ + public Brush(String name, int opacity, int flow, float radius, int hardness, float spacing, boolean pressureToOpacity, boolean pressureToFlow, boolean pressureToRadius, float jitter, int noise, int water, boolean lockTransparency, float waterArea) { + this.name = name; + this.opacity = opacity; + this.flow = flow; + this.radius = radius; + this.hardness = hardness; + this.spacing = spacing; + this.pressureToOpacity = pressureToOpacity; + this.pressureToFlow = pressureToFlow; + this.pressureToRadius = pressureToRadius; + this.jitter = jitter; + this.noise = noise; + this.water = water; + this.waterArea = waterArea; + this.lockTransparency = lockTransparency; + } + + /** Constructs a new default brush. */ + public Brush() { + + } + + /** + * Constructs a new brush. + * + * @param name Display name of the brush. + */ + public Brush(String name) { + this.name = name; + } + + /** + * Returns display friendly name of brush + * @return Brush name + */ + public String getName() { + return name; + } + + /** + * Set display friendly name of brush + * @param name Brush name + */ + public void setName(String name) { + this.name = name; + } + + /** + * Returns copy of this brush. + * + * @return Copy of brush. + */ + public Brush copy() { + return new Brush(name, opacity, flow, radius, hardness, spacing, pressureToOpacity, pressureToFlow, pressureToRadius, jitter, noise, water, lockTransparency, waterArea); + } + + public void decode(ByteBuffer in) { + opacity = in.getShort(); + flow = in.getShort(); + radius = in.getFloat(); + hardness = in.get(); + spacing = in.getFloat(); + pressureToOpacity = in.get() != 0 ? true : false; + pressureToFlow = in.get() != 0 ? true : false; + pressureToRadius = in.get() != 0 ? true : false; + jitter = in.getFloat(); + noise = in.getShort(); + water = in.getShort(); + waterArea = in.getFloat(); + lockTransparency = in.get() != 0 ? true : false; + name = in.asCharBuffer().toString(); + } + + public void encode(ByteBuffer out) { + out.putShort((short) opacity); + out.putShort((short) flow); + out.putFloat(radius); + out.put((byte) hardness); + out.putFloat(spacing); + out.put((byte) (pressureToOpacity ? 0xFF : 0x00)); + out.put((byte) (pressureToFlow ? 0xFF : 0x00)); + out.put((byte) (pressureToRadius ? 0xFF : 0x00)); + out.putFloat(jitter); + out.putShort((short) noise); + out.putShort((short) water); + out.putFloat(waterArea); + out.put((byte) (lockTransparency ? 0xFF : 0x00)); + out.asCharBuffer().put(name); + } + + public void setOpacity(int opacity) { + this.opacity = opacity; + } + + public int getOpacity() { + return opacity; + } + + public void setFlow(int flow) { + this.flow = flow; + } + + public int getFlow() { + return flow; + } + + public void setRadius(float radius) { + this.radius = radius; + } + + public float getRadius() { + return radius; + } + + public void setHardness(int hardness) { + this.hardness = hardness; + } + + public int getHardness() { + return hardness; + } + + public void setSpacing(float spacing) { + this.spacing = spacing; + } + + public float getSpacing() { + return spacing; + } + + public void setPressureToFlow(boolean pressureToFlow) { + this.pressureToFlow = pressureToFlow; + } + + public boolean isPressureToFlow() { + return pressureToFlow; + } + + public void setPressureToRadius(boolean pressureToRadius) { + this.pressureToRadius = pressureToRadius; + } + + public boolean isPressureToRadius() { + return pressureToRadius; + } + + public void setJitter(float jitter) { + this.jitter = jitter; + } + + public float getJitter() { + return jitter; + } + + public void setNoise(int noise) { + this.noise = noise; + } + + public int getNoise() { + return noise; + } + + public void setWater(int water) { + this.water = water; + } + + public int getWater() { + return water; + } + + public void setWaterArea(float waterArea) { + this.waterArea = waterArea; + } + + public float getWaterArea() { + return waterArea; + } + + public void setLockTransparency(boolean lockTransparency) { + this.lockTransparency = lockTransparency; + } + + public boolean isLockTransparency() { + return lockTransparency; + } + + public void setPressureToOpacity(boolean pressureToOpacity) { + this.pressureToOpacity = pressureToOpacity; + } + + public boolean isPressureToOpacity() { + return pressureToOpacity; + } +} diff --git a/src/com/jotuntech/sketcher/common/Canvas.java b/src/com/jotuntech/sketcher/common/Canvas.java new file mode 100755 index 0000000..4e067dc --- /dev/null +++ b/src/com/jotuntech/sketcher/common/Canvas.java @@ -0,0 +1,78 @@ +package com.jotuntech.sketcher.common; + +import java.awt.Image; +import java.awt.image.ImageObserver; +import java.util.LinkedHashSet; +import java.util.Set; + +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +public class Canvas implements ImageObserver { + private int width, height; + private TwoWayHashMap layerMap; + private Set observers; + private volatile boolean drawing; + + public Canvas(int width, int height) { + this.drawing = false; + + this.width = width; + this.height = height; + + this.layerMap = new TwoWayHashMap(); + + this.observers = new LinkedHashSet(); + + getLayerMap().addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent e) { + addImageObserverToLayers(); + } + }); + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public TwoWayHashMap getLayerMap() { + return layerMap; + } + + public void addImageObserver(ImageObserver observer) { + observers.add(observer); + } + + public Set getImageObservers() { + return observers; + } + + public ImageObserver getImageObserver() { + return this; + } + + private void addImageObserverToLayers() { + for(Layer l : getLayerMap().values()) { + l.addImageObserver(this); + } + } + + public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) { + for(ImageObserver o : observers) { + o.imageUpdate(img, infoflags, x, y, width, height); + } + return true; + } + + public void setDrawing(boolean drawing) { + this.drawing = drawing; + } + + public boolean isDrawing() { + return drawing; + } +} diff --git a/src/com/jotuntech/sketcher/common/ConsoleOutputStream.java b/src/com/jotuntech/sketcher/common/ConsoleOutputStream.java new file mode 100755 index 0000000..ebdab0f --- /dev/null +++ b/src/com/jotuntech/sketcher/common/ConsoleOutputStream.java @@ -0,0 +1,24 @@ +package com.jotuntech.sketcher.common; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; + +import javax.swing.JTextArea; + +public class ConsoleOutputStream extends OutputStream { + JTextArea textArea; + + public ConsoleOutputStream(JTextArea textArea) { + this.textArea = textArea; + + PrintStream consolePrintStream = new PrintStream(this); + System.setOut(consolePrintStream); + System.setErr(consolePrintStream); + } + + public void write(int ch) throws IOException { + textArea.append((char)ch + ""); + textArea.setCaretPosition(textArea.getText().length()); + } +} \ No newline at end of file diff --git a/src/com/jotuntech/sketcher/common/Copyable.java b/src/com/jotuntech/sketcher/common/Copyable.java new file mode 100755 index 0000000..5ef03f0 --- /dev/null +++ b/src/com/jotuntech/sketcher/common/Copyable.java @@ -0,0 +1,5 @@ +package com.jotuntech.sketcher.common; + +public interface Copyable { + public V copy(); +} diff --git a/src/com/jotuntech/sketcher/common/DiffPacker.java b/src/com/jotuntech/sketcher/common/DiffPacker.java new file mode 100755 index 0000000..c896c1d --- /dev/null +++ b/src/com/jotuntech/sketcher/common/DiffPacker.java @@ -0,0 +1,55 @@ +package com.jotuntech.sketcher.common; + +import java.util.zip.Deflater; + +public class DiffPacker { + private Deflater deflater; + private byte[] encodedDiff; + + public DiffPacker() { + deflater = new Deflater(Deflater.BEST_COMPRESSION, true); + } + + public int pack(short[] diffAlpha, short[] diffRed, short[] diffGreen, short[] diffBlue, byte[] output) { + return pack(diffAlpha, diffRed, diffGreen, diffBlue, output, 0, output.length); + } + + public int pack(short[] diffAlpha, short[] diffRed, short[] diffGreen, short[] diffBlue, byte[] output, int ofs, int len) { + /* Create buffers, or enlarge them if they are too small */ + if(encodedDiff == null || encodedDiff.length < diffAlpha.length * 8) { + encodedDiff = new byte[diffAlpha.length * 8]; + } + + int lastAlpha = 0, lastRed = 0, lastGreen = 0, lastBlue = 0; + for(int diffOffset = 0, alphaOffset = 0, redOffset = diffAlpha.length * 2, greenOffset = diffAlpha.length * 4, blueOffset = diffAlpha.length * 6; diffOffset < diffAlpha.length; diffOffset++, alphaOffset++, redOffset++, greenOffset++, blueOffset++) { + short alpha = (short) (diffAlpha[diffOffset] - lastAlpha); + short red = (short) (diffRed[diffOffset] - lastRed); + short green = (short) (diffGreen[diffOffset] - lastGreen); + short blue = (short) (diffBlue[diffOffset] - lastBlue); + + encodedDiff[alphaOffset++] = (byte) (0xFF & (alpha >> 8)); + encodedDiff[alphaOffset] = (byte) (0xFF & alpha); + encodedDiff[redOffset++] = (byte) (0xFF & (red >> 8)); + encodedDiff[redOffset] = (byte) (0xFF & red); + encodedDiff[greenOffset++] = (byte) (0xFF & (green >> 8)); + encodedDiff[greenOffset] = (byte) (0xFF & green); + encodedDiff[blueOffset++] = (byte) (0xFF & (blue >> 8)); + encodedDiff[blueOffset] = (byte) (0xFF & blue); + + lastAlpha = diffAlpha[diffOffset]; + lastRed = diffRed[diffOffset]; + lastGreen = diffGreen[diffOffset]; + lastBlue = diffBlue[diffOffset]; + } + + /* Deflate encoded diffs */ + deflater.reset(); + deflater.setInput(encodedDiff, 0, diffAlpha.length * 8); + deflater.finish(); + int compressedLength = 0; + while(!deflater.finished() && compressedLength < len) { + compressedLength += deflater.deflate(output, ofs + compressedLength, len - compressedLength); + } + return compressedLength; + } +} diff --git a/src/com/jotuntech/sketcher/common/DiffUnpacker.java b/src/com/jotuntech/sketcher/common/DiffUnpacker.java new file mode 100755 index 0000000..bb3b6de --- /dev/null +++ b/src/com/jotuntech/sketcher/common/DiffUnpacker.java @@ -0,0 +1,49 @@ +package com.jotuntech.sketcher.common; + +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +public class DiffUnpacker { + Inflater inflater; + private byte[] encodedDiff; + + public DiffUnpacker() { + inflater = new Inflater(true); + } + + public void unpack(byte[] input, int ofs, int len, short[] outputAlpha, short[] outputRed, short[] outputGreen, short[] outputBlue) { + if(encodedDiff == null || encodedDiff.length < input.length * 8) { + encodedDiff = new byte[outputAlpha.length * 8]; + } + + inflater.reset(); + inflater.setInput(input, ofs, len); + + int encodedLength = 0; + try { + while(!inflater.finished() && encodedLength < encodedDiff.length) { + encodedLength += inflater.inflate(encodedDiff, encodedLength, encodedDiff.length - encodedLength); + } + } catch(DataFormatException e) { + throw new RuntimeException(e); + } + + if(encodedLength < outputAlpha.length * 8) { + Log.error("needsInput = " + inflater.needsInput()); + throw new RuntimeException("Encoded length " + encodedLength + " is shorter than output length " + outputAlpha.length * 8 + "."); + } + + short alpha = 0, red = 0, green = 0, blue = 0; + for(int outputOffset = 0, alphaOffset = 0, redOffset = outputAlpha.length * 2, greenOffset = outputAlpha.length * 4, blueOffset = outputAlpha.length * 6; outputOffset < outputAlpha.length; outputOffset++, alphaOffset++, redOffset++, greenOffset++, blueOffset++) { + alpha += (short) ((encodedDiff[alphaOffset++] << 8) | (encodedDiff[alphaOffset] & 0xff)); + red += (short) ((encodedDiff[redOffset++] << 8) | (encodedDiff[redOffset] & 0xff)); + green += (short) ((encodedDiff[greenOffset++] << 8) | (encodedDiff[greenOffset] & 0xff)); + blue += (short) ((encodedDiff[blueOffset++] << 8) | (encodedDiff[blueOffset] & 0xff)); + + outputAlpha[outputOffset] = alpha; + outputRed[outputOffset] = red; + outputGreen[outputOffset] = green; + outputBlue[outputOffset] = blue; + } + } +} diff --git a/src/com/jotuntech/sketcher/common/Input.java b/src/com/jotuntech/sketcher/common/Input.java new file mode 100755 index 0000000..a1eacd7 --- /dev/null +++ b/src/com/jotuntech/sketcher/common/Input.java @@ -0,0 +1,69 @@ +package com.jotuntech.sketcher.common; + +import java.awt.Point; +import java.awt.geom.Point2D; +import java.nio.ByteBuffer; + +/** + * Class for containing tablet input data. + * + * @author Thor Harald Johansen + */ +public class Input extends Point2D.Float implements Streamable { + /** Pressure axis */ + public int pressure = 0; + + /** Creates a new input. */ + public Input() { + + } + + public Input(float x, float y) { + super(x, y); + } + + public Input(float x, float y, int pressure) { + this.x = x; + this.y = y; + this.pressure = pressure; + } + + public Input(Point p) { + x = p.x; + y = p.y; + } + + public Input(Point p, int pressure) { + x = p.x; + y = p.y; + this.pressure = pressure; + } + + /** + * Calculates the difference between the input and the argument such that if + * one axis of the input is greater than one axis of the argument, the + * resultant axis will be negative. + * + * @param d Input to calculate difference against. + * @return Difference of inputs. + */ + public Input difference(Input d) { + return new Input(d.x - x, d.y - y, (d.pressure + pressure) >> 1); + } + + public Input clone() { + return new Input(x, y, pressure); + } + + public void decode(ByteBuffer in) { + x = in.getFloat(); + y = in.getFloat(); + pressure = in.getInt(); + } + + public void encode(ByteBuffer out) { + out.putFloat(x); + out.putFloat(y); + out.putInt(pressure); + } +} diff --git a/src/com/jotuntech/sketcher/common/Layer.java b/src/com/jotuntech/sketcher/common/Layer.java new file mode 100755 index 0000000..9ec0646 --- /dev/null +++ b/src/com/jotuntech/sketcher/common/Layer.java @@ -0,0 +1,34 @@ +package com.jotuntech.sketcher.common; + +import java.awt.Graphics2D; +import java.awt.image.ImageObserver; + +/** Generic interface for layers. + * @author Thor Harald Johansen + * + */ +public interface Layer { + public void setName(String name); + public String getName(); + public int getType(); + public void draw(Graphics2D g); + public UndoData copyTo(Layer destination, ImageObserver obs, boolean move, float x, float y, float dx, float dy, float width, float height); + public UndoData merge(Layer fromLayer); + public void undo(UndoData undoData); + public void setAlphaRule(int alphaComposite); + public int getAlphaRule(); + public void setOpacity(float opacity); + public float getOpacity(); + public Layer copy(); + public void clear(); + public Input line(Input start, Input end, int color, Brush brush, Layer originalLayer); + public int getColor(Input position); + public void addImageObserver(ImageObserver observer); + public ImageObserver getImageObserver(); + public void clean(); + public boolean isEmpty(); + public void reset(); + public void decode(byte[] data); + public byte[] encode(); + +} diff --git a/src/com/jotuntech/sketcher/common/LayerType.java b/src/com/jotuntech/sketcher/common/LayerType.java new file mode 100755 index 0000000..dc4ce9e --- /dev/null +++ b/src/com/jotuntech/sketcher/common/LayerType.java @@ -0,0 +1,7 @@ +package com.jotuntech.sketcher.common; + +public class LayerType { + public final static int BITMAP = 1; + public final static int VECTOR = 2; + public final static int BITMAP_PHANTOM = 3; +} diff --git a/src/com/jotuntech/sketcher/common/Log.java b/src/com/jotuntech/sketcher/common/Log.java new file mode 100755 index 0000000..008f9df --- /dev/null +++ b/src/com/jotuntech/sketcher/common/Log.java @@ -0,0 +1,84 @@ +package com.jotuntech.sketcher.common; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +public class Log { + /** Log debug messages? */ + private final static boolean debug = true; + + /** Log info messages? */ + private final static boolean info = true; + + /** Log error messages? */ + private final static boolean error = true; + + /** Log warning messages? */ + private final static boolean warn = true; + + /** Date format */ + private final static DateFormat df = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss", Locale.US); + + /** + * Logs the specified debug message. + * + * @param message Message to log. + */ + public final static void debug(String message) { + if(debug) { + log(df.format(new Date()) + " [DEBUG: " + Thread.currentThread().getName() + "] " + message); + } + } + + /** + * Logs the specified info message. + * + * @param message Message to log. + */ + public final static void info(String message) { + if(info) { + log(df.format(new Date()) + " [INFO: " + Thread.currentThread().getName() + "] " + message); + } + } + + /** + * Logs the specified error message. + * + * @param message Message to log. + */ + public final static void error(String message) { + if(error) { + log(df.format(new Date()) + " [ERROR: " + Thread.currentThread().getName() + "] " + message); + } + } + + public final static void error(Throwable t) { + if(error) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(baos); + t.printStackTrace(ps); + String message = baos.toString(); + ps.close(); + log(df.format(new Date()) + " [ERROR: " + Thread.currentThread().getName() + "] " + message); + } + } + + /** + * Logs the specified warn message. + * + * @param message Message to log. + */ + public final static void warn(String message) { + if(warn) { + log(df.format(new Date()) + " [WARN: " + Thread.currentThread().getName() + "] " + message); + } + } + + private final static void log(String message) { + System.err.println(message); + } +} diff --git a/src/com/jotuntech/sketcher/common/PixelPacker.java b/src/com/jotuntech/sketcher/common/PixelPacker.java new file mode 100755 index 0000000..032753e --- /dev/null +++ b/src/com/jotuntech/sketcher/common/PixelPacker.java @@ -0,0 +1,99 @@ +package com.jotuntech.sketcher.common; + +import java.util.zip.Deflater; + +public class PixelPacker { + private Deflater deflater; + private byte[] encodedPixels = null; + + public PixelPacker() { + deflater = new Deflater(Deflater.BEST_COMPRESSION, true); + } + + public int pack(int[] pixels, byte[] output) { + return pack(pixels, output, 0, output.length); + } + + public int pack(int[] pixels, byte[] output, int ofs, int len) { + /* Create buffers, or enlarge them if they are too small */ + if(encodedPixels == null || encodedPixels.length < pixels.length * 6) { + encodedPixels = new byte[pixels.length * 6]; + } + + int lastAlpha = 0, lastY = 0, lastCb = 256, lastCr = 256; + for(int pixelOffset = 0, YOffset = pixels.length, CbOffset = pixels.length * 2, CrOffset = pixels.length * 4; pixelOffset < pixels.length; pixelOffset++, YOffset++, CbOffset++, CrOffset++) { + int pixel = pixels[pixelOffset]; + + /* Extract alpha channel */ + int alpha = (pixel >> 24) & 0xFF; + + /* Differential transform */ + int deltaAlpha = alpha - lastAlpha; + lastAlpha = alpha; + + /* Convert negative deltas to equivalent positive deltas */ + if(deltaAlpha < 0) { + deltaAlpha = 256 + deltaAlpha; + } + + /* Luma and chroma deltas */ + int deltaY, deltaCb, deltaCr; + + /* Color deltas are zero if alpha is zero, for better compression */ + if(alpha == 0) { + deltaY = 0; + deltaCb = 0; + deltaCr = 0; + } else { + /* Extract color channels */ + int red = (pixel >> 16) & 0xFF; + int green = (pixel >> 8) & 0xFF; + int blue = pixel & 0xFF; + + /* Forward color transform */ + int Y = (red + (green << 1) + blue) >> 2; + int Cb = 256 + blue - green; + int Cr = 256 + red - green; + + /* Differential transform */ + deltaY = Y - lastY; + deltaCb = Cb - lastCb; + deltaCr = Cr - lastCr; + lastY = Y; + lastCb = Cb; + lastCr = Cr; + + /* Convert negative deltas to equivalent positive deltas */ + if(deltaY < 0) { + deltaY = 256 + deltaY; + } + + if(deltaCb < 0) { + deltaCb = 512 + deltaCb; + } + + if(deltaCr < 0) { + deltaCr = 512 + deltaCr; + } + } + + /* Store image channels */ + encodedPixels[pixelOffset] = (byte) deltaAlpha; + encodedPixels[YOffset] = (byte) deltaY; + encodedPixels[CbOffset++] = (byte) (deltaCb >>> 8); + encodedPixels[CbOffset] = (byte) deltaCb; + encodedPixels[CrOffset++] = (byte) (deltaCr >>> 8); + encodedPixels[CrOffset] = (byte) deltaCr; + } + + /* Deflate encoded pixels */ + deflater.reset(); + deflater.setInput(encodedPixels, 0, pixels.length * 6); + deflater.finish(); + int compressedLength = 0; + while(!deflater.finished() && compressedLength < len) { + compressedLength += deflater.deflate(output, ofs + compressedLength, len - compressedLength); + } + return compressedLength; + } +} diff --git a/src/com/jotuntech/sketcher/common/PixelUnpacker.java b/src/com/jotuntech/sketcher/common/PixelUnpacker.java new file mode 100755 index 0000000..12c3027 --- /dev/null +++ b/src/com/jotuntech/sketcher/common/PixelUnpacker.java @@ -0,0 +1,77 @@ +package com.jotuntech.sketcher.common; + +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + + +public class PixelUnpacker { + Inflater inflater; + private byte[] encodedPixels; + + public PixelUnpacker() { + inflater = new Inflater(true); + } + + public void unpack(byte[] input, int ofs, int len, int[] output) { + if(encodedPixels == null || encodedPixels.length < input.length * 6) { + encodedPixels = new byte[output.length * 6]; + } + + inflater.reset(); + inflater.setInput(input, ofs, len); + //Log.debug("Inflating from offset " + ofs + " length " + len); + + int encodedLength = 0; + try { + while(!inflater.finished() && encodedLength < encodedPixels.length) { + encodedLength += inflater.inflate(encodedPixels, encodedLength, encodedPixels.length - encodedLength); + } + } catch(DataFormatException e) { + throw new RuntimeException(e); + } + + if(encodedLength < output.length * 6) { + Log.error("needsInput = " + inflater.needsInput()); + throw new RuntimeException("Encoded length " + encodedLength + " is shorter than output length " + output.length * 6 + "."); + } + + int lastAlpha = 0, lastY = 0, lastCb = 256, lastCr = 256; + for(int outputOffset = 0, YOffset = output.length, CbOffset = output.length * 2, CrOffset = output.length * 4; outputOffset < output.length; outputOffset++, YOffset++, CbOffset++, CrOffset++) { + /* Extract alpha channel delta */ + int deltaAlpha = encodedPixels[outputOffset] & 0xFF; + + /* Differential transform with overflow wrapping */ + int alpha = (lastAlpha + deltaAlpha) & 0xFF; + lastAlpha = alpha; + + /* Color channel deltas are always zero when alpha is zero */ + if(alpha == 0) { + output[outputOffset] = 0; + ++CbOffset; + ++CrOffset; + } else { + /* Extract color channel deltas */ + int deltaY = encodedPixels[YOffset] & 0xFF; + int deltaCb = ((encodedPixels[CbOffset++] & 0xFF) << 8) | (encodedPixels[CbOffset] & 0xFF); + int deltaCr = ((encodedPixels[CrOffset++] & 0xFF) << 8) | (encodedPixels[CrOffset] & 0xFF); + + /* Differental transform with overflow wrapping */ + int Y = (lastY + deltaY) & 0xFF; + int Cb = (lastCb + deltaCb) & 0x1FF; + int Cr = (lastCr + deltaCr) & 0x1FF; + lastY = Y; + lastCb = Cb; + lastCr = Cr; + + /* Inverse color transform */ + Cb -= 256; + Cr -= 256; + int green = Y - ((Cb + Cr) >> 2); + int red = Cr + green; + int blue = Cb + green; + + output[outputOffset] = Pixels.pack(alpha, red, green, blue); + } + } + } +} diff --git a/src/com/jotuntech/sketcher/common/Pixels.java b/src/com/jotuntech/sketcher/common/Pixels.java new file mode 100755 index 0000000..795eac9 --- /dev/null +++ b/src/com/jotuntech/sketcher/common/Pixels.java @@ -0,0 +1,129 @@ +package com.jotuntech.sketcher.common; + + +public class Pixels { + private final static int RANDOM_SIZE = 3079; + private static int[] randomNumbers; + private static int randomCounter; + + public static int random() { + if(randomNumbers == null) { + randomNumbers = new int[RANDOM_SIZE]; + for(int i = 0; i < RANDOM_SIZE; i++) { + randomNumbers[i] = (int) Math.round(Math.random() * 255f); + } + } + + randomCounter %= RANDOM_SIZE; + return randomNumbers[randomCounter++]; + } + + /* + public final static int squareRoot(int a_nInput) + { + int op = a_nInput; + int res = 0; + int one = 1 << 30; // The second-to-top bit is set: use 1u << 14 for uint16_t type; use 1uL<<30 for int type + + // "one" starts at the highest power of four <= than the argument. + while (one > op) { + one >>= 2; + } + + while (one != 0) { + if (op >= res + one) { + op = op - (res + one); + res = res + 2 * one; + } + res >>= 1; + one >>= 2; + } + + // Do arithmetic rounding to nearest integer + if (op > res) { + res++; + } + + return res; + } + */ + + public static int lerp(int destination, int source, int alpha) { + /* mask destination */ + int destinationRB = destination & 0x00FF00FF; + int destinationAG = destination >>> 8 & 0x00FF00FF; + + /* mask source */ + int sourceRB = source & 0x00FF00FF; + int sourceAG = source >>> 8 & 0x00FF00FF; + + /* calc difference */ + int deltaRB = sourceRB - destinationRB; + int deltaAG = sourceAG - destinationAG; + + /* scale difference by alpha */ + deltaRB *= alpha; + deltaAG *= alpha; + deltaRB >>>= 8; + deltaAG >>>= 8; + + /* add delta to destination and mask */ + int RB = (deltaRB + destinationRB) & 0x00FF00FF; + int AG = (deltaAG + destinationAG) << 8 & 0xFF00FF00; + + return RB | AG; + } + + public static int getChannel0(int pixel) { + return pixel >>> 24; + } + + public static int getChannel1(int pixel) { + return (pixel >>> 16) & 0xFF; + } + + public static int getChannel2(int pixel) { + return (pixel >>> 8) & 0xFF; + } + + public static int getChannel3(int pixel) { + return pixel & 0xFF; + } + + public static int stripChannel0(int pixel) { + return pixel & 0xFFFFFF; + } + + public static int pack(int c1, int c2, int c3) { + return (c1 << 16) | (c2 << 8) | c3; + } + + public static int pack(int c0, int color) { + return (c0 << 24) | color; + } + + public static int pack(int c0, int c1, int c2, int c3) { + return (c0 << 24) | (c1 << 16) | (c2 << 8) | c3; + } + + private final static float a = 0.055f; + + public static float gammaDecode(float v) { + v /= 255f; + if(v <= 0.04045d) { + v /= 12.92d; + } else { + v = (float) Math.pow((v + a) / (1d + a), 2.4f); + } + return v; + } + + public static int gammaEncode(float v) { + if(v <= 0.0031308) { + v *= 12.92d; + } else { + v = (1f + a) * (float) Math.pow(v, 1 / 2.4f) - a; + } + return Math.round(v * 255f); + } +} diff --git a/src/com/jotuntech/sketcher/common/Streamable.java b/src/com/jotuntech/sketcher/common/Streamable.java new file mode 100755 index 0000000..f10c4ac --- /dev/null +++ b/src/com/jotuntech/sketcher/common/Streamable.java @@ -0,0 +1,13 @@ +package com.jotuntech.sketcher.common; + +import java.nio.ByteBuffer; + +/** + * Abstract class for streamable objects. + * + * @author Thor Harald Johansen + */ +public interface Streamable { + public abstract void decode(ByteBuffer in); + public abstract void encode(ByteBuffer out); +} diff --git a/src/com/jotuntech/sketcher/common/StreamableUtils.java b/src/com/jotuntech/sketcher/common/StreamableUtils.java new file mode 100755 index 0000000..168c4f0 --- /dev/null +++ b/src/com/jotuntech/sketcher/common/StreamableUtils.java @@ -0,0 +1,17 @@ +package com.jotuntech.sketcher.common; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** Utility methods for the DataIO interface. + * @author Thor Harald Johansen + * + */ +public class StreamableUtils { + public static Streamable create(String className, ByteBuffer in) throws IOException, IllegalAccessException, ClassNotFoundException, InstantiationException { + Streamable streamable = (Streamable)Class.forName(className).newInstance(); + streamable.decode(in); + return streamable; + } + +} diff --git a/src/com/jotuntech/sketcher/common/TwoWayHashMap.java b/src/com/jotuntech/sketcher/common/TwoWayHashMap.java new file mode 100755 index 0000000..a7d16e0 --- /dev/null +++ b/src/com/jotuntech/sketcher/common/TwoWayHashMap.java @@ -0,0 +1,194 @@ +package com.jotuntech.sketcher.common; + +import java.util.Collection; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +/** + * This map is for storing values with keys. A value can be located + * with a key and vice versa. This class will assume that no mutations to the keys + * are made. Values may implement Copyable to get included in copy() operations + * performed on the map. + * + * @author Thor Harald Johansen + * + * @param Type of values this map will store + */ +public class TwoWayHashMap implements Map, Copyable> { + private Map keyMap; + private Map valueMap; + private List changeListeners; + private Integer autoKey; + + /** Create new integer map */ + public TwoWayHashMap() { + keyMap = new LinkedHashMap(); + valueMap = new IdentityHashMap(); + changeListeners = new LinkedList(); + autoKey = new Integer(1); + } + + @SuppressWarnings("unchecked") + public TwoWayHashMap(TwoWayHashMap map) { + keyMap = new LinkedHashMap(); + valueMap = new IdentityHashMap(); + changeListeners = new LinkedList(); + autoKey = map.autoKey; + + for(Map.Entry e : map.keyMap.entrySet()) { + if(e.getValue() instanceof Copyable) { + put(e.getKey(), ((Copyable)e.getValue()).copy()); + } else { + put(e.getKey(), e.getValue()); + } + } + } + + /** Insert value into map under specified key */ + public V put(K key, V value) { + /** Put key into key map, store old value */ + V oldValue = keyMap.put(key, value); + + /** Remove old value from value map */ + valueMap.remove(oldValue); + + /** Put value into value map */ + valueMap.put(value, key); + + /** Notify change listener method */ + change(); + + /** Return old value */ + return oldValue; + } + + /** Insert value into map and auto-assign key + * + * @return Auto-assigned key + */ + @SuppressWarnings("unchecked") + public K put(V value) { + /** Put value into map */ + put((K) autoKey, value); + + /** Increment auto key and return previous one */ + return (K) autoKey++; + } + + /** Remove entry for specified key from map + * + * @return Entry that was removed, or null if no such entry. + */ + public V remove(Object key) { + /** Remove key from key map, store old value */ + V oldValue = keyMap.remove(key); + + /** Remove old value from value map */ + valueMap.remove(oldValue); + + /** Notify change listener method */ + change(); + + /** Return old value */ + return oldValue; + } + + /** Remove entry for specified value from map + * + * @return Key of entry that was removed, or null if no such entry. + */ + public K removeByValue(V value) { + /** Remove old value from value map, store old key */ + K oldKey = valueMap.remove(value); + + /** Remove old key from key map */ + keyMap.remove(oldKey); + + /** Notify change listener method */ + change(); + + /** Return old key */ + return oldKey; + } + + /** Get value for specified key */ + public V get(Object key) { + return keyMap.get(key); + } + + /** Get key for specified value */ + public K getKeyForValue(V value) { + return valueMap.get(value); + } + + /** Get entry set from map */ + public Set> entrySet() { + return keyMap.entrySet(); + } + + /** Get set of keys from map */ + public Set keySet() { + return keyMap.keySet(); + } + + /** Get collection of values from map */ + public Collection values() { + return keyMap.values(); + } + + public void addChangeListener(ChangeListener l) { + changeListeners.add(l); + } + + /** Notify listeners of change to map */ + public void change() { + for(ChangeListener l : changeListeners) { + l.stateChanged(new ChangeEvent(this)); + } + } + + public TwoWayHashMap copy() { + return new TwoWayHashMap(this); + } + + public void clear() { + keyMap.clear(); + valueMap.clear(); + change(); + } + + public void setNextKey(Integer nextKey) { + this.autoKey = nextKey; + } + + public Integer getNextKey() { + return autoKey; + } + + public boolean containsKey(Object key) { + return keyMap.containsKey(key); + } + + public boolean containsValue(Object value) { + return valueMap.containsKey(value); + } + + public boolean isEmpty() { + return keyMap.isEmpty(); + } + + public void putAll(Map m) { + throw new UnsupportedOperationException(); + } + + public int size() { + return keyMap.size(); + } +} diff --git a/src/com/jotuntech/sketcher/common/UndoData.java b/src/com/jotuntech/sketcher/common/UndoData.java new file mode 100755 index 0000000..ebea625 --- /dev/null +++ b/src/com/jotuntech/sketcher/common/UndoData.java @@ -0,0 +1,8 @@ +package com.jotuntech.sketcher.common; + + +public interface UndoData { + public void reset(); + public void decode(byte[] in); + public byte[] encode(); +} diff --git a/src/com/jotuntech/sketcher/common/UndoEntry.java b/src/com/jotuntech/sketcher/common/UndoEntry.java new file mode 100755 index 0000000..ef2c540 --- /dev/null +++ b/src/com/jotuntech/sketcher/common/UndoEntry.java @@ -0,0 +1,24 @@ +package com.jotuntech.sketcher.common; + +import java.lang.ref.WeakReference; + +public class UndoEntry { + private WeakReference layer; + private UndoData undoData; + + public void setLayer(Layer layer) { + this.layer = new WeakReference(layer); + } + + public Layer getLayer() { + return layer.get(); + } + + public void setUndoData(UndoData undoData) { + this.undoData = undoData; + } + + public UndoData getUndoData() { + return undoData; + } +} diff --git a/src/com/jotuntech/sketcher/common/User.java b/src/com/jotuntech/sketcher/common/User.java new file mode 100755 index 0000000..4c0ef1a --- /dev/null +++ b/src/com/jotuntech/sketcher/common/User.java @@ -0,0 +1,136 @@ +package com.jotuntech.sketcher.common; + +import java.awt.Rectangle; +import java.lang.ref.WeakReference; +import java.util.Deque; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; + + +/** + * User context specific data + * + * @author Thor Harald Johansen + */ +public class User implements Comparable { + /** User's name */ + private String name; + + /** Viewer status */ + private boolean viewer; + + /** Current layer */ + private WeakReference layer = new WeakReference(null); + + /** Phantom layer */ + private Layer phantomLayer; + + /** Current brush */ + private Brush brush; + + /** Current color */ + private int color = 0x000000; + + /** Current cursor */ + private Input cursor = new Input(0f, 0f); + + /** Undo deque */ + private Deque undoDeque = new LinkedList(); + + /** Tag position */ + private Rectangle tag = new Rectangle(Short.MAX_VALUE, Short.MAX_VALUE, Short.MAX_VALUE, Short.MAX_VALUE); + + /** Attributes */ + private Map attributes = new HashMap(); + + + public User() { + this.phantomLayer = new BitmapLayer("Phantom Layer"); + } + + public User(String name) { + this.name = name; + this.phantomLayer = new BitmapLayer(name + " Phantom Layer"); + } + + public int compareTo(User o) { + return getName().compareTo(((User)o).getName()); + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setLayer(Layer layer) { + this.layer = new WeakReference(layer); + } + + public Layer getLayer() { + return layer.get(); + } + + public void setBrush(Brush brush) { + this.brush = brush; + } + + public Brush getBrush() { + return brush; + } + + public void setColor(int color) { + this.color = color; + } + + public int getColor() { + return color; + } + + public void setCursor(Input cursor) { + this.cursor = cursor; + } + + public Input getCursor() { + return cursor; + } + + public void setTag(Rectangle tag) { + this.tag = tag; + } + + public Rectangle getTag() { + return tag; + } + + public void setPhantomLayer(Layer phantomLayer) { + this.phantomLayer = phantomLayer; + } + + public Layer getPhantomLayer() { + return phantomLayer; + } + + public void setAttribute(String key, Object value) { + attributes.put(key, value); + } + + public Object getAttribute(String key) { + return attributes.get(key); + } + + public Deque getUndoDeque() { + return undoDeque; + } + + public void setViewer(boolean viewer) { + this.viewer = viewer; + } + + public boolean isViewer() { + return viewer; + } +} diff --git a/src/com/jotuntech/sketcher/common/filter/AutoContrastFilter.java b/src/com/jotuntech/sketcher/common/filter/AutoContrastFilter.java new file mode 100755 index 0000000..02edb70 --- /dev/null +++ b/src/com/jotuntech/sketcher/common/filter/AutoContrastFilter.java @@ -0,0 +1,101 @@ +package com.jotuntech.sketcher.common.filter; + +import com.jotuntech.sketcher.common.Pixels; + +public class AutoContrastFilter implements Filter { + int[] alphaHistogram, colorHistogram; + int alphaBlackPoint, alphaWhitePoint, colorBlackPoint, colorWhitePoint, totalPixels, alphaRange, colorRange; + float maxBlackFraction, maxWhiteFraction; + + public AutoContrastFilter() { + + } + + public void setSize(int width, int height) { } + + public void setParameterA(float a) { + maxBlackFraction = a; + } + + public void setParameterB(float b) { + maxWhiteFraction = b; + } + + public void setParameterC(float c) { } + + public boolean isPass1ReadOnly() { + return true; + } + + public boolean hasPass2() { + return true; + } + + public boolean isPass2Reversed() { + return false; + } + + public void startPass1() { + totalPixels = 0; + alphaHistogram = new int[256]; + colorHistogram = new int[256]; + } + + public int processPass1Pixel(int pixel) { + int alpha = Pixels.getChannel0(pixel); + if(alpha > 0) { + ++alphaHistogram[alpha]; + ++colorHistogram[(Pixels.getChannel1(pixel) + (Pixels.getChannel2(pixel) << 1) + Pixels.getChannel3(pixel)) >> 2]; + ++totalPixels; + } + return pixel; + } + + public void startPass2() { + int maxBlackPixels = (int) Math.ceil(maxBlackFraction * totalPixels); + int maxWhitePixels = (int) Math.ceil(maxWhiteFraction * totalPixels); + + alphaBlackPoint = 0; + for(int alphaBlackPixels = 0; alphaBlackPixels + alphaHistogram[alphaBlackPoint] <= maxBlackPixels && alphaBlackPoint < alphaHistogram.length; alphaBlackPixels += alphaHistogram[alphaBlackPoint++]) { } + + alphaWhitePoint = alphaHistogram.length - 1; + for(int alphaWhitePixels = 0; alphaWhitePixels + alphaHistogram[alphaWhitePoint] <= maxWhitePixels && alphaWhitePoint >= 0; alphaWhitePixels += alphaHistogram[alphaWhitePoint--]) { } + + System.err.println("alphaBlackPoint = " + alphaBlackPoint); + System.err.println("alphaWhitePoint = " + alphaWhitePoint); + + alphaRange = alphaWhitePoint - alphaBlackPoint; + + if(alphaRange <= 0) { + alphaBlackPoint = 0; + alphaWhitePoint = 255; + alphaRange = 255; + } + + colorBlackPoint = 0; + for(int colorBlackPixels = 0; colorBlackPixels + colorHistogram[colorBlackPoint] <= maxBlackPixels && colorBlackPoint < colorHistogram.length; colorBlackPixels += colorHistogram[colorBlackPoint++]) { } + + colorWhitePoint = colorHistogram.length - 1; + for(int colorWhitePixels = 0; colorWhitePixels + colorHistogram[colorWhitePoint] <= maxWhitePixels && colorWhitePoint >= 0; colorWhitePixels += colorHistogram[colorWhitePoint--]) { } + + System.err.println("colorBlackPoint = " + colorBlackPoint); + System.err.println("colorWhitePoint = " + colorWhitePoint); + + colorRange = colorWhitePoint - colorBlackPoint; + + if(colorRange <= 0) { + colorBlackPoint = 0; + colorWhitePoint = 255; + colorRange = 255; + } + } + + public int processPass2Pixel(int pixel) { + int alpha = Math.min(255, Math.max(0, Pixels.getChannel0(pixel) - alphaBlackPoint) * 255 / alphaRange); + int red = Math.min(255, Math.max(0, Pixels.getChannel1(pixel) - colorBlackPoint) * 255 / colorRange); + int green = Math.min(255, Math.max(0, Pixels.getChannel2(pixel) - colorBlackPoint) * 255 / colorRange); + int blue = Math.min(255, Math.max(0, Pixels.getChannel3(pixel) - colorBlackPoint) * 255 / colorRange); + return Pixels.pack(alpha, red, green, blue); + } + +} diff --git a/src/com/jotuntech/sketcher/common/filter/BlurFilter.java b/src/com/jotuntech/sketcher/common/filter/BlurFilter.java new file mode 100755 index 0000000..fe6144e --- /dev/null +++ b/src/com/jotuntech/sketcher/common/filter/BlurFilter.java @@ -0,0 +1,120 @@ +package com.jotuntech.sketcher.common.filter; + +import com.jotuntech.sketcher.common.Pixels; + +public class BlurFilter implements Filter { + float xAlpha, xRed, xGreen, xBlue, cutoff; + float[] yAlpha, yRed, yGreen, yBlue; + int width, height, x, y; + boolean xAlphaWait = false; + boolean[] yAlphaWait; + + public BlurFilter() { + + } + + public void setSize(int width, int height) { + this.width = width; + this.height = height; + } + + public void setParameterA(float a) { + this.cutoff = 1 / a; + } + + public void setParameterB(float b) { } + + public void setParameterC(float c) { } + + public int processPass1Pixel(int pixel) { + float inAlpha = Pixels.getChannel0(pixel); + float inRed = Pixels.getChannel1(pixel); + float inGreen = Pixels.getChannel2(pixel); + float inBlue = Pixels.getChannel3(pixel); + + float outAlpha, outRed, outGreen, outBlue; + if(x == 0) { + xAlpha = inAlpha; + xRed = inRed; + xGreen = inGreen; + xBlue = inBlue; + } else { + xAlpha += (inAlpha - xAlpha) * cutoff; + if(inAlpha < 1) { + xAlphaWait = true; + } else { + xAlphaWait = false; + xRed += (inRed - xRed) * cutoff; + xGreen += (inGreen - xGreen) * cutoff; + xBlue += (inBlue - xBlue) * cutoff; + } + } + + if(y == 0) { + outAlpha = xAlpha; + outRed = xRed; + outGreen = xGreen; + outBlue = xBlue; + } else { + outAlpha = yAlpha[x] + (xAlpha - yAlpha[x]) * cutoff; + if(xAlpha < 1) { + yAlphaWait[x] = true; + outRed = yRed[x]; + outGreen = yGreen[x]; + outBlue = yBlue[x]; + } else { + yAlphaWait[x] = false; + outRed = yRed[x] + (xRed - yRed[x]) * cutoff; + outGreen = yGreen[x] + (xGreen - yGreen[x]) * cutoff; + outBlue = yBlue[x] + (xBlue - yBlue[x]) * cutoff; + } + } + + yAlpha[x] = outAlpha; + yRed[x] = outRed; + yGreen[x] = outGreen; + yBlue[x] = outBlue; + + if(++x == width) { + x = 0; + ++y; + } + + return Pixels.pack((int) outAlpha, (int) outRed, (int) outGreen, (int) outBlue); + } + + public int processPass2Pixel(int pixel) { + return processPass1Pixel(pixel); + } + + public boolean isPass1ReadOnly() { + return false; + } + + public boolean hasPass2() { + return true; + } + + public boolean isPass2Reversed() { + return true; + } + + public void startPass1() { + x = 0; + y = 0; + xAlpha = 0; + xRed = 0; + xGreen = 0; + xBlue = 0; + yAlpha = new float[width]; + yRed = new float[width]; + yGreen = new float[width]; + yBlue = new float[width]; + xAlphaWait = false; + yAlphaWait = new boolean[width]; + } + + public void startPass2() { + startPass1(); + } +} diff --git a/src/com/jotuntech/sketcher/common/filter/Filter.java b/src/com/jotuntech/sketcher/common/filter/Filter.java new file mode 100755 index 0000000..6a0a2ad --- /dev/null +++ b/src/com/jotuntech/sketcher/common/filter/Filter.java @@ -0,0 +1,15 @@ +package com.jotuntech.sketcher.common.filter; + +public interface Filter { + public void setSize(int width, int height); + public void setParameterA(float a); + public void setParameterB(float b); + public void setParameterC(float c); + public boolean isPass1ReadOnly(); + public boolean hasPass2(); + public boolean isPass2Reversed(); + public void startPass1(); + public int processPass1Pixel(int pixel); + public void startPass2(); + public int processPass2Pixel(int pixel); +} diff --git a/src/com/jotuntech/sketcher/server/AccessController.java b/src/com/jotuntech/sketcher/server/AccessController.java new file mode 100755 index 0000000..3b0486e --- /dev/null +++ b/src/com/jotuntech/sketcher/server/AccessController.java @@ -0,0 +1,11 @@ +package com.jotuntech.sketcher.server; + +import com.jotuntech.sketcher.common.User; + +public interface AccessController { + boolean isLoginAllowed(Server server, String login, String password); + boolean isKickAllowed(Server server, User source, User target); + boolean isMessageAllowed(Server server, User source, String message); + boolean isLayerActionAllowed(Server server, User source); + boolean isViewer(Server server, User source); +} diff --git a/src/com/jotuntech/sketcher/server/Command.java b/src/com/jotuntech/sketcher/server/Command.java new file mode 100755 index 0000000..98b1fa9 --- /dev/null +++ b/src/com/jotuntech/sketcher/server/Command.java @@ -0,0 +1,7 @@ +package com.jotuntech.sketcher.server; + +import com.jotuntech.sketcher.common.Streamable; + +public interface Command extends Streamable { + public int perform(Server server, Connection connection); +} diff --git a/src/com/jotuntech/sketcher/server/CommandEntry.java b/src/com/jotuntech/sketcher/server/CommandEntry.java new file mode 100755 index 0000000..817a6e5 --- /dev/null +++ b/src/com/jotuntech/sketcher/server/CommandEntry.java @@ -0,0 +1,24 @@ +package com.jotuntech.sketcher.server; + +public class CommandEntry { + private Integer sourceKey; + private Command command; + + public CommandEntry(Integer sourceKey, Command command) { + if(sourceKey == null) { + throw new NullPointerException("Source key can't be null."); + } else if(command == null) { + throw new NullPointerException("Command can't be null."); + } + this.sourceKey = sourceKey; + this.command = command; + } + + public Integer getSourceKey() { + return sourceKey; + } + + public Command getCommand() { + return command; + } +} diff --git a/src/com/jotuntech/sketcher/server/Connection.java b/src/com/jotuntech/sketcher/server/Connection.java new file mode 100755 index 0000000..76319ce --- /dev/null +++ b/src/com/jotuntech/sketcher/server/Connection.java @@ -0,0 +1,128 @@ +/** + * + */ +package com.jotuntech.sketcher.server; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.util.concurrent.ArrayBlockingQueue; + +import com.jotuntech.sketcher.common.User; + +/** + * Client connection + * + * @author Thor Harald Johansen + */ +public class Connection { + /** Return values for Command */ + public final static int SEND_NONE = 0; /** Send to nobody */ + public final static int SEND_SELF = 1; /** Send to self only */ + public final static int SEND_OTHERS = 2; /** Send to others only */ + public final static int SEND_ALL = 3; /** Send to self and others */ + + public final static long MAX_PING = 300000; + + /** Socket channel */ + private SocketChannel channel; + + /** User to which connection belongs. */ + private User user; + + /** Input buffer */ + private ByteBuffer inputBuffer; + + /** Output buffer */ + private ByteBuffer outputBuffer; + + /** Send queue */ + private ArrayBlockingQueue sendQueue = new ArrayBlockingQueue(98304); + + /** Time of death */ + private long timeOfDeath; + + /** Time of last client ping */ + private long lastPing; + + //private Random sendRandom = new Random(0x59A70A401C8CD1EAL); + //private Random recvRandom = new Random(0x707733360A596E0AL); + + public Connection() { + + } + + public Connection(SocketChannel channel) throws IOException { + /** Store socket channel and parent object. */ + this.channel = channel; + + /** Allocate transmission buffers */ + inputBuffer = ByteBuffer.allocate(65538); + outputBuffer = ByteBuffer.allocate(65538); + + /** Disable time of death */ + timeOfDeath = Long.MAX_VALUE; + + /** Set time of last ping */ + lastPing = System.currentTimeMillis(); + } + + /** Set user for connection */ + public void setUser(User user) { + this.user = user; + } + + /** Get user for connection */ + public User getUser() { + return user; + } + + public void setChannel(SocketChannel channel) { + this.channel = channel; + } + + public SocketChannel getChannel() { + return channel; + } + + public ByteBuffer getInputBuffer() { + return inputBuffer; + } + + public ByteBuffer getOutputBuffer() { + return outputBuffer; + } + + public ArrayBlockingQueue getSendQueue() { + return sendQueue; + } + + public void setTimeOfDeath(long timeOfDeath) { + this.timeOfDeath = timeOfDeath; + this.sendQueue.clear(); + } + + public boolean hasTimeOfDeath() { + return timeOfDeath != Long.MAX_VALUE; + } + + public boolean isTimeOfDeath() { + return timeOfDeath != Long.MAX_VALUE && System.currentTimeMillis() >= timeOfDeath; + } + + public void setLastPing(long lastPing) { + this.lastPing = lastPing; + } + + public long getLastPing() { + return lastPing; + } + + //public Random getSendRandom() { + // return sendRandom; + //} + + //public Random getRecvRandom() { + // return recvRandom; + //} +} \ No newline at end of file diff --git a/src/com/jotuntech/sketcher/server/Server.java b/src/com/jotuntech/sketcher/server/Server.java new file mode 100755 index 0000000..0a423d5 --- /dev/null +++ b/src/com/jotuntech/sketcher/server/Server.java @@ -0,0 +1,713 @@ +package com.jotuntech.sketcher.server; + +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.awt.image.ImageObserver; +import java.io.DataOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedByInterruptException; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.zip.DeflaterOutputStream; + +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import com.jotuntech.sketcher.common.BitmapLayer; +import com.jotuntech.sketcher.common.Canvas; +import com.jotuntech.sketcher.common.Layer; +import com.jotuntech.sketcher.common.Log; +import com.jotuntech.sketcher.common.StreamableUtils; +import com.jotuntech.sketcher.common.TwoWayHashMap; +import com.jotuntech.sketcher.common.User; +import com.jotuntech.sketcher.server.command.KickCommand; +import com.jotuntech.sketcher.server.command.ServerMessageCommand; +import com.jotuntech.sketcher.server.command.SignOutCommand; + +public class Server extends Thread implements ImageObserver { + /** Map of client connections */ + private TwoWayHashMap connectionMap; + + /** Buffer for encoding commands before transmission */ + private ByteBuffer commandBuffer; + + /** Array of active users - always replace instead of mutating */ + private User[] users; + + /** Map of connections - always replace instead of muting */ + private TwoWayHashMap safeConnectionMap; + + /** Layers and pixel information */ + private Canvas canvas; + + /** Custom attributes for the host */ + private Map attributes = new HashMap(); + + /** TCP port number */ + private int port; + + /** Flattened image of canvas */ + private BufferedImage image; + + /** Pixel array for image */ + private int[] pixels; + + /** Dirty rectangle since last merge */ + private Rectangle mergeDirty; + + /** Data output stream for animation data */ + private DataOutputStream animationDOS; + + /** Optional access controller for callback to host */ + private AccessController accessController; + + /** Socket channel that the server listens on */ + private ServerSocketChannel serverChannel; + + /** Set to inject command into server loop for broadcasting to clients */ + private CommandEntry injectCommand; + + /** Millisecond time of last layer cleanup */ + private long lastLayerClean; + + /** Voice chat server */ + private VoiceServer voiceServer; + + /** MOTD text */ + private String motd = null; + + public Server(String name, int port, int width, int height) throws IOException { + super(name); + + this.port = port; + this.users = new User[0]; + this.safeConnectionMap = new TwoWayHashMap(); + + connectionMap = new TwoWayHashMap(); + connectionMap.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent e) { + /** Find number of connections with signed-in users */ + int numUsers = 0; + for(Connection c : connectionMap.values()) { + if(c.getUser() == null) { + continue; + } + + ++numUsers; + } + + /** Allocate array for new user list */ + User[] newUsers = new User[numUsers]; + + /** Populate array with users */ + int index = 0; + for(Connection c : connectionMap.values()) { + if(c.getUser() == null) { + continue; + } + + newUsers[index++] = c.getUser(); + } + + /** Replace array atomically */ + users = newUsers; + + /** Replicate connection map */ + TwoWayHashMap newMap = connectionMap.copy(); + safeConnectionMap = newMap; + } + }); + + canvas = new Canvas(width, height); + + image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + pixels = ((DataBufferInt)image.getRaster().getDataBuffer()).getData(); + + /** Initialize flattened canvas image by filling it with white */ + Graphics2D g2 = (Graphics2D)image.getGraphics(); + g2.setColor(Color.WHITE); + g2.fillRect(0, 0, canvas.getWidth(), canvas.getHeight()); + g2.dispose(); + + /** Create default layers */ + TwoWayHashMap layerMap = getCanvas().getLayerMap(); + layerMap.put(new BitmapLayer("Sketch")); + layerMap.put(new BitmapLayer("Color")); + layerMap.put(new BitmapLayer("Shadow")); + layerMap.put(new BitmapLayer("Light")); + layerMap.put(new BitmapLayer("Ink")); + layerMap.put(new BitmapLayer("Doodle")); + + /** Receive image updates from canvas */ + canvas.addImageObserver(this); + + /** Allocate buffer to hold command length and data */ + commandBuffer = ByteBuffer.allocate(65538); + + /** Start layer cleanup cycle at a random time for smooth operation of multiple servers */ + lastLayerClean = System.currentTimeMillis() - Math.round(Math.random() * 30000); + + /** Open listening socket */ + serverChannel = ServerSocketChannel.open(); + serverChannel.configureBlocking(false); + serverChannel.socket().bind(new java.net.InetSocketAddress(port)); + Log.info("Sketcher server running on port " + port); + + /** Start voice chat server */ + voiceServer = new VoiceServer(port + 100); + voiceServer.start(); + } + + public Canvas getCanvas() { + return canvas; + } + + public TwoWayHashMap getConnectionMap() { + return connectionMap; + } + + public TwoWayHashMap getSafeConnectionMap() { + return safeConnectionMap; + } + + public User[] getUsers() { + return users; + } + + public void setAttribute(String key, Object value) { + attributes.put(key, value); + } + + public Object getAttribute(String key) { + return attributes.get(key); + } + + public int getPort() { + return port; + } + + public BufferedImage getImage() { + return image; + } + + public void announce(String message) { + Log.info("Announce: " + message); + injectCommand = new CommandEntry(0, new ServerMessageCommand(message)); + } + + public void kick(String username, String reason) { + Log.info("Kick: " + username + " (" + reason + ")"); + /** Kick command was initialized with a name, and not a key, so we must look it up */ + for(Map.Entry e : getSafeConnectionMap().entrySet()) { + User u = e.getValue().getUser(); + if(u == null) { + continue; + } + if(username.equalsIgnoreCase(u.getName())) { + Command c = new KickCommand(e.getKey(), reason); + c.perform(this, null); + injectCommand = new CommandEntry(0, c); + break; + } + } + } + + public boolean imageUpdate(Image img, int infoflags, int left, int top, int width, int height) { + if(infoflags == ImageObserver.ALLBITS) { + /** Entire image needs an update */ + left = 0; + top = 0; + width = getCanvas().getWidth(); + height = getCanvas().getHeight(); + } else { + left = Math.max(0, Math.min(getCanvas().getWidth() - 1, left)); + top = Math.max(0, Math.min(getCanvas().getHeight() - 1, top)); + width = Math.max(0, Math.min(getCanvas().getWidth(), left + width) - left); + height = Math.max(0, Math.min(getCanvas().getHeight(), top + height) - top); + } + + Graphics2D g2 = (Graphics2D)image.getGraphics(); + + /** Set clipping rectangle */ + g2.setClip(left, top, width, height); + + /** Composite source over destination */ + AlphaComposite ac = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1); + g2.setComposite(ac); + + /** Clear with white */ + g2.setColor(Color.WHITE); + g2.fillRect(left, top, width, height); + + /** Create a map of layers, showing which of them have phantom layers */ + IdentityHashMap> selectedLayers = new IdentityHashMap>(); + for(Connection c : connectionMap.values()) { + User u = c.getUser(); + + if(u == null) { + continue; + } + + Layer l = u.getLayer(); + + if(l == null) { + continue; + } + + Set us = selectedLayers.get(l); + + if(us == null) { + us = new HashSet(); + selectedLayers.put(l, us); + } + + us.add(u.getPhantomLayer()); + } + + /** Paint layers over it */ + for(Layer l : canvas.getLayerMap().values()) { + Set ls = selectedLayers.get(l); + if(l instanceof BitmapLayer && ls != null) { + ((BitmapLayer) l).draw(g2, ls); + } else { + l.draw(g2); + } + } + + g2.dispose(); + + if(mergeDirty == null) { + mergeDirty = new Rectangle(left, top, width, height); + } else { + mergeDirty.union(new Rectangle(left, top, width, height)); + } + + if(animationDOS != null) { + synchronized(animationDOS) { + try { + animationDOS.writeLong(System.currentTimeMillis()); + animationDOS.writeInt(left); + animationDOS.writeInt(top); + animationDOS.writeInt(width); + animationDOS.writeInt(height); + for(int y = top; y < top + height; y++) { + for(int x = left; x < left + width; x++) { + animationDOS.writeInt(pixels[y * image.getWidth() + x]); + } + } + } catch (IOException e) { + try { animationDOS.close(); } catch (IOException e1) { } + e.printStackTrace(); + } + } + } + + return true; + } + + public void setAccessController(AccessController accessController) { + this.accessController = accessController; + } + + public AccessController getAccessController() { + return accessController; + } + + + public void setAnimationFile(String name) throws IOException { + if(animationDOS != null) { + synchronized(animationDOS) { + animationDOS.close(); + } + } + + animationDOS = new DataOutputStream(new DeflaterOutputStream(new FileOutputStream(name, true))); + } + + public void run() { + try { + while(!interrupted()) { + /** Conserve CPU time when idle */ + sleep(20); + + /** Run layer clean-up every 30 seconds */ + if(lastLayerClean + 30000 < System.currentTimeMillis()) { + /** Cleanup now happens internally and separately on client and server */ + for(Layer l : canvas.getLayerMap().values()) { + /** Clean up layer */ + l.clean(); + } + + /** Time of last layer cleanup is now */ + lastLayerClean = System.currentTimeMillis(); + + //Log.info("Cleaned layers."); + } + + /** Accept and initialize client connections */ + for(SocketChannel channel = serverChannel.accept(); channel != null; channel = serverChannel.accept()) { + channel.configureBlocking(false); + Connection connection = new Connection(channel); + Integer connectionKey = connectionMap.put(connection); + Log.debug("Accepting connection " + connectionKey + " from " + channel.socket().getRemoteSocketAddress()); + } + + /** Broadcast any injected command */ + if(injectCommand != null) { + for(Connection c : connectionMap.values()) { + c.getSendQueue().offer(injectCommand); + } + injectCommand = null; + } + + /** Loop through all client connections */ + for(Map.Entry entry : connectionMap.entrySet()) { + /** Fetch basic information about each connection */ + Integer connectionKey = entry.getKey(); + Connection connection = entry.getValue(); + SocketChannel channel = connection.getChannel(); + + /** All connection specific errors are trapped by this 'try' statement */ + try { + if(!channel.isConnected() && !connection.hasTimeOfDeath()) { + /** Remove closed connection from map */ + connectionMap.remove(connectionKey); + + /** Remove voice chat client if any */ + voiceServer.removeClient(connectionKey); + + if(connection.getUser() != null) { + /** Connection has closed unceremoniously. Broadcast to other clients. */ + CommandEntry commandEntry = new CommandEntry(connectionKey, new SignOutCommand("Connection was closed")); + for(Connection c : connectionMap.values()) { + c.getSendQueue().offer(commandEntry); + } + Log.debug("Connection " + connectionKey + " had no time of death, sent explicit sign-out."); + } + + + Log.debug("Connection " + connectionKey + " was closed."); + + /** The loop must be broken here to avoid a ConcurrentModificationException */ + break; + } else if(connection.isTimeOfDeath()) { + /** Client signed out earlier. Close the channel. */ + try { channel.close(); } catch(IOException e) { } + + /** Remove from connection map */ + connectionMap.remove(connectionKey); + + /** Remove voice chat client if any */ + voiceServer.removeClient(connectionKey); + + Log.debug("Connection " + connectionKey + " has reached time of death and was closed."); + + /** The loop must be broken here to avoid a ConcurrentModificationException */ + break; + } else if(!connection.hasTimeOfDeath() && System.currentTimeMillis() - connection.getLastPing() > Connection.MAX_PING) { + connection.setTimeOfDeath(System.currentTimeMillis() + 5000); + + CommandEntry commandEntry = new CommandEntry(connectionKey, new SignOutCommand("Ping timeout")); + for(Connection c : connectionMap.values()) { + c.getSendQueue().offer(commandEntry); + } + + if(connection.getUser() != null) { + Log.info(connection.getUser().getName() + " has signed out (Ping timeout)."); + } + } + + /** Get buffers for networking */ + ByteBuffer inputBuffer = connection.getInputBuffer(); + ByteBuffer outputBuffer = connection.getOutputBuffer(); + + ArrayBlockingQueue sendQueue = connection.getSendQueue(); + + //Random sendRandom = connection.getSendRandom(); + //Random recvRandom = connection.getRecvRandom(); + + //int startPosition = outputBuffer.position(); + + /** Only check send queue if there is room for a minimal command in the output buffer. */ + while(outputBuffer.remaining() >= 2 + 4 + 1 + 2 && sendQueue.size() > 0) { + /** Peek at the head of the queue */ + CommandEntry e = sendQueue.peek(); + + /** Get command entry fields */ + Integer k = e.getSourceKey(); + Command c = e.getCommand(); + + /** Clear command buffer */ + commandBuffer.clear(); + + /** Write peer key */ + commandBuffer.putInt(k); + + /** Write command name */ + String commandName = c.getClass().getSimpleName(); + commandBuffer.put((byte) (commandName.length() - 1)); + for(int i = 0; i < commandName.length(); i++) { + commandBuffer.putChar(commandName.charAt(i)); + } + + /** Encode and write command data */ + c.encode(commandBuffer); + + /** Flip command buffer before reading */ + commandBuffer.flip(); + + /** Discard command if there is no room for it in the output buffer */ + if(commandBuffer.remaining() + 2 > outputBuffer.remaining()) { + break; + } + + /** Append command length and command data to output buffer */ + outputBuffer.putShort((short) (commandBuffer.remaining() - 1)); + outputBuffer.put(commandBuffer); + + /** Remove command from queue */ + sendQueue.remove(); + } + + //for(int i = startPosition; i < outputBuffer.position(); i++) { + // outputBuffer.put(i, (byte) (outputBuffer.get(i) ^ sendRandom.nextInt(256))); + //} + + /** Flip output buffer */ + outputBuffer.flip(); + + if(outputBuffer.remaining() > 0) { + try { + /** Flush buffer */ + channel.write(outputBuffer); + } catch (IOException e) { + Log.info("Connection" + connectionKey + " was broken (" + e.getMessage() + "). Sending sign-out."); + try { channel.close(); } catch(IOException e2) { } + connectionMap.remove(connectionKey); + CommandEntry commandEntry = new CommandEntry(connectionKey, new SignOutCommand("Connection was broken: " + e.getMessage())); + for(Connection c : connectionMap.values()) { + c.getSendQueue().offer(commandEntry); + } + + /** Remove voice chat client if any */ + voiceServer.removeClient(connectionKey); + + break; + } + } + + /** Prepare output buffer for append */ + outputBuffer.compact(); + + /** Mortal connections are mute and have no voice chat audio */ + if(connection.hasTimeOfDeath()) { + continue; + } + + /** Append socket data to input buffer */ + //startPosition = inputBuffer.position(); + int readResult = channel.read(inputBuffer); + if(readResult == -1) { + /** Close the channel */ + try { channel.close(); } catch(IOException e) { } + + /** Remove from connection map */ + connectionMap.remove(connectionKey); + + /** Broadcast sign-out to other clients */ + CommandEntry commandEntry = new CommandEntry(connectionKey, new SignOutCommand("Connection was closed")); + for(Connection c : connectionMap.values()) { + c.getSendQueue().offer(commandEntry); + } + + /** Remove voice chat client if any */ + voiceServer.removeClient(connectionKey); + + Log.info("Connection " + connectionKey + " reached end of stream. Sending sign-out."); + + /** The loop must be broken here to avoid a ConcurrentModificationException */ + break; + } else if(readResult > 0) { + //for(int i = startPosition; i < inputBuffer.position(); i++) { + // inputBuffer.put(i, (byte) (inputBuffer.get(i) ^ recvRandom.nextInt(256))); + //} + + /** Prepare input buffer for reading */ + inputBuffer.flip(); + + /** Only check input buffer if long enough to hold a minimal command. */ + while(inputBuffer.remaining() >= 2 + 4 + 1 + 2) { + /** Peek at length */ + int commandLength = (inputBuffer.getShort(inputBuffer.position()) & 0xFFFF) + 1; + + /** Only read input buffer if long enough to read entire command. */ + if(inputBuffer.remaining() < commandLength + 2) { + break; + } + + /** We already have the command length */ + inputBuffer.getShort(); + + /** Map data into command buffer */ + ByteBuffer commandBuffer = inputBuffer.slice(); + commandBuffer.limit(commandLength); + + /** Skip command data in input buffer */ + inputBuffer.position(inputBuffer.position() + commandLength); + + /** Ignore peer key from client */ + commandBuffer.getInt(); + + /** Read command name */ + int commandNameLength = (commandBuffer.get() & 0xFF) + 1; + StringBuffer commandNameBuffer = new StringBuffer(); + for(int i = 0; i < commandNameLength; i++) { + commandNameBuffer.append(commandBuffer.getChar()); + } + String commandName = commandNameBuffer.toString(); + + /** Attempt to instanciate command */ + Command command = (Command) StreamableUtils.create("com.jotuntech.sketcher.server.command." + commandName, commandBuffer); + + /** Wrap it in a CommandEntry in case of a broadcast */ + CommandEntry commandEntry = new CommandEntry(connectionKey, command); + + /** Perform the command and check the result */ + switch(command.perform(this, connection)) { + /** Command wishes to be broadcast to all connections */ + case Connection.SEND_ALL: + for(Connection c : connectionMap.values()) { + c.getSendQueue().offer(commandEntry); + } + break; + + /** Command wishes to be broadcast to all but its own connection */ + case Connection.SEND_OTHERS: + for(Connection c : connectionMap.values()) { + if(c != connection) { + c.getSendQueue().offer(commandEntry); + } + } + break; + + /** Command wishes to loop back to the client */ + case Connection.SEND_SELF: + connection.getSendQueue().offer(commandEntry); + break; + + /** Command wishes to be anonymous */ + case Connection.SEND_NONE: + break; + } + } + + /** Prepare input buffer for appending */ + inputBuffer.compact(); + } + } catch (IOException e) { + /** Remove from connection map */ + connectionMap.remove(connectionKey); + + /** Close the channel */ + try { channel.close(); } catch(IOException e2) { } + + if(connection.getUser() != null) { + /** Broadcast sign-out to clients */ + CommandEntry commandEntry = new CommandEntry(connectionKey, new SignOutCommand("Connection was broken: " + e.getMessage())); + for(Connection c : connectionMap.values()) { + c.getSendQueue().offer(commandEntry); + } + } + + /** Remove voice chat client if any */ + voiceServer.removeClient(connectionKey); + + Log.info("Connection " + connectionKey + " was broken (" + e.getMessage() + "). Sending sign-out."); + + /** The loop must be broken here to avoid a ConcurrentModificationException */ + break; + } catch(Throwable t) { + /** We have crashed and must let the client know, so we let connection linger a bit */ + connection.setTimeOfDeath(System.currentTimeMillis() + 5000); + + if(connection.getUser() != null) { + /** Broadcast sign-out to clients */ + CommandEntry commandEntry = new CommandEntry(connectionKey, new SignOutCommand("Connection was crashed: " + t.getClass().getSimpleName())); + for(Connection c : connectionMap.values()) { + c.getSendQueue().offer(commandEntry); + } + } + + Log.error("Connection " + connectionKey + " crashed!"); + Log.error(t); + + /** Remove voice chat client if any */ + voiceServer.removeClient(connectionKey); + + /** The loop must be broken here to avoid a ConcurrentModificationException */ + break; + } + } + + } + } catch(ClosedByInterruptException e) { + Log.info("Server interrupted."); + } catch(InterruptedException e) { + Log.info("Server interrupted."); + } catch(Throwable e) { + Log.error("Server crashed."); + Log.error(e); + } + + /** Shut down the voice server */ + voiceServer.interrupt(); + + /** Close all client connections */ + for(Connection c : connectionMap.values()) { + try { c.getChannel().close(); } catch(IOException e) { } + } + + /** Close server socket channel */ + try { serverChannel.close(); } catch(IOException e) { } + + /** Empty the user list */ + users = new User[0]; + + /** Clear the connection map */ + connectionMap.clear(); + + } + + public VoiceServer getVoiceServer() { + return voiceServer; + } + + public String getMOTD() { + return motd; + } + + public void setMOTD(String motd) { + this.motd = motd; + } + + public void setMergeDirty(Rectangle mergeDirty) { + this.mergeDirty = mergeDirty; + } + + public Rectangle getMergeDirty() { + return mergeDirty; + } +} \ No newline at end of file diff --git a/src/com/jotuntech/sketcher/server/Service.java b/src/com/jotuntech/sketcher/server/Service.java new file mode 100755 index 0000000..d2baba2 --- /dev/null +++ b/src/com/jotuntech/sketcher/server/Service.java @@ -0,0 +1,244 @@ +package com.jotuntech.sketcher.server; + +import java.awt.AlphaComposite; +import java.awt.Image; +import java.awt.image.ImageObserver; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.PrintWriter; +import java.util.Properties; +import java.util.Map.Entry; + +import javax.imageio.ImageIO; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import com.jotuntech.sketcher.common.Canvas; +import com.jotuntech.sketcher.common.Layer; +import com.jotuntech.sketcher.common.TwoWayHashMap; +import com.jotuntech.sketcher.common.User; + +public class Service { + boolean canvasModified = false; + long motdLastModified = 0; + + public static void main(String[] args) throws Throwable { + new Service().run(args); + } + + private void run(String[] args) throws Throwable { + System.setProperty("java.net.preferIPv4Stack", "true"); + + BufferedReader pbr = new BufferedReader(new FileReader(args[0])); + String propertiesPath = pbr.readLine(); + pbr.close(); + + Properties properties = new Properties(); + properties.load(new FileInputStream(new File(propertiesPath))); + + String serverName = properties.getProperty("server.name", "Rental"); + int port = Integer.valueOf(properties.getProperty("server.port", "10000")); + final File talkFile = new File(properties.getProperty("server.talk", "talk.txt")); + final File userFile = new File(properties.getProperty("server.user", "user.txt")); + String motdFilename = properties.getProperty("server.motd", null); + final File motdFile = motdFilename == null ? null : new File(motdFilename); + String captureFilename = properties.getProperty("server.capture", "capture.jpg"); + final File captureFile = new File(captureFilename); + final File captureTempFile = new File(captureFilename + ".tmp"); + final File saveFile = new File(properties.getProperty("server.save", "save.canvas")); + final File secretFolder = new File(properties.getProperty("server.secret", "secret/")); + final File flagsFile = new File(properties.getProperty("server.flags", "server.flags")); + + AccessController ac = new AccessController() { + public boolean isKickAllowed(Server server, User source, User target) { + try { + BufferedReader br = new BufferedReader(new FileReader(flagsFile)); + String flags = br.readLine(); + br.close(); + + if(flags.indexOf('k') >= 0) { + return true; + } + } catch (IOException e) { } + + return false; + } + + public boolean isLayerActionAllowed(Server server, User source) { + try { + BufferedReader br = new BufferedReader(new FileReader(flagsFile)); + String flags = br.readLine(); + br.close(); + + if(flags.indexOf('l') >= 0) { + return true; + } + } catch (IOException e) { } + + return false; + } + + public boolean isLoginAllowed(Server server, String login, String password) { + try { + File secretFile = new File(secretFolder, login); + + BufferedReader br = new BufferedReader(new FileReader(secretFile)); + String hash = br.readLine(); + br.close(); + + secretFile.delete(); + secretFile.createNewFile(); + + if(password.equals(hash)) { + return true; + } + } catch (IOException e) { } + + return false; + } + + public boolean isMessageAllowed(Server server, User source, String message) { + return true; + } + + public boolean isViewer(Server server, User source) { + return false; + } + }; + + ImageObserver io = new ImageObserver() { + public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) { + canvasModified = true; + return true; + } + }; + + final Server server = new Server(serverName, port, 2048, 1536); + server.setAccessController(ac); + server.getCanvas().addImageObserver(io); + + if(saveFile.exists()) { + FileInputStream fis = new FileInputStream(saveFile); + ObjectInputStream ois = new ObjectInputStream(fis); + + Canvas c = server.getCanvas(); + + TwoWayHashMap layerMap = c.getLayerMap(); + + synchronized(layerMap) { + layerMap.clear(); + + layerMap.setNextKey(ois.readInt()); + + while(ois.available() > 0) { + int k = ois.readInt(); + Layer l = (Layer) ois.readObject(); + if(l.getAlphaRule() == 0) { + l.setAlphaRule(AlphaComposite.SRC_OVER); + } + layerMap.put(k, l); + } + } + + c.imageUpdate(null, ImageObserver.ALLBITS, 0, 0, c.getWidth(), c.getHeight()); + + ois.close(); + } + + server.getConnectionMap().addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent e) { + try { + PrintWriter pw = new PrintWriter(new FileWriter(userFile)); + for(User u : server.getUsers()) { + pw.println(u.getName()); + } + pw.close(); + } catch(Throwable e2) { + e2.printStackTrace(); + } + } + }); + + server.start(); + + Runtime.getRuntime().addShutdownHook(new Thread() { + public void run() { + try { + FileOutputStream fos = new FileOutputStream(saveFile); + ObjectOutputStream oos = new ObjectOutputStream(fos); + + TwoWayHashMap layerMap = server.getCanvas().getLayerMap(); + oos.writeInt(layerMap.getNextKey()); + for(Entry e : layerMap.entrySet()) { + int k = e.getKey(); + Layer l = e.getValue(); + oos.writeInt(k); + oos.writeObject(l); + } + + oos.close(); + } catch(Throwable e) { + e.printStackTrace(); + } + } + }); + + if(!talkFile.exists()) { + talkFile.createNewFile(); + } + + int captureCounter = 0; + + for(;;) { + if(canvasModified && captureCounter % 60 == 0) { + ImageIO.write(server.getImage(), "PNG", captureTempFile); + captureFile.delete(); + captureTempFile.renameTo(captureFile); + canvasModified = false; + } + + if(talkFile.length() > 0) { + BufferedReader br = new BufferedReader(new FileReader(talkFile)); + String line = br.readLine(); + if(line != null) { + if(line.startsWith("kick ")) { + server.kick(line.substring(5), "by room owner"); + } else { + server.announce(line); + } + } + br.close(); + + talkFile.delete(); + talkFile.createNewFile(); + } + + if(motdFile != null && motdFile.lastModified() > motdLastModified) { + motdLastModified = motdFile.lastModified(); + StringBuffer buffer = new StringBuffer(1024); + char[] charBuffer = new char[1024]; + FileReader fr = new FileReader(motdFile); + int charsRead = fr.read(charBuffer); + if(charsRead > 0) { + do { + buffer.append(charBuffer, 0, charsRead); + charsRead = fr.read(charBuffer); + } while(charsRead > 0); + + server.setMOTD(buffer.toString()); + } + fr.close(); + } + + Thread.sleep(5000); + ++captureCounter; + } + } +} diff --git a/src/com/jotuntech/sketcher/server/VoiceServer.java b/src/com/jotuntech/sketcher/server/VoiceServer.java new file mode 100755 index 0000000..e203aaa --- /dev/null +++ b/src/com/jotuntech/sketcher/server/VoiceServer.java @@ -0,0 +1,98 @@ +package com.jotuntech.sketcher.server; + +import java.io.IOException; +import java.net.DatagramSocket; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.DatagramChannel; +import java.util.HashMap; + +public class VoiceServer extends Thread { + public final static int SPEEX_FRAME_BYTES = 640; + private HashMap clients = new HashMap(); + ByteBuffer packetBuffer; + int listenPort; + + public VoiceServer(int listenPort) { + super("VoiceServer"); + this.listenPort = listenPort; + } + + public void run() { + System.err.println("Voice server started."); + packetBuffer = ByteBuffer.allocate(SPEEX_FRAME_BYTES + 8); + + DatagramChannel channel = null; + try { + channel = DatagramChannel.open(); + channel.configureBlocking(true); + DatagramSocket socket = channel.socket(); + socket.bind(new InetSocketAddress(listenPort)); + channel.configureBlocking(false); + + while(!interrupted()) { + sleep(10); + + for(SocketAddress source = channel.receive(packetBuffer); source != null; source = channel.receive(packetBuffer)) { + packetBuffer.flip(); + Integer sourceKey = packetBuffer.getInt(0); + /** Allow only familiar clients */ + if(clients.containsKey(sourceKey)) { + if(clients.get(sourceKey) == null || !clients.get(sourceKey).equals(source)) { + /** Client is familiar but needs to be assigned an address */ + clients.put(sourceKey, (InetSocketAddress) source); + System.err.println("Assigning address " + source + " to client " + sourceKey); + } + synchronized(clients) { + for(InetSocketAddress address : clients.values()) { + if(address == null) { + /** Send only to clients with assigned addresses */ + continue; + } + if(address.equals(source)) { + /** Send only to other clients than source of packet */ + continue; + } + if(channel.send(packetBuffer, address) > 0) { + packetBuffer.flip(); + } else { + System.err.println("Dropped packet."); + } + } + } + } + packetBuffer.clear(); + } + } + } catch(InterruptedException e) { + + } catch (IOException e) { + e.printStackTrace(); + } + + try { + if(channel != null) { + channel.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + + System.err.println("Voice server interrupted."); + } + + public void addClient(Integer peerKey) { + synchronized(clients) { + clients.put(peerKey, null); + } + System.err.println("Added client " + peerKey); + } + + public void removeClient(Integer peerKey) { + synchronized(clients) { + clients.remove(peerKey); + } + System.err.println("Removed client " + peerKey); + } +} diff --git a/src/com/jotuntech/sketcher/server/command/CanvasCommand.java b/src/com/jotuntech/sketcher/server/command/CanvasCommand.java new file mode 100755 index 0000000..edc2572 --- /dev/null +++ b/src/com/jotuntech/sketcher/server/command/CanvasCommand.java @@ -0,0 +1,36 @@ +package com.jotuntech.sketcher.server.command; + +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.server.Command; +import com.jotuntech.sketcher.server.Connection; +import com.jotuntech.sketcher.server.Server; + +public class CanvasCommand implements Command { + protected int width, height; + + public CanvasCommand() { + + } + + public CanvasCommand(int width, int height) { + this.width = width; + this.height = height; + } + + public int perform(Server server, Connection connection) { + /** Do nothing on server. */ + return Connection.SEND_NONE; + } + + public void decode(ByteBuffer in) { + width = in.getInt(); + height = in.getInt(); + } + + public void encode(ByteBuffer out) { + out.putInt(width); + out.putInt(height); + } + +} diff --git a/src/com/jotuntech/sketcher/server/command/CreateLayerCommand.java b/src/com/jotuntech/sketcher/server/command/CreateLayerCommand.java new file mode 100755 index 0000000..1a5a2cc --- /dev/null +++ b/src/com/jotuntech/sketcher/server/command/CreateLayerCommand.java @@ -0,0 +1,74 @@ +package com.jotuntech.sketcher.server.command; + +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.common.BitmapLayer; +import com.jotuntech.sketcher.common.Layer; +import com.jotuntech.sketcher.common.User; +import com.jotuntech.sketcher.server.AccessController; +import com.jotuntech.sketcher.server.Command; +import com.jotuntech.sketcher.server.CommandEntry; +import com.jotuntech.sketcher.server.Connection; +import com.jotuntech.sketcher.server.Server; + +public class CreateLayerCommand implements Command { + int layerKey; + int type; + String name; + + public CreateLayerCommand() { + + } + + public CreateLayerCommand(int layerKey, int type, String name) { + this.layerKey = layerKey; + this.type = type; + this.name = name; + } + + public int perform(Server server, Connection connection) { + User user = connection.getUser(); + + if(user == null) { + /** Connections with users only */ + return Connection.SEND_NONE; + } + + if(user.isViewer()) { + return Connection.SEND_NONE; + } + + /** Check access controller */ + AccessController controller = server.getAccessController(); + if(controller != null) { + if(!controller.isLayerActionAllowed(server, user)) { + connection.getSendQueue().offer(new CommandEntry(0, new ServerMessageCommand("You are not allowed to do that."))); + return Connection.SEND_NONE; + } + } + + Layer l = new BitmapLayer(name); + layerKey = server.getCanvas().getLayerMap().put(l); + + return Connection.SEND_ALL; + } + + public void decode(ByteBuffer in) { + layerKey = in.getInt(); + type = in.get(); + StringBuffer nameBuffer = new StringBuffer(); + while(in.remaining() >= 2) { + nameBuffer.append(in.getChar()); + } + name = nameBuffer.toString(); + } + + public void encode(ByteBuffer out) { + out.putInt(layerKey); + out.put((byte) type); + for(int i = 0; i < name.length(); i++) { + out.putChar(name.charAt(i)); + } + } + +} diff --git a/src/com/jotuntech/sketcher/server/command/CursorCommand.java b/src/com/jotuntech/sketcher/server/command/CursorCommand.java new file mode 100755 index 0000000..0fcb1ec --- /dev/null +++ b/src/com/jotuntech/sketcher/server/command/CursorCommand.java @@ -0,0 +1,58 @@ + +package com.jotuntech.sketcher.server.command; + +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.common.Input; +import com.jotuntech.sketcher.common.User; +import com.jotuntech.sketcher.server.Command; +import com.jotuntech.sketcher.server.Connection; +import com.jotuntech.sketcher.server.Server; + +/** Cursor command. + * @author Thor Harald Johansen + * + */ +public class CursorCommand implements Command { + Input input; + + /** Constructs an Cursor command. */ + public CursorCommand() { + + } + + /** Constructs an Cursor command. + * @param user Source user. + * @param input Cursor position */ + public CursorCommand(Input input) { + this.input = input; + } + + public int perform(Server server, Connection connection) { + User user = connection.getUser(); + + if(user == null) { + /** Signed in users only. */ + return Connection.SEND_NONE; + } + + if(user.isViewer()) { + return Connection.SEND_NONE; + } + + user.setCursor(input); + + /** Return and broadcast. */ + return Connection.SEND_OTHERS; + } + + public void decode(ByteBuffer in) { + input = new Input(); + input.decode(in); + } + + public void encode(ByteBuffer out) { + input.encode(out); + } +} + diff --git a/src/com/jotuntech/sketcher/server/command/DeleteLayerCommand.java b/src/com/jotuntech/sketcher/server/command/DeleteLayerCommand.java new file mode 100755 index 0000000..c7768f7 --- /dev/null +++ b/src/com/jotuntech/sketcher/server/command/DeleteLayerCommand.java @@ -0,0 +1,86 @@ +package com.jotuntech.sketcher.server.command; + +import java.awt.image.ImageObserver; +import java.nio.ByteBuffer; +import java.util.HashSet; +import java.util.Set; + +import com.jotuntech.sketcher.common.Canvas; +import com.jotuntech.sketcher.common.Layer; +import com.jotuntech.sketcher.common.UndoEntry; +import com.jotuntech.sketcher.common.User; +import com.jotuntech.sketcher.server.AccessController; +import com.jotuntech.sketcher.server.Command; +import com.jotuntech.sketcher.server.CommandEntry; +import com.jotuntech.sketcher.server.Connection; +import com.jotuntech.sketcher.server.Server; + +public class DeleteLayerCommand implements Command { + int layerKey; + + public DeleteLayerCommand() { + + } + + public DeleteLayerCommand(int layerKey) { + this.layerKey = layerKey; + } + + public int perform(Server server, Connection connection) { + User user = connection.getUser(); + + if(user == null) { + return Connection.SEND_NONE; + } + + if(user.isViewer()) { + return Connection.SEND_NONE; + } + + /** Check access controller */ + AccessController controller = server.getAccessController(); + if(controller != null) { + if(!controller.isLayerActionAllowed(server, user)) { + connection.getSendQueue().offer(new CommandEntry(0, new ServerMessageCommand("You are not allowed to do that."))); + return Connection.SEND_NONE; + } + } + + Canvas canvas = server.getCanvas(); + Layer layer = canvas.getLayerMap().remove(layerKey); + + if(layer == null) { + return Connection.SEND_NONE; + } + + if(canvas.getImageObserver() != null) { + canvas.getImageObserver().imageUpdate(null, ImageObserver.ALLBITS, 0, 0, canvas.getWidth(), canvas.getHeight()); + } + + // Remove references to layer from undo deque + for(Connection c: server.getConnectionMap().values()) { + Set uers = new HashSet(); + if(c.getUser() == null) { + continue; + } + for(UndoEntry ue : c.getUser().getUndoDeque()) { + if(ue.getLayer() == layer) { + uers.add(ue); + } + } + for(UndoEntry ue : uers) { + c.getUser().getUndoDeque().remove(ue); + } + } + + return Connection.SEND_ALL; + } + + public void decode(ByteBuffer in) { + layerKey = in.getInt(); + } + + public void encode(ByteBuffer out) { + out.putInt(layerKey); + } +} diff --git a/src/com/jotuntech/sketcher/server/command/FilterCommand.java b/src/com/jotuntech/sketcher/server/command/FilterCommand.java new file mode 100755 index 0000000..d473c96 --- /dev/null +++ b/src/com/jotuntech/sketcher/server/command/FilterCommand.java @@ -0,0 +1,83 @@ +package com.jotuntech.sketcher.server.command; + +import java.awt.AlphaComposite; +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.common.BitmapLayer; +import com.jotuntech.sketcher.common.User; +import com.jotuntech.sketcher.common.filter.Filter; +import com.jotuntech.sketcher.server.Command; +import com.jotuntech.sketcher.server.Connection; +import com.jotuntech.sketcher.server.Server; + +public class FilterCommand implements Command { + private Filter filter; + private int x, y, w, h; + float a, b, c; + + public FilterCommand() { + + } + + public FilterCommand(Filter filter, float x, float y, float w, float h, float a, float b, float c) { + this.filter = filter; + this.x = (int) x; + this.y = (int) y; + this.w = (int) w; + this.h = (int) h; + this.a = a; + this.b = b; + this.c = c; + } + + public int perform(Server server, Connection connection) { + if(connection.getUser() == null || connection.getUser().getLayer() == null) { + return Connection.SEND_NONE; + } + + User user = connection.getUser(); + + BitmapLayer pl = (BitmapLayer) user.getPhantomLayer(); + filter.setSize(w, h); + filter.setParameterA(a); + user.getLayer().copyTo(pl, null, false, x, y, x, y, w, h); + pl.setAlphaRule(AlphaComposite.SRC); + pl.applyFilter(filter, user.getLayer().getImageObserver(), x, y, w, h); + return Connection.SEND_OTHERS; + } + + public void decode(ByteBuffer in) { + x = (int) in.getFloat(); + y = (int) in.getFloat(); + w = (int) in.getFloat(); + h = (int) in.getFloat(); + a = in.getFloat(); + b = in.getFloat(); + c = in.getFloat(); + + StringBuffer filterNameBuffer = new StringBuffer(); + while(in.remaining() >= 2) { + filterNameBuffer.append(in.getChar()); + } + String filterName = "com.jotuntech.sketcher.common.filter." + filterNameBuffer.toString(); + try { + filter = (Filter) Class.forName(filterName).newInstance(); + } catch (Throwable t) { + t.printStackTrace(); + } + } + + public void encode(ByteBuffer out) { + out.putFloat(x); + out.putFloat(y); + out.putFloat(w); + out.putFloat(h); + out.putFloat(a); + out.putFloat(b); + out.putFloat(c); + String filterName = filter.getClass().getSimpleName(); + for(int i = 0; i < filterName.length(); i++) { + out.putChar(filterName.charAt(i)); + } + } +} diff --git a/src/com/jotuntech/sketcher/server/command/KickCommand.java b/src/com/jotuntech/sketcher/server/command/KickCommand.java new file mode 100755 index 0000000..a26e067 --- /dev/null +++ b/src/com/jotuntech/sketcher/server/command/KickCommand.java @@ -0,0 +1,73 @@ +package com.jotuntech.sketcher.server.command; + +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.server.AccessController; +import com.jotuntech.sketcher.server.Command; +import com.jotuntech.sketcher.server.CommandEntry; +import com.jotuntech.sketcher.server.Connection; +import com.jotuntech.sketcher.server.Server; + +public class KickCommand implements Command { + int targetKey; + String reason; + + public KickCommand() { + + } + + public KickCommand(int targetKey, String reason) { + this.targetKey = targetKey; + this.reason = reason; + } + + public int perform(Server server, Connection connection) { + if(connection != null && connection.getUser() == null) { + /** Signed in users only. */ + return Connection.SEND_NONE; + } + + /** Locate referenced connection */ + Connection targetConnection = server.getConnectionMap().get(targetKey); + if(targetConnection == null || targetConnection.getUser() == null) { + connection.getSendQueue().offer(new CommandEntry(0, new ServerMessageCommand("The user you are trying to kick doesn't exist."))); + return Connection.SEND_NONE; + } + + if(connection != null) { + /** Check access controller */ + AccessController controller = server.getAccessController(); + if(controller != null) { + if(!controller.isKickAllowed(server, connection.getUser(), targetConnection.getUser())) { + connection.getSendQueue().offer(new CommandEntry(0, new ServerMessageCommand("You are not allowed to do that."))); + return Connection.SEND_NONE; + } + } + } + + /** Remove from connection map */ + server.getConnectionMap().remove(targetConnection); + + /** Set time of death */ + targetConnection.setTimeOfDeath(System.currentTimeMillis() + 5000); + + /** Return and broadcast */ + return Connection.SEND_ALL; + } + + public void decode(ByteBuffer in) { + targetKey = in.getInt(); + StringBuffer reasonBuffer = new StringBuffer(); + while(in.remaining() >= 2) { + reasonBuffer.append(in.getChar()); + } + reason = reasonBuffer.toString(); + } + + public void encode(ByteBuffer out) { + out.putInt(targetKey); + for(int i = 0; i < reason.length(); i++) { + out.putChar(reason.charAt(i)); + } + } +} \ No newline at end of file diff --git a/src/com/jotuntech/sketcher/server/command/LayerDataCommand.java b/src/com/jotuntech/sketcher/server/command/LayerDataCommand.java new file mode 100755 index 0000000..7c6fbd2 --- /dev/null +++ b/src/com/jotuntech/sketcher/server/command/LayerDataCommand.java @@ -0,0 +1,40 @@ +package com.jotuntech.sketcher.server.command; + +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.server.Command; +import com.jotuntech.sketcher.server.Connection; +import com.jotuntech.sketcher.server.Server; + +public class LayerDataCommand implements Command { + int layerKey; + boolean phantom; + byte[] data; + + public LayerDataCommand() { + } + + public LayerDataCommand(int layerKey, boolean phantom, byte[] data) { + this.layerKey = layerKey; + this.phantom = phantom; + this.data = data; + } + + public int perform(Server server, Connection connection) { + /** Not implemented on server */ + return Connection.SEND_NONE; + } + + public void decode(ByteBuffer in) { + layerKey = in.getInt(); + phantom = in.get() != 0; + data = new byte[in.remaining()]; + in.get(data); + } + + public void encode(ByteBuffer out) { + out.putInt(layerKey); + out.put((byte) (phantom ? 0xFF : 0x00)); + out.put(data); + } +} diff --git a/src/com/jotuntech/sketcher/server/command/LineCommand.java b/src/com/jotuntech/sketcher/server/command/LineCommand.java new file mode 100755 index 0000000..66ba33d --- /dev/null +++ b/src/com/jotuntech/sketcher/server/command/LineCommand.java @@ -0,0 +1,76 @@ +/** + * + */ +package com.jotuntech.sketcher.server.command; + +import java.awt.AlphaComposite; +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.common.Input; +import com.jotuntech.sketcher.common.Layer; +import com.jotuntech.sketcher.common.User; +import com.jotuntech.sketcher.server.Command; +import com.jotuntech.sketcher.server.Connection; +import com.jotuntech.sketcher.server.Server; + + +public class LineCommand implements Command { + protected Input input; + + /** Constructs a Daub command */ + public LineCommand() { + + } + + /** Constructs a Daub command. + * @param user Source user. + * @param input Position of new daub. */ + public LineCommand(Input input) { + this.input = input; + } + + + public int perform(Server server, Connection connection) { + User user = connection.getUser(); + + if(user.isViewer()) { + return Connection.SEND_NONE; + } + + if(user != null && user.getLayer() != null && user.getBrush() != null) { + Layer l = user.getPhantomLayer(); + l.setOpacity(Math.abs(user.getBrush().getOpacity()) / 255f); + if(user.getBrush().isLockTransparency()) { + l.setAlphaRule(AlphaComposite.SRC_ATOP); + } else if(user.getBrush().getOpacity() >= 0) { + l.setAlphaRule(AlphaComposite.SRC_OVER); + } else { + l.setAlphaRule(AlphaComposite.DST_OUT); + } + + /** Draw line */ + Input end = user.getPhantomLayer().line(user.getCursor(), input, user.getColor(), user.getBrush(), user.getLayer()); + + if(end != null) { + /** Move cursor on success */ + user.setCursor(end); + + /** Return and broadcast */ + return Connection.SEND_OTHERS; + } + } + + /** Return silently */ + return Connection.SEND_NONE; + } + + public void decode(ByteBuffer in) { + input = new Input(); + input.decode(in); + } + + public void encode(ByteBuffer out) { + input.encode(out); + } +} + diff --git a/src/com/jotuntech/sketcher/server/command/MergeCommand.java b/src/com/jotuntech/sketcher/server/command/MergeCommand.java new file mode 100755 index 0000000..06f0ad8 --- /dev/null +++ b/src/com/jotuntech/sketcher/server/command/MergeCommand.java @@ -0,0 +1,50 @@ +package com.jotuntech.sketcher.server.command; + +import java.nio.ByteBuffer; +import java.util.Deque; + +import com.jotuntech.sketcher.common.UndoEntry; +import com.jotuntech.sketcher.common.User; +import com.jotuntech.sketcher.server.Command; +import com.jotuntech.sketcher.server.Connection; +import com.jotuntech.sketcher.server.Server; + +public class MergeCommand implements Command { + + public int perform(Server server, Connection connection) { + User user = connection.getUser(); + + if(user == null || user.getLayer() == null) { + return Connection.SEND_NONE; + } + + if(user.isViewer()) { + return Connection.SEND_NONE; + } + + if(user.getPhantomLayer().isEmpty()) { + return Connection.SEND_NONE; + } + + UndoEntry undoEntry = new UndoEntry(); + undoEntry.setLayer(user.getLayer()); + undoEntry.setUndoData(user.getLayer().merge(user.getPhantomLayer())); + Deque undoDeque = user.getUndoDeque(); + undoDeque.addFirst(undoEntry); + while(undoDeque.size() > 10) { + undoDeque.removeLast(); + } + + server.setMergeDirty(null); + + return Connection.SEND_OTHERS; + } + + public void decode(ByteBuffer in) { + + } + + public void encode(ByteBuffer out) { + + } +} diff --git a/src/com/jotuntech/sketcher/server/command/MoveCommand.java b/src/com/jotuntech/sketcher/server/command/MoveCommand.java new file mode 100755 index 0000000..e7b65b5 --- /dev/null +++ b/src/com/jotuntech/sketcher/server/command/MoveCommand.java @@ -0,0 +1,69 @@ +package com.jotuntech.sketcher.server.command; + +import java.awt.AlphaComposite; +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.common.BitmapLayer; +import com.jotuntech.sketcher.common.Layer; +import com.jotuntech.sketcher.common.User; +import com.jotuntech.sketcher.server.Command; +import com.jotuntech.sketcher.server.Connection; +import com.jotuntech.sketcher.server.Server; + +public class MoveCommand implements Command { + private float sx, sy, dx, dy, w, h; + + public MoveCommand() { + + } + + public MoveCommand(float sx, float sy, float dx, float dy, float w, float h) { + this.sx = sx; + this.sy = sy; + this.dx = dx; + this.dy = dy; + this.w = w; + this.h = h; + } + + public int perform(Server server, Connection connection) { + User user = connection.getUser(); + if(user == null || user.getLayer() == null) { + return Connection.SEND_NONE; + } + + if(user.isViewer()) { + return Connection.SEND_NONE; + } + + if(user.getPhantomLayer().isEmpty()) { + Layer pl = (BitmapLayer) user.getPhantomLayer(); + pl.setAlphaRule(AlphaComposite.SRC_OVER); + pl.setOpacity(1); + user.getLayer().copyTo(pl, null, false, sx, sy, dx, dy, w, h); + } else { + user.getPhantomLayer().copyTo(user.getPhantomLayer(), user.getLayer().getImageObserver(), true, sx, sy, dx, dy, w, h); + user.getPhantomLayer().clean(); + } + + return Connection.SEND_OTHERS; + } + + public void decode(ByteBuffer in) { + sx = in.getFloat(); + sy = in.getFloat(); + dx = in.getFloat(); + dy = in.getFloat(); + w = in.getFloat(); + h = in.getFloat(); + } + + public void encode(ByteBuffer out) { + out.putFloat(sx); + out.putFloat(sy); + out.putFloat(dx); + out.putFloat(dy); + out.putFloat(w); + out.putFloat(h); + } +} diff --git a/src/com/jotuntech/sketcher/server/command/PingCommand.java b/src/com/jotuntech/sketcher/server/command/PingCommand.java new file mode 100755 index 0000000..f51440e --- /dev/null +++ b/src/com/jotuntech/sketcher/server/command/PingCommand.java @@ -0,0 +1,32 @@ +package com.jotuntech.sketcher.server.command; + +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.server.Command; +import com.jotuntech.sketcher.server.Connection; +import com.jotuntech.sketcher.server.Server; + +public class PingCommand implements Command { + private long timestamp; + + public PingCommand() { + this.timestamp = System.currentTimeMillis(); + } + + public PingCommand(long timestamp) { + this.timestamp = timestamp; + } + + public int perform(Server server, Connection connection) { + connection.setLastPing(System.currentTimeMillis()); + return Connection.SEND_NONE; + } + + public void decode(ByteBuffer in) { + timestamp = in.getLong(); + } + + public void encode(ByteBuffer out) { + out.putLong(timestamp); + } +} diff --git a/src/com/jotuntech/sketcher/server/command/SayCommand.java b/src/com/jotuntech/sketcher/server/command/SayCommand.java new file mode 100755 index 0000000..7ed2d79 --- /dev/null +++ b/src/com/jotuntech/sketcher/server/command/SayCommand.java @@ -0,0 +1,69 @@ +package com.jotuntech.sketcher.server.command; + +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.common.Log; +import com.jotuntech.sketcher.common.User; +import com.jotuntech.sketcher.server.AccessController; +import com.jotuntech.sketcher.server.Command; +import com.jotuntech.sketcher.server.CommandEntry; +import com.jotuntech.sketcher.server.Connection; +import com.jotuntech.sketcher.server.Server; + +public class SayCommand implements Command { + boolean isAction; + String text; + + public SayCommand() { + + } + + public SayCommand(String text, boolean isAction) { + this.isAction = isAction; + this.text = text; + } + + public int perform(Server server, Connection connection) { + User user = connection.getUser(); + + if(user == null) { + /** Signed in users only. */ + return Connection.SEND_NONE; + } + + if(user.isViewer()) { + connection.getSendQueue().offer(new CommandEntry(0, new ServerMessageCommand("Chat is not permitted in viewer mode. Please sign in to participate."))); + return Connection.SEND_NONE; + } + + AccessController controller = server.getAccessController(); + if(controller != null && !controller.isMessageAllowed(server, connection.getUser(), text)) { + connection.getSendQueue().offer(new CommandEntry(0, new ServerMessageCommand("You are not allowed to say that."))); + return Connection.SEND_NONE; + } + + if(isAction) { + Log.info("* " + connection.getUser().getName() + text); + } else { + Log.info("<" + connection.getUser().getName() + "> " + text); + } + + return Connection.SEND_ALL; + } + + public void decode(ByteBuffer in) { + isAction = in.get() != 0 ? true : false; + StringBuffer textBuffer = new StringBuffer(); + while(in.remaining() >= 2) { + textBuffer.append(in.getChar()); + } + text = textBuffer.toString(); + } + + public void encode(ByteBuffer out) { + out.put((byte) (isAction ? 0xFF : 0x00)); + for(int i = 0; i < text.length(); i++) { + out.putChar(text.charAt(i)); + } + } +} diff --git a/src/com/jotuntech/sketcher/server/command/ServerMessageCommand.java b/src/com/jotuntech/sketcher/server/command/ServerMessageCommand.java new file mode 100755 index 0000000..c255bb8 --- /dev/null +++ b/src/com/jotuntech/sketcher/server/command/ServerMessageCommand.java @@ -0,0 +1,37 @@ +package com.jotuntech.sketcher.server.command; + +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.server.Command; +import com.jotuntech.sketcher.server.Connection; +import com.jotuntech.sketcher.server.Server; + +public class ServerMessageCommand implements Command { + String text; + + public ServerMessageCommand() { + } + + public ServerMessageCommand(String text) { + this.text = text; + } + + public int perform(Server server, Connection connection) { + /** Return silently */ + return Connection.SEND_NONE; + } + + public void decode(ByteBuffer in) { + StringBuffer textBuffer = new StringBuffer(); + while(in.remaining() >= 2) { + textBuffer.append(in.getChar()); + } + text = textBuffer.toString(); + } + + public void encode(ByteBuffer out) { + for(int i = 0; i < text.length(); i++) { + out.putChar(text.charAt(i)); + } + } +} diff --git a/src/com/jotuntech/sketcher/server/command/SetBrushCommand.java b/src/com/jotuntech/sketcher/server/command/SetBrushCommand.java new file mode 100755 index 0000000..50daa76 --- /dev/null +++ b/src/com/jotuntech/sketcher/server/command/SetBrushCommand.java @@ -0,0 +1,49 @@ +package com.jotuntech.sketcher.server.command; + +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.common.Brush; +import com.jotuntech.sketcher.common.User; +import com.jotuntech.sketcher.server.Command; +import com.jotuntech.sketcher.server.Connection; +import com.jotuntech.sketcher.server.Server; + +public class SetBrushCommand implements Command { + Brush brush; + + public SetBrushCommand() { + + } + + public SetBrushCommand(Brush brush) { + this.brush = brush; + } + + public int perform(Server server, Connection connection) { + return perform(connection.getUser()) ? Connection.SEND_OTHERS : Connection.SEND_NONE; + } + + private boolean perform(User user) { + if(user == null) { + /** Signed in users only */ + return false; + } + + if(user.isViewer()) { + return false; + } + + user.setBrush(brush); + + return true; + } + + public void decode(ByteBuffer in) { + brush = new Brush(); + brush.decode(in); + } + + public void encode(ByteBuffer out) { + brush.encode(out); + } +} diff --git a/src/com/jotuntech/sketcher/server/command/SetColorCommand.java b/src/com/jotuntech/sketcher/server/command/SetColorCommand.java new file mode 100755 index 0000000..169af55 --- /dev/null +++ b/src/com/jotuntech/sketcher/server/command/SetColorCommand.java @@ -0,0 +1,48 @@ +package com.jotuntech.sketcher.server.command; + +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.common.User; +import com.jotuntech.sketcher.server.Command; +import com.jotuntech.sketcher.server.Connection; +import com.jotuntech.sketcher.server.Server; + +public class SetColorCommand implements Command { + static boolean firstWhite = true; + int color; + + public SetColorCommand() { + } + + public SetColorCommand(int color) { + this.color = color; + } + + public int perform(Server server, Connection connection) { + return perform(connection.getUser()) ? Connection.SEND_OTHERS : Connection.SEND_NONE; + } + + private boolean perform(User user) { + if(user == null) { + /** Signed in users only */ + return false; + } + + if(user.isViewer()) { + return false; + } + + user.setColor(color); + + return true; + } + + public void decode(ByteBuffer in) { + color = in.getInt(); + } + + public void encode(ByteBuffer out) { + out.putInt(color); + } + +} diff --git a/src/com/jotuntech/sketcher/server/command/SetLayerCommand.java b/src/com/jotuntech/sketcher/server/command/SetLayerCommand.java new file mode 100755 index 0000000..4cad756 --- /dev/null +++ b/src/com/jotuntech/sketcher/server/command/SetLayerCommand.java @@ -0,0 +1,61 @@ +package com.jotuntech.sketcher.server.command; + +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.common.Canvas; +import com.jotuntech.sketcher.common.Layer; +import com.jotuntech.sketcher.common.User; +import com.jotuntech.sketcher.server.Command; +import com.jotuntech.sketcher.server.Connection; +import com.jotuntech.sketcher.server.Server; + +public class SetLayerCommand implements Command { + Integer layerKey; + + public SetLayerCommand() { + } + + public SetLayerCommand(Integer layerKey) { + this.layerKey = layerKey; + } + + public int perform(Server server, Connection connection) { + return perform(server.getCanvas(), connection.getUser()); + } + + private int perform(Canvas canvas, User user) { + if(user == null) { + return Connection.SEND_NONE; + } + + if(user.isViewer()) { + return Connection.SEND_NONE; + } + + if(canvas == null) { + return Connection.SEND_NONE; + } + + Layer layer = canvas.getLayerMap().get(layerKey); + + if(layer == null) { + return Connection.SEND_NONE; + } + + if(user.getLayer() == layer) { + return Connection.SEND_NONE; + } + + user.setLayer(layer); + + return Connection.SEND_OTHERS; + } + + public void decode(ByteBuffer in) { + layerKey = in.getInt(); + } + + public void encode(ByteBuffer out) { + out.putInt(layerKey); + } +} diff --git a/src/com/jotuntech/sketcher/server/command/SignInCommand.java b/src/com/jotuntech/sketcher/server/command/SignInCommand.java new file mode 100755 index 0000000..88341f7 --- /dev/null +++ b/src/com/jotuntech/sketcher/server/command/SignInCommand.java @@ -0,0 +1,241 @@ +/** + * + */ +package com.jotuntech.sketcher.server.command; + +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.Map.Entry; + +import com.jotuntech.sketcher.common.BitmapUndoData; +import com.jotuntech.sketcher.common.Canvas; +import com.jotuntech.sketcher.common.Layer; +import com.jotuntech.sketcher.common.Log; +import com.jotuntech.sketcher.common.UndoEntry; +import com.jotuntech.sketcher.common.User; +import com.jotuntech.sketcher.server.AccessController; +import com.jotuntech.sketcher.server.Command; +import com.jotuntech.sketcher.server.CommandEntry; +import com.jotuntech.sketcher.server.Connection; +import com.jotuntech.sketcher.server.Server; + +/** + * @author Thor Harald Johansen + * + */ +public class SignInCommand implements Command { + Integer peerKey; + String login; + String password; + boolean viewer; + + public SignInCommand() { + + } + + public SignInCommand(Integer peerKey, String login, String password, boolean viewer) { + this.peerKey = peerKey; + this.login = login; + this.password = password; + this.viewer = viewer; + } + + public void decode(ByteBuffer in) { + peerKey = in.getInt(); + viewer = in.get() != 0; + int loginLength = (in.get() & 0xFF) + 1; + StringBuffer loginBuffer = new StringBuffer(); + for(int i = 0; i < loginLength; i++) { + loginBuffer.append(in.getChar()); + } + login = loginBuffer.toString(); + + StringBuffer passwordBuffer = new StringBuffer(); + while(in.remaining() >= 2) { + passwordBuffer.append(in.getChar()); + } + password = passwordBuffer.toString(); + } + + public void encode(ByteBuffer out) { + out.putInt(peerKey); + out.put((byte) (viewer ? 0xFF : 0x00)); + out.put((byte) (login.length() - 1)); + for(int i = 0; i < login.length(); i++) { + out.putChar(login.charAt(i)); + } + for(int i = 0; i < password.length(); i++) { + out.putChar(password.charAt(i)); + } + } + + public int perform(Server server, Connection connection) { + AccessController ac = server.getAccessController(); + + if(!ac.isLoginAllowed(server, login, password)) { + connection.getSendQueue().offer(new CommandEntry(0, new ServerMessageCommand("Access to Sketcher is denied."))); + connection.setTimeOfDeath(System.currentTimeMillis() + 5000); + return Connection.SEND_NONE; + } + + password = new String(); + + /** Kill any zombies */ + for(Map.Entry e : server.getConnectionMap().entrySet()) { + Integer k = e.getKey(); + Connection c = e.getValue(); + User u = c.getUser(); + + if(u != null && u != connection.getUser() && u.getName().equalsIgnoreCase(login)) { + /** Tell duplicate connection to die */ + for(Connection c2 : server.getConnectionMap().values()) { + c2.getSendQueue().offer(new CommandEntry(k, new SignOutCommand("Duplicate user removed"))); + } + c.setTimeOfDeath(System.currentTimeMillis()); + } + } + + /** Create user */ + User user = new User(login); + + /** Assign proper viewer status */ + viewer = ac.isViewer(server, user); + user.setViewer(viewer); + + /** Fetch canvas for context */ + Canvas canvas = server.getCanvas(); + + /** Tell client canvas size */ + connection.getSendQueue().offer(new CommandEntry(0, new CanvasCommand(canvas.getWidth(), canvas.getHeight()))); + + /** Send MOTD if there is any */ + String motd = server.getMOTD(); + if(motd != null) { + connection.getSendQueue().offer(new CommandEntry(0, new ServerMessageCommand(motd))); + } + + /** Assign proper peer key */ + peerKey = server.getConnectionMap().getKeyForValue(connection); + + /** Tell client its signed in */ + connection.getSendQueue().offer(new CommandEntry(peerKey, this)); + + //int layerBytes = 0, inflatedBytes = 0; + for(Entry e : server.getCanvas().getLayerMap().entrySet()) { + Integer k = e.getKey(); + Layer l = e.getValue(); + + connection.getSendQueue().offer(new CommandEntry(0, new CreateLayerCommand(k, l.getType(), l.getName()))); + connection.getSendQueue().offer(new CommandEntry(0, new ServerMessageCommand("Loading layer " + l.getName()))); + + l.reset(); + for(byte[] data = l.encode(); data != null; data = l.encode()) { + connection.getSendQueue().offer(new CommandEntry(0, new LayerDataCommand(k, false, data))); + //layerBytes += data.length; + //inflatedBytes += BitmapTile.SIZE * BitmapTile.SIZE * 4; + } + } + + /** Send user list */ + for(Map.Entry e : server.getConnectionMap().entrySet()) { + Integer k = e.getKey(); + Connection c = e.getValue(); + User u = c.getUser(); + + if(c.hasTimeOfDeath() || u == null || u == connection.getUser()) { + continue; + } + + /** Send user name */ + connection.getSendQueue().offer(new CommandEntry(k, new SignInCommand(e.getKey(), u.getName(), new String(), u.isViewer()))); + + /** If brush is set for user, then send brush */ + if(u.getBrush() != null) { + connection.getSendQueue().offer(new CommandEntry(k, new SetBrushCommand(u.getBrush()))); + } + + /** Send color for user */ + connection.getSendQueue().offer(new CommandEntry(k, new SetColorCommand(u.getColor()))); + + /** If layer is set for user, then send layer */ + Integer layerKey = canvas.getLayerMap().getKeyForValue(u.getLayer()); + if(layerKey != null) { + connection.getSendQueue().offer(new CommandEntry(k, new SetLayerCommand(layerKey))); + } + + /** If cursor is set for user, then send cursor */ + if(u.getCursor() != null) { + connection.getSendQueue().offer(new CommandEntry(k, new CursorCommand(u.getCursor()))); + } + } + + /** Send user associated data */ + for(Map.Entry e : server.getConnectionMap().entrySet()) { + Integer k = e.getKey(); + Connection c = e.getValue(); + User u = c.getUser(); + + if(c.hasTimeOfDeath() || u == null || u == connection.getUser()) { + continue; + } + + connection.getSendQueue().offer(new CommandEntry(0, new ServerMessageCommand("Loading temporary data for " + u.getName()))); + + /** Send phantom layer data */ + Layer l = u.getPhantomLayer(); + for(byte[] data = l.encode(); data != null; data = l.encode()) { + connection.getSendQueue().offer(new CommandEntry(k, new LayerDataCommand(0, true, data))); + //layerBytes += data.length; + //inflatedBytes += BitmapTile.SIZE * BitmapTile.SIZE * 4; + } + + /** Send undo deque */ + for(UndoEntry ue : u.getUndoDeque()) { + Integer layerKey = canvas.getLayerMap().getKeyForValue(ue.getLayer()); + connection.getSendQueue().offer(new CommandEntry(k, new UndoEntryCommand(layerKey))); + BitmapUndoData bud = (BitmapUndoData) ue.getUndoData(); + bud.reset(); + for(byte[] data = bud.encode(); data != null; data = bud.encode()) { + connection.getSendQueue().offer(new CommandEntry(k, new UndoDataCommand(data))); + } + } + } + + /** Assign user to connection */ + connection.setUser(user); + + /** Notify connection map listeners */ + server.getConnectionMap().change(); + + /** Add canvas as phantom layer listener */ + user.getPhantomLayer().addImageObserver(server.getCanvas()); + + /** Let the user know that load is complete. */ + connection.getSendQueue().offer(new CommandEntry(0, new ServerMessageCommand("Sketcher is ready."))); + + /** Log event */ + Log.info(login + " has signed in."); + + /** HACK: Broadcast sign-in manually. */ + for(Connection c : server.getConnectionMap().values()) { + if(c.getUser() == user) { + continue; + } + c.getSendQueue().offer(new CommandEntry(peerKey, this)); + } + + /** Set and send default layer */ + for(Map.Entry e : canvas.getLayerMap().entrySet()) { + if("Doodle".equals(e.getValue().getName())) { + user.setLayer(e.getValue()); + for(Connection c : server.getConnectionMap().values()) { + c.getSendQueue().offer(new CommandEntry(peerKey, new SetLayerCommand(e.getKey()))); + } + break; + } + } + + /** Return "silently" */ + return Connection.SEND_NONE; + } +} diff --git a/src/com/jotuntech/sketcher/server/command/SignOutCommand.java b/src/com/jotuntech/sketcher/server/command/SignOutCommand.java new file mode 100755 index 0000000..e3d6f39 --- /dev/null +++ b/src/com/jotuntech/sketcher/server/command/SignOutCommand.java @@ -0,0 +1,47 @@ +package com.jotuntech.sketcher.server.command; + +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.common.Log; +import com.jotuntech.sketcher.server.Command; +import com.jotuntech.sketcher.server.Connection; +import com.jotuntech.sketcher.server.Server; + +public class SignOutCommand implements Command { + String message; + + public SignOutCommand() { + message = new String(); + } + + public SignOutCommand(String message) { + this.message = message; + } + + public int perform(Server server, Connection connection) { + /** Set time of death */ + connection.setTimeOfDeath(System.currentTimeMillis()); + + /** Log it */ + if(connection.getUser() != null) { + Log.info(connection.getUser().getName() + " has signed out (" + message + ")."); + } + + /** Return and broadcast */ + return connection.getUser() == null ? Connection.SEND_NONE : Connection.SEND_OTHERS; + } + + public void decode(ByteBuffer in) { + StringBuffer messageBuffer = new StringBuffer(); + while(in.remaining() >= 2) { + messageBuffer.append(in.getChar()); + } + message = messageBuffer.toString(); + } + + public void encode(ByteBuffer out) { + for(int i = 0; i < message.length(); i++) { + out.putChar(message.charAt(i)); + } + } +} diff --git a/src/com/jotuntech/sketcher/server/command/UndoCommand.java b/src/com/jotuntech/sketcher/server/command/UndoCommand.java new file mode 100755 index 0000000..6cc8dbd --- /dev/null +++ b/src/com/jotuntech/sketcher/server/command/UndoCommand.java @@ -0,0 +1,38 @@ +package com.jotuntech.sketcher.server.command; + +import java.nio.ByteBuffer; +import java.util.Deque; + +import com.jotuntech.sketcher.common.UndoEntry; +import com.jotuntech.sketcher.common.User; +import com.jotuntech.sketcher.server.Command; +import com.jotuntech.sketcher.server.Connection; +import com.jotuntech.sketcher.server.Server; + +public class UndoCommand implements Command { + + public int perform(Server server, Connection connection) { + User user = connection.getUser(); + if(user == null) { + return Connection.SEND_NONE; + } + if(user.isViewer()) { + return Connection.SEND_NONE; + } + Deque undoDeque = connection.getUser().getUndoDeque(); + if(undoDeque.size() == 0) { + return Connection.SEND_NONE; + } + UndoEntry undoEntry = undoDeque.removeFirst(); + undoEntry.getLayer().undo(undoEntry.getUndoData()); + return Connection.SEND_OTHERS; + } + + public void decode(ByteBuffer in) { + + } + + public void encode(ByteBuffer out) { + + } +} diff --git a/src/com/jotuntech/sketcher/server/command/UndoDataCommand.java b/src/com/jotuntech/sketcher/server/command/UndoDataCommand.java new file mode 100755 index 0000000..07fdc56 --- /dev/null +++ b/src/com/jotuntech/sketcher/server/command/UndoDataCommand.java @@ -0,0 +1,32 @@ +package com.jotuntech.sketcher.server.command; + +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.server.Command; +import com.jotuntech.sketcher.server.Connection; +import com.jotuntech.sketcher.server.Server; + +public class UndoDataCommand implements Command { + byte[] data; + + public UndoDataCommand() { + + } + + public UndoDataCommand(byte[] data) { + this.data = data; + } + + public int perform(Server server, Connection connection) { + return Connection.SEND_NONE; + } + + public void decode(ByteBuffer in) { + data = new byte[in.remaining()]; + in.get(data); + } + + public void encode(ByteBuffer out) { + out.put(data); + } +} diff --git a/src/com/jotuntech/sketcher/server/command/UndoEntryCommand.java b/src/com/jotuntech/sketcher/server/command/UndoEntryCommand.java new file mode 100755 index 0000000..1b75754 --- /dev/null +++ b/src/com/jotuntech/sketcher/server/command/UndoEntryCommand.java @@ -0,0 +1,31 @@ +package com.jotuntech.sketcher.server.command; + +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.server.Command; +import com.jotuntech.sketcher.server.Connection; +import com.jotuntech.sketcher.server.Server; + +public class UndoEntryCommand implements Command { + int layerKey; + + public UndoEntryCommand() { + + } + + public UndoEntryCommand(int layerKey) { + this.layerKey = layerKey; + } + + public int perform(Server server, Connection connection) { + return Connection.SEND_NONE; + } + + public void decode(ByteBuffer in) { + layerKey = in.getInt(); + } + + public void encode(ByteBuffer out) { + out.putInt(layerKey); + } +} diff --git a/src/com/jotuntech/sketcher/server/command/VoiceCommand.java b/src/com/jotuntech/sketcher/server/command/VoiceCommand.java new file mode 100755 index 0000000..be03b5e --- /dev/null +++ b/src/com/jotuntech/sketcher/server/command/VoiceCommand.java @@ -0,0 +1,44 @@ +package com.jotuntech.sketcher.server.command; + +import java.nio.ByteBuffer; + +import com.jotuntech.sketcher.common.User; +import com.jotuntech.sketcher.server.Command; +import com.jotuntech.sketcher.server.Connection; +import com.jotuntech.sketcher.server.Server; + +public class VoiceCommand implements Command { + boolean voiceEnabled; + + public VoiceCommand() { + + } + + public VoiceCommand(boolean voiceEnabled) { + this.voiceEnabled = voiceEnabled; + } + + public int perform(Server server, Connection connection) { + User user = connection.getUser(); + if(user == null) { + return Connection.SEND_NONE; + } + if(user.isViewer()) { + return Connection.SEND_NONE; + } + if(voiceEnabled) { + server.getVoiceServer().addClient(server.getConnectionMap().getKeyForValue(connection)); + } else { + server.getVoiceServer().removeClient(server.getConnectionMap().getKeyForValue(connection)); + } + return Connection.SEND_NONE; + } + + public void decode(ByteBuffer in) { + voiceEnabled = in.get() != 0; + } + + public void encode(ByteBuffer out) { + out.put((byte) (voiceEnabled ? 0xFF : 0x00)); + } +} diff --git a/src/com/jotuntech/sketcher/tests/CacheTest.java b/src/com/jotuntech/sketcher/tests/CacheTest.java new file mode 100755 index 0000000..8c53d9d --- /dev/null +++ b/src/com/jotuntech/sketcher/tests/CacheTest.java @@ -0,0 +1,54 @@ +package com.jotuntech.sketcher.tests; + + +public class CacheTest { + static int[] intArray; + static short[] shortArray; + + public static void main(String[] args) { + for(int i = 0; i < 1000; i++) { + test(); + } + } + + static private void fillArrays() { + intArray = new int[512]; + shortArray = new short[512]; + for(int i = 0; i < 512; ++i) { + intArray[i] = (int) Math.round(Math.random() * 511d); + shortArray[i] = (short) Math.round(Math.random() * 511d); + } + } + + static private void test() { + fillArrays(); + long intStart = System.nanoTime(); + for(int i = 0; i < 8192; i++) { + testInt(); + } + long intStop = System.nanoTime(); + long intTime = intStop - intStart; + + long shortStart = System.nanoTime(); + for(int i = 0; i < 8192; i++) { + testShort(); + } + long shortStop = System.nanoTime(); + long shortTime = shortStop - shortStart; + + System.err.println("Integer test: " + intTime + " ns"); + System.err.println("Short test: " + shortTime + " ns"); + } + + static private void testInt() { + for(int i = 0; i < 512; ++i) { + intArray[i] ^= intArray[i] >> 1; + } + } + + static private void testShort() { + for(int i = 0; i < 512; ++i) { + shortArray[i] ^= shortArray[i] >> 1; + } + } +} diff --git a/src/com/jotuntech/sketcher/tests/PackBitsTest.java b/src/com/jotuntech/sketcher/tests/PackBitsTest.java new file mode 100755 index 0000000..a50744e --- /dev/null +++ b/src/com/jotuntech/sketcher/tests/PackBitsTest.java @@ -0,0 +1,80 @@ +package com.jotuntech.sketcher.tests; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +import com.jotuntech.sketcher.client.PackBitsOutputStream; + + +public class PackBitsTest { + public static void main(String[] args) { + int x = 120; + x &= 0x1FF; + byte a = (byte) (x >>> 8); + byte b = (byte) x; + int y = ((a & 0xFF) << 8) | (b & 0xFF); + + System.out.println(y); + + byte[] testData = new byte[] { + 1, 2, 3, 4, 5, 5, 5, 5, 6, 5, 5, 5, 6, 7, 8, 9, + 1, 2, 3, 4, 5, 5, 5, 5, 6, 5, 6, 5, 6, 7, 8, 9, + 1, 2, 3, 4, 5, 5, 4, 3, 2, 5, 6, 5, 6, 7, 8, 9, + 1, 2, 3, 4, 5, 5, 5, 5, 6, 5, 6, 5, 6, 7, 8, 9, + 1, 2, 3, 4, 5, 5, 5, 5, 6, 5, 6, 5, 6, 7, 8, 9, + 1, 2, 3, 4, 5, 5, 5, 5, 6, 5, 6, 5, 6, 7, 8, 9, + 1, 2, 3, 4, 5, 5, 5, 5, 6, 5, 6, 5, 6, 7, 8, 9, + 1, 2, 3, 4, 5, 5, 5, 5, 6, 5, 6, 5, 6, 7, 8, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 + }; + + byte[] expectedOutput = new byte[] { + 3, 1, 2, 3, 4, -3, 5, 0, 6, -2, 5, 7, 6, 7, 8, 9, 1, + 2, 3, 4, -3, 5, 11, 6, 5, 6, 5, 6, 7, 8, 9, 1, 2, + 3, 4, 1, 5, 5, 13, 4, 3, 2, 5, 6, 5, 6, 7, 8, 9, + 1, 2, 3, 4, -3, 5, 11, 6, 5, 6, 5, 6, 7, 8, 9, 1, + 2, 3, 4, -3, 5, 11, 6, 5, 6, 5, 6, 7, 8, 9, 1, 2, + 3, 4, -3, 5, 11, 6, 5, 6, 5, 6, 7, 8, 9, 1, 2, 3, + 4, -3, 5, 11, 6, 5, 6, 5, 6, 7, 8, 9, 1, 2, 3, 4, + -3, 5, 6, 6, 5, 6, 5, 6, 7, 8, -127, 9, 16, 9, 1, 2, + 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 + }; + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(baos); + PackBitsOutputStream pbos = new PackBitsOutputStream(dos); + try { + pbos.write(testData); + pbos.close(); + byte[] output = baos.toByteArray(); + if(output.length != expectedOutput.length) { + System.err.println("Length of output " + output.length + " does not match expected " + expectedOutput.length + "!"); + return; + } + for(int i = 0; i < output.length; i++) { + System.err.print(output[i] + " "); + if(output[i] != expectedOutput[i]) { + System.err.println(); + System.err.println("Output error at index " + i + ": Expected " + expectedOutput[i] + " but got " + output[i] + "!"); + return; + } + if(i % 16 == 0 && i > 0) { + System.err.println(); + } + } + System.err.println(); + System.err.println("Test successful!"); + } catch (IOException e) { + e.printStackTrace(); + } + } +}