eu.maxschuster.vaadin.signaturefield.client.signaturepad.SignaturePad.java Source code

Java tutorial

Introduction

Here is the source code for eu.maxschuster.vaadin.signaturefield.client.signaturepad.SignaturePad.java

Source

/*
 * Copyright 2014 Max Schuster
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package eu.maxschuster.vaadin.signaturefield.client.signaturepad;

import com.google.gwt.canvas.client.Canvas;
import com.google.gwt.canvas.dom.client.Context2d;
import com.google.gwt.dom.client.ImageElement;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.Touch;
import com.google.gwt.event.dom.client.LoadEvent;
import com.google.gwt.event.dom.client.LoadHandler;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.event.dom.client.MouseMoveEvent;
import com.google.gwt.event.dom.client.MouseMoveHandler;
import com.google.gwt.event.dom.client.MouseUpEvent;
import com.google.gwt.event.dom.client.MouseUpHandler;
import com.google.gwt.event.dom.client.TouchEndEvent;
import com.google.gwt.event.dom.client.TouchEndHandler;
import com.google.gwt.event.dom.client.TouchMoveEvent;
import com.google.gwt.event.dom.client.TouchMoveHandler;
import com.google.gwt.event.dom.client.TouchStartEvent;
import com.google.gwt.event.dom.client.TouchStartHandler;
import com.google.gwt.event.shared.GwtEvent;
import com.google.gwt.event.shared.HandlerManager;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.event.shared.HasHandlers;
import com.google.gwt.safehtml.shared.SafeUri;
import com.google.gwt.safehtml.shared.UriUtils;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.RootPanel;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * A GWT port of {@code SignaturePad} by
 * <b>Szymon Nowak (<a href="https://github.com/szimek">
 * szimek</a>)</b>.<br>
 * <br>
 * The MIT licensed original version can be found at 
 * <a href="https://github.com/szimek/signature_pad">
 * https://github.com/szimek/signature_pad</a><br>
 * <br>
 * @author Max Schuster
 */
public class SignaturePad implements HasHandlers {

    private final Canvas canvas;

    private final Context2d context;

    private final List<Point> points = new ArrayList<Point>();

    private final HandlerManager handlerManager;

    private boolean empty = true;

    private double velocityFilterWeight = 0.7d;

    private double minWidth = 0.5d;

    private double maxWidth = 2.5d;

    private Double dotSize = null;

    private String penColor = "black";

    private String backgroundColor = "rgba(0,0,0,0)";

    private double lastVelocity = 0;

    private double lastWidth = 0;

    boolean mouseButtonDown = false;

    private boolean readOnly = false;

    public SignaturePad(Canvas canvas) {
        handlerManager = new HandlerManager(this);

        this.canvas = canvas;
        context = canvas.getContext2d();

        handleMouseEvents();
        handleTouchEvents();
    }

    @Override
    public void fireEvent(GwtEvent<?> event) {
        handlerManager.fireEvent(event);
    }

    public HandlerRegistration addBeginEventHandler(StrokeBeginEventHandler handler) {
        return handlerManager.addHandler(StrokeBeginEvent.TYPE, handler);
    }

    public HandlerRegistration addEndEventHandler(StrokeEndEventHandler handler) {
        return handlerManager.addHandler(StrokeEndEvent.TYPE, handler);
    }

    public void removeBeginEventHandler(StrokeBeginEventHandler handler) {
        handlerManager.removeHandler(StrokeBeginEvent.TYPE, handler);
    }

    public void removeEndEventHandler(StrokeEndEventHandler handler) {
        handlerManager.removeHandler(StrokeEndEvent.TYPE, handler);
    }

    public void clear() {
        context.setFillStyle(getBackgroundColor());
        context.clearRect(0d, 0d, canvas.getCoordinateSpaceWidth(), canvas.getCoordinateSpaceHeight());
        context.fillRect(0d, 0d, canvas.getCoordinateSpaceWidth(), canvas.getCoordinateSpaceHeight());
        reset();
    }

    public String toDataURL(String imageType, float quality) {
        return canvas.toDataUrl(imageType);
    }

