/*
* Copyright 2008 Google Inc.
*
* 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 com.google.gwt.widgetideas.graphics.client.impl;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.ImageElement;
import com.google.gwt.dom.client.Style.Overflow;
import com.google.gwt.dom.client.Style.Position;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.user.client.Element;
import com.google.gwt.widgetideas.graphics.client.CanvasGradient;
import com.google.gwt.widgetideas.graphics.client.GWTCanvas;
import com.google.gwt.widgetideas.graphics.client.JSOStack;
import java.util.ArrayList;
import java.util.Stack;
/**
* Deferred binding implementation of GWTCanvas for IE6. It is an implementation
* of canvas on top of VML.
*/
public class GWTCanvasImplIE6 implements GWTCanvasImpl {
public static final String BUTT = "flat";
public static final String DESTINATION_OVER = "afterBegin";
public static final String SOURCE_OVER = "beforeEnd";
// Used for sub pixel precision in pathing calculations.
protected static final double subPixlFactor = 10;
protected static final double subPixelFactorHalf = subPixlFactor / 2;
/**
* Takes in a double and returns a floored int. Leverages the fact that
* bitwise OR intifies the value.
*/
public static native int doubleToFlooredInt(double val) /*-{
return (val | 0);
}-*/;
private static native void ensureNamespace(Document doc, String prefix,
String urn) /*-{
if (!doc.namespaces[prefix]) {
doc.namespaces.add(prefix, urn, "#default#VML");
}
}-*/;
private static void ensureNamespacesAndStylesheet(Document doc) {
ensureNamespace(doc, "g_vml_", "urn:schemas-microsoft-com:vml");
ensureStyleSheet(doc);
}
private static native void ensureStyleSheet(Document doc) /*-{
if (!doc.styleSheets["gwt_canvas_"]) {
var ss = doc.createStyleSheet();
ss.owningElement.id = "gwt_canvas_";
ss.cssText = "canvas{display:inline-block;overflow:hidden;" +
// default size is 300x150 in Gecko and Opera
"text-align:left;width:300px;height:150px}" +
"g_vml_\\:*{behavior:url(#default#VML)}";
}
}-*/;
protected VMLContext context;
protected double[] matrix;
/**
* This will be used for an array join. Currently a bit faster than
* StringBuilder.append() & toString() because of the extra collections
* overhead.
*/
protected JSOStack<String> pathStr = JSOStack.create();
/**
* Stack uses preallocated arrays which makes push() slightly faster than
* [].push() since each push is simply an indexed setter.
*/
private Stack<VMLContext> contextStack = new Stack<VMLContext>();
private double currentX = 0;
private double currentY = 0;
private Element parentElement = null;
private int parentHeight = 0;
private int parentWidth = 0;
private Element shapeContainer = null;
public void arc(double x, double y, double radius, double startAngle,
double endAngle, boolean anticlockwise) {
pathStr.push(PathElement.arc(x, y, radius, startAngle, endAngle,
anticlockwise, this));
}
public void beginPath() {
pathStr.clear();
}
public void clear() {
pathStr.clear();
shapeContainer.setInnerHTML("");
}
public void clear(int width, int height) {
clear();
}
public void closePath() {
pathStr.push(PathElement.closePath());
}
public Element createElement() {
context = new VMLContext();
matrix = context.matrix;
return createParentElement();
}
public void cubicCurveTo(double cp1x, double cp1y, double cp2x, double cp2y,
double x, double y) {
pathStr.push(PathElement.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y, this));
currentX = x;
currentY = y;
}
public void drawImage(ImageElement img, double sourceX, double sourceY,
double sourceWidth, double sourceHeight, double destX, double destY,
double destWidth, double destHeight) {
double fullWidth = img.getWidth();
double fullHeight = img.getHeight();
JSOStack<String> vmlStr = JSOStack.getScratchArray();
vmlStr.push("<g_vml_:group style=\"position:absolute;width:10px;height:10px;");
double dX = getCoordX(matrix, destX, destY);
double dY = getCoordY(matrix, destX, destY);
// If we have a transformation matrix with rotation/scale, we
// apply a filter
if (context.matrix[0] != 1 || context.matrix[1] != 0) {
// We create a padding bounding box to prevent clipping.
vmlStr.push("padding-right:");
vmlStr.push(parentWidth + "px;");
vmlStr.push("padding-bottom:");
vmlStr.push(parentHeight + "px;");
vmlStr.push("filter:progid:DXImageTransform.Microsoft.Matrix(M11='");
vmlStr.push("" + matrix[0]);
vmlStr.push("',");
vmlStr.push("M12='");
vmlStr.push("" + matrix[1]);
vmlStr.push("',");
vmlStr.push("M21='");
vmlStr.push("" + matrix[3]);
vmlStr.push("',");
vmlStr.push("M22='");
vmlStr.push("" + matrix[4]);
vmlStr.push("',");
vmlStr.push("Dx='");
vmlStr.push("" + Math.floor(((dX / 10))));
vmlStr.push("',");
vmlStr.push("Dy='");
vmlStr.push("" + Math.floor(((dY / 10))));
vmlStr.push("', SizingMethod='clip');");
} else {
vmlStr.push("left:");
vmlStr.push((int) dX / 10 + "px;");
vmlStr.push("top:");
vmlStr.push((int) dY / 10 + "px");
}
vmlStr.push("\" coordsize=\"100 100\" coordorigin=\"0 0\"><g_vml_:image src=\"");
vmlStr.push(img.getSrc());
vmlStr.push("\" style=\"");
vmlStr.push("width:");
vmlStr.push(String.valueOf((int) (destWidth * 10)));
vmlStr.push("px;height:");
vmlStr.push(String.valueOf((int) (destHeight * 10)));
vmlStr.push("px;\" cropleft=\"");
vmlStr.push(String.valueOf(sourceX / fullWidth));
vmlStr.push("\" croptop=\"");
vmlStr.push(String.valueOf(sourceY / fullHeight));
vmlStr.push("\" cropright=\"");
vmlStr.push(String.valueOf((fullWidth - sourceX - sourceWidth) / fullWidth));
vmlStr.push("\" cropbottom=\"");
vmlStr.push(String.valueOf((fullHeight - sourceY - sourceHeight)
/ fullHeight));
vmlStr.push("\"/></g_vml_:group>");
insert("BeforeEnd", vmlStr.join());
}
public void fill() {
if (pathStr.isEmpty()) {
return;
}
JSOStack<String> shapeStr = JSOStack.getScratchArray();
shapeStr.push("<g_vml_:shape style=\"position:absolute;width:10px;height:10px;\" coordorigin=\"0 0\" coordsize=\"100 100\" fillcolor=\"");
shapeStr.push(context.fillStyle);
shapeStr.push("\" stroked=\"f\" path=\"");
shapeStr.push(pathStr.join());
shapeStr.push(" e\"><g_vml_:fill opacity=\"");
shapeStr.push("" + context.globalAlpha * context.fillAlpha);
// Unfinished Gradient implementation. Contributions welcome :).
if (context.fillGradient != null
&& context.fillGradient.colorStops.size() > 0) {
ArrayList<ColorStop> colorStops = context.fillGradient.colorStops;
shapeStr.push("\" color=\"");
shapeStr.push(colorStops.get(0).color.toString());
shapeStr.push("\" color2=\"");
shapeStr.push(colorStops.get(colorStops.size() - 1).color.toString());
shapeStr.push("\" type=\"");
shapeStr.push(context.fillGradient.type);
double minX = pathStr.getMinCoordX();
double maxX = pathStr.getMaxCoordX();
double minY = pathStr.getMinCoordY();
double maxY = pathStr.getMaxCoordY();
double dx = maxX - minX;
double dy = maxY - minY;
double fillLength = Math.sqrt((dx * dx) + (dy * dy));
double gradLength = context.fillGradient.length;
// Now add all the color stops
String colors = "";
for (int i = 1; i < colorStops.size() - 1; i++) {
ColorStop cs = colorStops.get(i);
double stopPosn = cs.offset * gradLength;
// /(Math.min(((stopPosn / fillLength) * 100), 100))
colors += 100 - (int) (((stopPosn / fillLength) * 100)) + "% "
+ cs.color.toString() + ",";
if (stopPosn > fillLength) {
break;
}
}
shapeStr.push("\" colors=\"");
shapeStr.push(colors);
shapeStr.push("\" angle=\"");
shapeStr.push(context.fillGradient.angle + "");
}
shapeStr.push("\"></g_vml_:fill></g_vml_:shape>");
String daStr = shapeStr.join();
insert(context.globalCompositeOperation, daStr);
}
public void fillRect(double x, double y, double w, double h) {
w += x;
h += y;
beginPath();
moveTo(x, y);
lineTo(x, h);
lineTo(w, h);
lineTo(w, y);
closePath();
fill();
pathStr.clear();
}
public VMLContext getContext() {
return context;
}
public int getCoordX(double[] matrix, double x, double y) {
int coordX = doubleToFlooredInt(Math.floor(10 * (matrix[0] * x + matrix[1]
* y + matrix[2]) - 4.5f));
// record current point to derive bounding box of current open path.
pathStr.logCoordX(coordX / 10);
return coordX;
}
public int getCoordY(double[] matrix, double x, double y) {
int coordY = doubleToFlooredInt(Math.floor(10 * (matrix[3] * x + matrix[4]
* y + matrix[5]) - 4.5f));
// record current point to derive bounding box of current open path.
pathStr.logCoordY(coordY / 10);
return coordY;
}
public String getFillStyle() {
return context.fillStyle;
}
public double getGlobalAlpha() {
return context.globalAlpha;
}
public String getGlobalCompositeOperation() {
if (context.globalCompositeOperation == GWTCanvasImplIE6.DESTINATION_OVER) {
return GWTCanvas.DESTINATION_OVER;
} else {
return GWTCanvas.SOURCE_OVER;
}
}
public String getLineCap() {
if (context.lineCap == GWTCanvasImplIE6.BUTT) {
return GWTCanvas.BUTT;
}
return context.lineCap;
}
public String getLineJoin() {
return context.lineJoin;
}
public double getLineWidth() {
return context.lineWidth;
}
public double getMiterLimit() {
return context.miterLimit;
}
public String getStrokeStyle() {
return context.strokeStyle;
}
public void lineTo(double x, double y) {
pathStr.push(PathElement.lineTo(x, y, this));
currentX = x;
currentY = y;
}
public void moveTo(double x, double y) {
pathStr.push(PathElement.moveTo(x, y, this));
currentX = x;
currentY = y;
}
public void quadraticCurveTo(double cpx, double cpy, double x, double y) {
double cp1x = (currentX + 2.0 / 3.0 * (cpx - currentX));
double cp1y = (currentY + 2.0 / 3.0 * (cpy - currentY));
double cp2x = (cp1x + (x - currentX) / 3.0);
double cp2y = (cp1y + (y - currentY) / 3.0);
pathStr.push(PathElement.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y, this));
currentX = x;
currentY = y;
}
public void rect(double x, double y, double w, double h) {
pathStr.push(PathElement.moveTo(x, y, this));
pathStr.push(PathElement.lineTo(x + w, y, this));
pathStr.push(PathElement.lineTo(x + w, y + h, this));
pathStr.push(PathElement.lineTo(x, y + h, this));
pathStr.push(PathElement.closePath());
currentX = x;
currentY = y + h;
}
public void restoreContext() {
if (!contextStack.isEmpty()) {
context = contextStack.pop();
matrix = context.matrix;
}
}
public void rotate(double angle) {
double s = Math.sin(-angle);
double c = Math.cos(-angle);
double a = matrix[0];
double b = matrix[1];
matrix[0] = a * c - (b * s);
matrix[1] = a * s + b * c;
a = matrix[3];
b = matrix[4];
matrix[3] = a * c - (b * s);
matrix[4] = a * s + b * c;
}
public void saveContext() {
contextStack.push(context);
context = new VMLContext(context);
matrix = context.matrix;
}
public void scale(double x, double y) {
context.arcScaleX *= x;
context.arcScaleY *= y;
// TODO(jaimeyap): We should try to figure out how to support scaling with
// different proportions in the X and Y axis. For now, supporting simple 1:1
// scaling is better than nothing.
context.lineScale *= x;
matrix[0] *= x;
matrix[1] *= y;
matrix[3] *= x;
matrix[4] *= y;
}
public void setBackgroundColor(Element element, String color) {
element.getStyle().setBackgroundColor(color);
}
public void setCoordHeight(Element elem, int height) {
elem.setAttribute("height", height + "");
clear(0, 0);
}
public void setCoordWidth(Element elem, int width) {
elem.setAttribute("width", width + "");
clear(0, 0);
}
public void setCurrentX(double currentX) {
this.currentX = currentX;
}
public void setCurrentY(double currentY) {
this.currentY = currentY;
}
public void setFillStyle(CanvasGradient gradient) {
context.fillGradient = (CanvasGradientImplIE6) gradient;
}
public void setFillStyle(String fillStyle) {
fillStyle = fillStyle.trim();
if (fillStyle.startsWith("rgba(")) {
int end = fillStyle.indexOf(")", 12);
if (end > -1) {
String[] guts = fillStyle.substring(5, end).split(",");
if (guts.length == 4) {
context.fillAlpha = Double.parseDouble(guts[3]);
context.fillStyle = "rgb(" + guts[0] + "," + guts[1] + "," + guts[2]
+ ")";
}
}
} else {
context.fillAlpha = 1;
context.fillStyle = fillStyle;
}
}
public void setGlobalAlpha(double globalAlpha) {
context.globalAlpha = globalAlpha;
}
public void setGlobalCompositeOperation(String gco) {
gco = gco.trim();
if (gco.equalsIgnoreCase(GWTCanvas.SOURCE_OVER)) {
context.globalCompositeOperation = GWTCanvasImplIE6.SOURCE_OVER;
} else if (gco.equalsIgnoreCase(GWTCanvas.DESTINATION_OVER)) {
context.globalCompositeOperation = GWTCanvasImplIE6.DESTINATION_OVER;
}
}
public void setLineCap(String lineCap) {
if (lineCap.trim().equalsIgnoreCase(GWTCanvas.BUTT)) {
context.lineCap = GWTCanvasImplIE6.BUTT;
} else {
context.lineCap = lineCap;
}
}
public void setLineJoin(String lineJoin) {
context.lineJoin = lineJoin;
}
public void setLineWidth(double lineWidth) {
context.lineWidth = lineWidth;
}
public void setMiterLimit(double miterLimit) {
context.miterLimit = miterLimit;
}
public void setPixelHeight(Element elem, int height) {
elem.getStyle().setHeight(height, Unit.PX);
parentHeight = height;
}
public void setPixelWidth(Element elem, int width) {
elem.getStyle().setWidth(width, Unit.PX);
parentWidth = width;
}
public void setStrokeStyle(CanvasGradient gradient) {
context.strokeGradient = (CanvasGradientImplIE6) gradient;
}
public void setStrokeStyle(String strokeStyle) {
strokeStyle = strokeStyle.trim();
if (strokeStyle.startsWith("rgba(")) {
int end = strokeStyle.indexOf(")", 12);
if (end > -1) {
String[] guts = strokeStyle.substring(5, end).split(",");
if (guts.length == 4) {
context.strokeAlpha = Double.parseDouble(guts[3]);
context.strokeStyle = "rgb(" + guts[0] + "," + guts[1] + ","
+ guts[2] + ")";
}
}
} else {
context.strokeAlpha = 1;
context.strokeStyle = strokeStyle;
}
}
public void stroke() {
if (pathStr.isEmpty()) {
return;
}
JSOStack<String> shapeStr = JSOStack.getScratchArray();
shapeStr.push("<g_vml_:shape style=\"position:absolute;width:10px;height:10px;\" coordorigin=\"0 0\" coordsize=\"100 100\" filled=\"f\" strokecolor=\"");
shapeStr.push(context.strokeStyle);
shapeStr.push("\" strokeweight=\"");
shapeStr.push("" + context.lineWidth * context.lineScale);
shapeStr.push("px\" path=\"");
shapeStr.push(pathStr.join());
shapeStr.push(" e\"><g_vml_:stroke opacity=\"");
shapeStr.push("" + context.globalAlpha * context.strokeAlpha);
shapeStr.push("\" miterlimit=\"");
shapeStr.push("" + context.miterLimit);
shapeStr.push("\" joinstyle=\"");
shapeStr.push(context.lineJoin);
shapeStr.push("\" endcap=\"");
shapeStr.push(context.lineCap);
shapeStr.push("\"></g_vml_:stroke></g_vml_:shape>");
insert(context.globalCompositeOperation, shapeStr.join());
}
public void strokeRect(double x, double y, double w, double h) {
w += x;
h += y;
beginPath();
moveTo(x, y);
lineTo(x, h);
lineTo(w, h);
lineTo(w, y);
closePath();
stroke();
pathStr.clear();
}
public void transform(double m11, double m12, double m21, double m22,
double dx, double dy) {
double a = matrix[0];
double b = matrix[1];
matrix[0] = a * m11 + b * m21;
matrix[1] = a * m12 + b * m22;
matrix[2] += a * dx + b * dy;
a = matrix[3];
b = matrix[4];
matrix[3] = a * m11 + b * m21;
matrix[4] = a * m12 + b * m22;
matrix[5] += a * dx + b * dy;
}
public void translate(double x, double y) {
matrix[2] += matrix[0] * x + matrix[1] * y;
matrix[5] += matrix[3] * x + matrix[4] * y;
}
private Element createParentElement() {
// TODO(jaimeyap): We should probably refactor the Widget's constructor to
// take in the Document as a dependency.
Document doc = Document.get();
parentElement = doc.createElement("canvas").cast();
shapeContainer = doc.createDivElement().cast();
shapeContainer.getStyle().setPosition(Position.ABSOLUTE);
shapeContainer.getStyle().setOverflow(Overflow.HIDDEN);
shapeContainer.getStyle().setWidth(100, Unit.PCT);
shapeContainer.getStyle().setHeight(100, Unit.PCT);
parentElement.appendChild(shapeContainer);
ensureNamespacesAndStylesheet(doc);
return parentElement;
}
private native void insert(String gco, String html) /*-{
this.@com.google.gwt.widgetideas.graphics.client.impl.GWTCanvasImplIE6::shapeContainer.insertAdjacentHTML(gco, html);
}-*/;
}