    public void fromDataURL(String dataURL, final Double width, final Double height) {
        SafeUri uri = UriUtils.fromTrustedString(dataURL);
        final Image image = new Image(uri);
        image.addLoadHandler(new LoadHandler() {
            @Override
            public void onLoad(LoadEvent event) {
                ImageElement imageElement = ImageElement.as(image.getElement());
                if (width == null && height == null) {
                    context.drawImage(imageElement, 0d, 0d);
                } else {
                    context.drawImage(imageElement, 0d, 0d, width, height);
                }

                RootPanel.get().remove(image);
            }
        });
        RootPanel.get().add(image);
        empty = false;
    }

    public void fromDataURL(String dataURL) {
        Double ratio = getDevicePixelWidth();
        if (ratio == null) {
            ratio = 1d;
        }
        final double width = canvas.getCoordinateSpaceWidth() / ratio;
        final double height = canvas.getCoordinateSpaceHeight() / ratio;

        fromDataURL(dataURL, width, height);
    }

    private void strokeUpdate(EventWrapper event) {
        if (isReadOnly()) {
            return;
        }
        Point point = createPoint(event);
        addPoint(point);
    };

    private void strokeBegin(EventWrapper event) {
        if (isReadOnly()) {
            return;
        }
        reset();
        strokeUpdate(event);
        fireEvent(new StrokeBeginEvent(this));
    };

    private void strokeDraw(Point point) {
        context.beginPath();
        drawPoint(point.getX(), point.getY(), getAppliedDotSize());
        context.closePath();
        context.fill();
    };

    private void strokeEnd(EventWrapper event) {
        boolean canDrawCurve = points.size() > 2;
        Point point = points.get(0);

        if (!canDrawCurve && point != null) {
            strokeDraw(point);
        }

        fireEvent(new StrokeEndEvent(this));
    };

    private void handleMouseEvents() {

        canvas.addMouseDownHandler(new MouseDownHandler() {

            @Override
            public void onMouseDown(MouseDownEvent event) {
                event.preventDefault();
                canvas.setFocus(true);
                NativeEvent nEvent = event.getNativeEvent();
                if ((nEvent.getButton() & NativeEvent.BUTTON_LEFT) != 0) {
                    Event.setCapture(canvas.getElement());
                    mouseButtonDown = true;
                    strokeBegin(new EventWrapper(nEvent));
                }
            }
        });

        canvas.addMouseMoveHandler(new MouseMoveHandler() {

            @Override
            public void onMouseMove(MouseMoveEvent event) {
                event.preventDefault();
                if (mouseButtonDown) {
                    strokeUpdate(new EventWrapper(event.getNativeEvent()));
                }
            }
        });

        canvas.addMouseUpHandler(new MouseUpHandler() {

            @Override
            public void onMouseUp(MouseUpEvent event) {
                event.preventDefault();
                Event.releaseCapture(canvas.getElement());
                NativeEvent nEvent = event.getNativeEvent();
                if ((nEvent.getButton() & NativeEvent.BUTTON_LEFT) != 0 && mouseButtonDown) {
                    mouseButtonDown = false;
                    strokeEnd(new EventWrapper(nEvent));
                }
            }
        });
    }

    private void handleTouchEvents() {

        // Pass touch events to canvas element on mobile IE.
        canvas.getElement().getStyle().setProperty("-ms-touch-action", "none");

        canvas.addTouchStartHandler(new TouchStartHandler() {

            @Override
            public void onTouchStart(TouchStartEvent event) {
                Event.setCapture(event.getRelativeElement());
                canvas.setFocus(true);
                event.preventDefault();
                Touch touch = event.getTouches().get(0);
                strokeBegin(new EventWrapper(touch));
            }
        });

        canvas.addTouchMoveHandler(new TouchMoveHandler() {

            @Override
            public void onTouchMove(TouchMoveEvent event) {
                event.preventDefault();
                Touch touch = event.getTouches().get(0);
                strokeUpdate(new EventWrapper(touch));
            }
        });

        canvas.addTouchEndHandler(new TouchEndHandler() {

            @Override
            public void onTouchEnd(TouchEndEvent event) {
                strokeEnd(new EventWrapper(event.getNativeEvent()));
                Event.releaseCapture(event.getRelativeElement());
            }
        });
    }

    public boolean isEmpty() {
        return empty;
    }

    private void reset() {
        points.clear();
        lastVelocity = 0;
        lastWidth = (minWidth + maxWidth) / 2;
        empty = true;
        context.setFillStyle(penColor);
    }

    private Point createPoint(EventWrapper event) {
        return new Point(event.getClientX() - canvas.getAbsoluteLeft(),
                event.getClientY() - canvas.getAbsoluteTop());
    }

    private void addPoint(Point point) {
        Point c2;
        Point c3;
        Beizer curve;
        CurveControlPoints tmp;

        points.add(point);

        if (points.size() > 2) {
            // To reduce the initial lag make it work with 3 points
            // by copying the first point to the beginning.
            if (points.size() == 3)
                points.add(0, points.get(0));

            tmp = calculateCurveControlPoints(points.get(0), points.get(1), points.get(2));
            c2 = tmp.getPoint2();
            tmp = calculateCurveControlPoints(points.get(1), points.get(2), points.get(3));
            c3 = tmp.getPoint1();
            curve = new Beizer(points.get(1), c2, c3, points.get(2));
            addCurve(curve);

            // Remove the first element from the list,
            // so that we always have no more than 4 points in points array.
            points.remove(0);
        }
    }

    private CurveControlPoints calculateCurveControlPoints(Point s1, Point s2, Point s3) {
        double dx1 = s1.getX() - s2.getX(), dy1 = s1.getY() - s2.getY();
        double dx2 = s2.getX() - s3.getX(), dy2 = s2.getY() - s3.getY();

        Coordinates m1 = new Coordinates((s1.getX() + s2.getX()) / 2d, (s1.getY() + s2.getY()) / 2d);
        Coordinates m2 = new Coordinates((s2.getX() + s3.getX()) / 2d, (s2.getY() + s3.getY()) / 2d);

        double l1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);
        double l2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);

        double dxm = (m1.getX() - m2.getX());
        double dym = (m1.getY() - m2.getY());

        double k = l2 / (l1 + l2);
        Coordinates cm = new Coordinates(m2.getX() + dxm * k, m2.getY() + dym * k);

        double tx = s2.getX() - cm.getX();
        double ty = s2.getY() - cm.getY();

        return new CurveControlPoints(new Point(m1.getX() + tx, m1.getY() + ty),
                new Point(m2.getX() + tx, m2.getY() + ty));
    }

    private void addCurve(Beizer curve) {
        Point startPoint = curve.getStartPoint();
        Point endPoint = curve.getEndPoint();

        double velocity = endPoint.velocityFrom(startPoint);
        velocity = velocityFilterWeight * velocity + (1 - velocityFilterWeight) * lastVelocity;

        double newWidth = strokeWidth(velocity);
        drawCurve(curve, lastWidth, newWidth);

        lastVelocity = velocity;
        lastWidth = newWidth;
    }

    private void drawPoint(double x, double y, double size) {
        context.moveTo(x, y);
        context.arc(x, y, size, 0, 2 * Math.PI, false);
        empty = false;
    }

    private void drawCurve(Beizer curve, double startWidth, double endWidth) {
        double widthDelta = endWidth - startWidth;
        double drawSteps = Math.floor(curve.getLength());
        double width;
        double t;
        double tt;
        double ttt;
        double u;
        double uu;
        double uuu;
        double x;
        double y;

        context.beginPath();
        for (int i = 0; i < drawSteps; i++) {
            // Calculate the Bezier (x, y) coordinate for this step.
            t = i / drawSteps;
            tt = t * t;
            ttt = tt * t;
            u = 1 - t;
            uu = u * u;
            uuu = uu * u;

            x = uuu * curve.getStartPoint().getX();
            x += 3 * uu * t * curve.getControl1().getX();
            x += 3 * u * tt * curve.getControl2().getX();
            x += ttt * curve.getEndPoint().getX();

            y = uuu * curve.getStartPoint().getY();
            y += 3 * uu * t * curve.getControl1().getY();
            y += 3 * u * tt * curve.getControl2().getY();
            y += ttt * curve.getEndPoint().getY();

            width = startWidth + ttt * widthDelta;
            drawPoint(x, y, width);
        }
        context.closePath();
        context.fill();
    }

    private double strokeWidth(double velocity) {
        return Math.max(maxWidth / (velocity + 1), minWidth);
    }

    protected double getAppliedDotSize() {
        if (dotSize != null) {
            return dotSize;
        }
        return (minWidth + maxWidth) / 2;
    }

    protected static native Double getDevicePixelWidth() /*-{
                                                         return $wnd.devicePixelWidth;
                                                         }-*/;

    public Canvas getCanvas() {
        return canvas;
    }

    public double getVelocityFilterWeight() {
        return velocityFilterWeight;
    }

    public void setVelocityFilterWeight(double velocityFilterWeight) {
        this.velocityFilterWeight = velocityFilterWeight;
    }

    public double getMinWidth() {
        return minWidth;
    }

    public void setMinWidth(double minWidth) {
        this.minWidth = minWidth;
    }

    public double getMaxWidth() {
        return maxWidth;
    }

    public void setMaxWidth(double maxWidth) {
        this.maxWidth = maxWidth;
    }

    public Double getDotSize() {
        return dotSize;
    }

    public void setDotSize(Double dotSize) {
        this.dotSize = dotSize;
    }

    public String getPenColor() {
        return penColor;
    }

    public void setPenColor(String penColor) {
        this.penColor = penColor;
    }

    public String getBackgroundColor() {
        return backgroundColor;
    }

    public void setBackgroundColor(String backgroundColor) {
        this.backgroundColor = backgroundColor;
    }

    public boolean isReadOnly() {
        return readOnly;
    }

    public void setReadOnly(boolean readOnly) {
        this.readOnly = readOnly;
    }

    protected class Point implements Cloneable {

        private final double x;

        private final double y;

        private final long time;

        public Point(double x, double y, Long time) {
            this.x = x;
            this.y = y;
            this.time = time != null ? time : new Date().getTime();
        }

        public Point(double x, double y) {
            this(x, y, null);
        }

        public double velocityFrom(Point start) {
            return (getTime() != start.getTime()) ? distanceTo(start) / (getTime() - start.getTime()) : 1;
        }

        public double distanceTo(Point start) {
            return Math.sqrt(Math.pow(getX() - start.getX(), 2) + Math.pow(getY() - start.getY(), 2));
        }

        public double getX() {
            return x;
        }

        public double getY() {
            return y;
        }

        public long getTime() {
            return time;
        }

    }

    protected class Beizer {

        private final Point startPoint;

        private final Point control1;

        private final Point control2;

        private final Point endPoint;

        public Beizer(Point startPoint, Point control1, Point control2, Point endPoint) {
            this.startPoint = startPoint;
            this.control1 = control1;
            this.control2 = control2;
            this.endPoint = endPoint;
        }

        public double getLength() {
            double steps = 10;
            double length = 0;
            double t;
            double cx;
            double cy;
            double px = 0d;
            double py = 0d;
            double xdiff;
            double ydiff;
            for (int i = 0; i <= steps; i++) {
                t = i / steps;
                cx = getPoint(t, startPoint.getX(), control1.getX(), control2.getX(), endPoint.getX());
                cy = getPoint(t, startPoint.getY(), control1.getY(), control2.getY(), endPoint.getY());
                if (i > 0) {
                    xdiff = cx - px;
                    ydiff = cy - py;
                    length += Math.sqrt(xdiff * xdiff + ydiff * ydiff);
                }
                px = cx;
                py = cy;
            }
            return length;
        }

        private double getPoint(double t, double start, double c1, double c2, double end) {
            return start * (1.0 - t) * (1.0 - t) * (1.0 - t) + 3.0 * c1 * (1.0 - t) * (1.0 - t) * t
                    + 3.0 * c2 * (1.0 - t) * t * t + end * t * t * t;
        }

        public Point getStartPoint() {
            return startPoint;
        }

        public Point getControl1() {
            return control1;
        }

        public Point getControl2() {
            return control2;
        }

        public Point getEndPoint() {
            return endPoint;
        }

    }

    protected class CurveControlPoints {

        private final Point point1;

        private final Point point2;

        public CurveControlPoints(Point point1, Point point2) {
            this.point1 = point1;
            this.point2 = point2;
        }

        public Point getPoint1() {
            return point1;
        }

        public Point getPoint2() {
            return point2;
        }

    }

    protected class Coordinates {

        private final double x;

        private final double y;

        public Coordinates(double x, double y) {
            this.x = x;
            this.y = y;
        }

        public double getX() {
            return x;
        }

        public double getY() {
            return y;
        }

    }

    private class EventWrapper {

        private final double clientX;

        private final double clientY;

        public EventWrapper(double clientX, double clientY) {
            this.clientX = clientX;
            this.clientY = clientY;
        }

        public EventWrapper(Touch touch) {
            this(touch.getClientX(), touch.getClientY());
        }

        public EventWrapper(NativeEvent event) {
            this(event.getClientX(), event.getClientY());
        }

        public double getClientX() {
            return clientX;
        }

        public double getClientY() {
            return clientY;
        }

    }

}