/*
* Copyright (C) 2010-2016 JPEXS, All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3.0 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library.
*/
package com.jpexs.decompiler.flash.exporters.morphshape;
import com.jpexs.decompiler.flash.SWF;
import com.jpexs.decompiler.flash.exporters.commonshape.Matrix;
import com.jpexs.decompiler.flash.exporters.commonshape.SVGExporter;
import com.jpexs.decompiler.flash.tags.base.ImageTag;
import com.jpexs.decompiler.flash.tags.enums.ImageFormat;
import com.jpexs.decompiler.flash.types.ColorTransform;
import com.jpexs.decompiler.flash.types.FILLSTYLE;
import com.jpexs.decompiler.flash.types.GRADIENT;
import com.jpexs.decompiler.flash.types.GRADRECORD;
import com.jpexs.decompiler.flash.types.LINESTYLE2;
import com.jpexs.decompiler.flash.types.RGB;
import com.jpexs.decompiler.flash.types.RGBA;
import com.jpexs.decompiler.flash.types.SHAPE;
import com.jpexs.helpers.Helper;
import com.jpexs.helpers.SerializableImage;
import java.awt.Color;
import org.w3c.dom.Element;
/**
*
* @author JPEXS, Claus Wahlers
*/
public class SVGMorphShapeExporter extends DefaultSVGMorphShapeExporter {
protected Element path;
protected int lastPatternId;
private final Color defaultColor;
private final SWF swf;
private final SVGExporter exporter;
public SVGMorphShapeExporter(SWF swf, SHAPE shape, SHAPE endShape, SVGExporter exporter, Color defaultColor, ColorTransform colorTransform, double zoom) {
super(shape, endShape, colorTransform, zoom);
this.swf = swf;
this.defaultColor = defaultColor;
this.exporter = exporter;
}
@Override
public void beginFill(RGB color, RGB colorEnd) {
if (color == null) {
color = new RGB(defaultColor == null ? Color.black : defaultColor);
}
if (colorEnd == null) {
colorEnd = new RGB(defaultColor == null ? Color.black : defaultColor);
}
finalizePath();
path.setAttribute("stroke", "none");
path.setAttribute("fill", color.toHexRGB());
path.setAttribute("fill-rule", "evenodd");
path.appendChild(createAnimateElement("fill", color.toHexRGB(), colorEnd.toHexRGB()));
if (color instanceof RGBA) {
RGBA colorA = (RGBA) color;
if (colorA.alpha != 255) {
path.setAttribute("fill-opacity", Float.toString(colorA.getAlphaFloat()));
}
RGBA colorAEnd = (RGBA) colorEnd;
path.appendChild(createAnimateElement("fill-opacity", colorA.getAlphaFloat(), colorAEnd.getAlphaFloat()));
}
}
@Override
public void beginGradientFill(int type, GRADRECORD[] gradientRecords, GRADRECORD[] gradientRecordsEnd, Matrix matrix, Matrix matrixEnd, int spreadMethod, int interpolationMethod, float focalPointRatio, float focalPointRatioEnd) {
finalizePath();
Element gradient = (type == FILLSTYLE.LINEAR_GRADIENT)
? exporter.createElement("linearGradient")
: exporter.createElement("radialGradient");
populateGradientElement(gradient, type, gradientRecords, gradientRecordsEnd, matrix, matrixEnd, spreadMethod, interpolationMethod, focalPointRatio);
int id = exporter.gradients.indexOf(gradient);
if (id < 0) {
// todo: filter same gradients
id = exporter.gradients.size();
exporter.gradients.add(gradient);
}
gradient.setAttribute("id", "gradient" + id);
path.setAttribute("stroke", "none");
path.setAttribute("fill", "url(#gradient" + id + ")");
path.setAttribute("fill-rule", "evenodd");
exporter.addToDefs(gradient);
}
@Override
public void beginBitmapFill(int bitmapId, Matrix matrix, Matrix matrixEnd, boolean repeat, boolean smooth, ColorTransform colorTransform) {
finalizePath();
ImageTag image = swf.getImage(bitmapId);
if (image != null) {
SerializableImage img = image.getImageCached();
if (img != null) {
if (colorTransform != null) {
colorTransform.apply(img);
}
int width = img.getWidth();
int height = img.getHeight();
lastPatternId++;
String patternId = "PatternID_" + lastPatternId;
ImageFormat format = image.getImageFormat();
byte[] imageData = Helper.readStream(image.getImageData());
String base64ImgData = Helper.byteArrayToBase64String(imageData);
path.setAttribute("style", "fill:url(#" + patternId + ")");
Element pattern = exporter.createElement("pattern");
pattern.setAttribute("id", patternId);
pattern.setAttribute("patternUnits", "userSpaceOnUse");
pattern.setAttribute("overflow", "visible");
pattern.setAttribute("width", "" + width);
pattern.setAttribute("height", "" + height);
pattern.setAttribute("viewBox", "0 0 " + width + " " + height);
if (matrix != null) {
matrix = matrix.clone();
matrix.rotateSkew0 *= zoom / SWF.unitDivisor;
matrix.rotateSkew1 *= zoom / SWF.unitDivisor;
matrix.scaleX *= zoom / SWF.unitDivisor;
matrix.scaleY *= zoom / SWF.unitDivisor;
matrixEnd = matrixEnd.clone();
matrixEnd.rotateSkew0 *= zoom / SWF.unitDivisor;
matrixEnd.rotateSkew1 *= zoom / SWF.unitDivisor;
matrixEnd.scaleX *= zoom / SWF.unitDivisor;
matrixEnd.scaleY *= zoom / SWF.unitDivisor;
addMatrixAnimation(pattern, "patternTransform", matrix, matrixEnd);
}
Element imageElement = exporter.createElement("image");
imageElement.setAttribute("width", "" + width);
imageElement.setAttribute("height", "" + height);
imageElement.setAttribute("xlink:href", "data:image/" + format + ";base64," + base64ImgData);
pattern.appendChild(imageElement);
exporter.addToGroup(pattern);
}
}
}
@Override
public void lineStyle(double thickness, double thicknessEnd, RGB color, RGB colorEnd, boolean pixelHinting, String scaleMode, int startCaps, int endCaps, int joints, float miterLimit) {
finalizePath();
thickness *= zoom / SWF.unitDivisor;
thicknessEnd *= zoom / SWF.unitDivisor;
path.setAttribute("fill", "none");
if (color != null) {
path.setAttribute("stroke", color.toHexRGB());
path.appendChild(createAnimateElement("stroke", color.toHexRGB(), colorEnd.toHexRGB()));
}
path.setAttribute("stroke-width", Double.toString(thickness == 0 ? 1 : thickness));
path.appendChild(createAnimateElement("stroke-width", thickness, thicknessEnd));
if (color instanceof RGBA) {
RGBA colorA = (RGBA) color;
if (colorA.alpha != 255) {
path.setAttribute("stroke-opacity", Float.toString(colorA.getAlphaFloat()));
}
RGBA colorAEnd = (RGBA) colorEnd;
path.appendChild(createAnimateElement("fill-opacity", colorA.getAlphaFloat(), colorAEnd.getAlphaFloat()));
}
switch (startCaps) {
case LINESTYLE2.NO_CAP:
path.setAttribute("stroke-linecap", "butt");
break;
case LINESTYLE2.SQUARE_CAP:
path.setAttribute("stroke-linecap", "square");
break;
default:
path.setAttribute("stroke-linecap", "round");
break;
}
switch (joints) {
case LINESTYLE2.BEVEL_JOIN:
path.setAttribute("stroke-linejoin", "bevel");
break;
case LINESTYLE2.ROUND_JOIN:
path.setAttribute("stroke-linejoin", "round");
break;
default:
path.setAttribute("stroke-linejoin", "miter");
if (miterLimit >= 1 && miterLimit != 4f) {
path.setAttribute("stroke-miterlimit", Double.toString(miterLimit));
}
break;
}
}
@Override
public void lineGradientStyle(int type, GRADRECORD[] gradientRecords, GRADRECORD[] gradientRecordsEnd, Matrix matrix, Matrix matrixEnd, int spreadMethod, int interpolationMethod, float focalPointRatio, float focalPointRatioEnd) {
path.removeAttribute("stroke-opacity");
Element gradient = (type == FILLSTYLE.LINEAR_GRADIENT)
? exporter.createElement("linearGradient")
: exporter.createElement("radialGradient");
populateGradientElement(gradient, type, gradientRecords, gradientRecordsEnd, matrix, matrixEnd, spreadMethod, interpolationMethod, focalPointRatio);
int id = exporter.gradients.indexOf(gradient);
if (id < 0) {
// todo: filter same gradients
id = exporter.gradients.size();
exporter.gradients.add(gradient);
}
gradient.setAttribute("id", "gradient" + id);
path.setAttribute("stroke", "url(#gradient" + id + ")");
path.setAttribute("fill", "none");
exporter.addToDefs(gradient);
}
private Element createAnimateElement(String attributeName, Object startValue, Object endValue) {
Element animate = exporter.createElement("animate");
animate.setAttribute("dur", "2s"); // todo
animate.setAttribute("repeatCount", "indefinite");
animate.setAttribute("attributeName", attributeName);
animate.setAttribute("values", startValue + ";" + endValue);
return animate;
}
@Override
protected void finalizePath() {
if (path != null && pathData != null && pathData.length() > 0) {
path.setAttribute("d", pathData.toString().trim());
path.appendChild(createAnimateElement("d", pathData.toString().trim(), pathDataEnd.toString().trim()));
exporter.addToGroup(path);
}
path = exporter.createElement("path");
super.finalizePath();
}
private void addMatrixAnimation(Element element, String attribute, Matrix matrix, Matrix matrixEnd) {
final int animationLength = 2; // todo
final String animationLengthStr = animationLength + "s";
element.setAttribute(attribute, matrix.getSvgTransformationString(SWF.unitDivisor / zoom, 1));
// QR decomposition
double translateX = roundPixels400(matrix.translateX * zoom / SWF.unitDivisor);
double translateY = roundPixels400(matrix.translateY * zoom / SWF.unitDivisor);
double a = matrix.scaleX;
double b = matrix.rotateSkew0;
double c = matrix.rotateSkew1;
double d = matrix.scaleY;
double r = Math.sqrt(a * a + b * b);
double det = a * d - b * c;
double rotate = roundPixels400(Math.signum(b) * Math.acos(a / r) * 180 / Math.PI);
double scaleX = roundPixels400(r);
double scaleY = roundPixels400(det / r);
double skewX = roundPixels400(Math.atan((a * c + b * d) / (r * r)) * 180 / Math.PI);
double translateXEnd = roundPixels400(matrixEnd.translateX * zoom / SWF.unitDivisor);
double translateYEnd = roundPixels400(matrixEnd.translateY * zoom / SWF.unitDivisor);
a = matrixEnd.scaleX;
b = matrixEnd.rotateSkew0;
c = matrixEnd.rotateSkew1;
d = matrixEnd.scaleY;
double rEnd = Math.sqrt(a * a + b * b);
double detEnd = a * d - b * c;
double rotateEnd = roundPixels400(Math.signum(b) * Math.acos(a / rEnd) * 180 / Math.PI);
double scaleXEnd = roundPixels400(rEnd);
double scaleYEnd = roundPixels400(detEnd / rEnd);
double skewXEnd = roundPixels400(Math.atan((a * c + b * d) / (rEnd * rEnd)) * 180 / Math.PI);
Element animateClear = exporter.createElement("animateTransform");
animateClear.setAttribute("dur", animationLengthStr);
animateClear.setAttribute("repeatCount", "indefinite");
animateClear.setAttribute("attributeName", attribute);
animateClear.setAttribute("type", "scale");
animateClear.setAttribute("additive", "replace");
animateClear.setAttribute("from", "1");
animateClear.setAttribute("to", "1");
Element animateTranslate = exporter.createElement("animateTransform");
animateTranslate.setAttribute("dur", animationLengthStr);
animateTranslate.setAttribute("repeatCount", "indefinite");
animateTranslate.setAttribute("attributeName", attribute);
animateTranslate.setAttribute("type", "translate");
animateTranslate.setAttribute("additive", "sum");
animateTranslate.setAttribute("from", translateX + " " + translateY);
animateTranslate.setAttribute("to", translateXEnd + " " + translateYEnd);
Element animateRotate = exporter.createElement("animateTransform");
animateRotate.setAttribute("dur", animationLengthStr);
animateRotate.setAttribute("repeatCount", "indefinite");
animateRotate.setAttribute("attributeName", attribute);
animateRotate.setAttribute("type", "rotate");
animateRotate.setAttribute("additive", "sum");
animateRotate.setAttribute("from", Double.toString(rotate));
animateRotate.setAttribute("to", Double.toString(rotateEnd));
Element animateScale = exporter.createElement("animateTransform");
animateScale.setAttribute("dur", animationLengthStr);
animateScale.setAttribute("repeatCount", "indefinite");
animateScale.setAttribute("attributeName", attribute);
animateScale.setAttribute("type", "scale");
animateScale.setAttribute("additive", "sum");
animateScale.setAttribute("from", scaleX + " " + scaleY);
animateScale.setAttribute("to", scaleXEnd + " " + scaleYEnd);
Element animateSkewX = exporter.createElement("animateTransform");
animateSkewX.setAttribute("dur", animationLengthStr);
animateSkewX.setAttribute("repeatCount", "indefinite");
animateSkewX.setAttribute("attributeName", attribute);
animateSkewX.setAttribute("type", "skewX");
animateSkewX.setAttribute("additive", "sum");
animateSkewX.setAttribute("from", Double.toString(skewX));
animateSkewX.setAttribute("to", Double.toString(skewXEnd));
element.appendChild(animateClear);
element.appendChild(animateTranslate);
element.appendChild(animateRotate);
element.appendChild(animateScale);
element.appendChild(animateSkewX);
/*
// LDU decomposition
double translateX = roundPixels400(matrix.translateX * zoom / SWF.unitDivisor);
double translateY = roundPixels400(matrix.translateY * zoom / SWF.unitDivisor);
double a = matrix.scaleX;
double b = matrix.rotateSkew0;
double c = matrix.rotateSkew1;
double d = matrix.scaleY;
double det = a * d - b * c;
double skewY = roundPixels400(Math.atan2(b, a) * 180 / Math.PI);
double scaleX = roundPixels400(a);
double scaleY = roundPixels400(det / a);
double skewX = roundPixels400(Math.atan2(c, a) * 180 / Math.PI);
double translateXEnd = roundPixels400(matrixEnd.translateX * zoom / SWF.unitDivisor);
double translateYEnd = roundPixels400(matrixEnd.translateY * zoom / SWF.unitDivisor);
a = matrixEnd.scaleX;
b = matrixEnd.rotateSkew0;
c = matrixEnd.rotateSkew1;
d = matrixEnd.scaleY;
double detEnd = a * d - b * c;
double skewYEnd = roundPixels400(Math.atan2(b, a) * 180 / Math.PI);
double scaleXEnd = roundPixels400(a);
double scaleYEnd = roundPixels400(detEnd / a);
double skewXEnd = roundPixels400(Math.atan2(c, a) * 180 / Math.PI);
Element animateClear = exporter.createElement("animateTransform");
animateClear.setAttribute("dur", animationLengthStr);
animateClear.setAttribute("repeatCount", "indefinite");
animateClear.setAttribute("attributeName", attribute);
animateClear.setAttribute("type", "scale");
animateClear.setAttribute("additive", "replace");
animateClear.setAttribute("from", "1");
animateClear.setAttribute("to", "1");
Element animateTranslate = exporter.createElement("animateTransform");
animateTranslate.setAttribute("dur", animationLengthStr);
animateTranslate.setAttribute("repeatCount", "indefinite");
animateTranslate.setAttribute("attributeName", attribute);
animateTranslate.setAttribute("type", "translate");
animateTranslate.setAttribute("additive", "sum");
animateTranslate.setAttribute("from", translateX + " " + translateY);
animateTranslate.setAttribute("to", translateXEnd + " " + translateYEnd);
Element animateSkewY = exporter.createElement("animateTransform");
animateSkewY.setAttribute("dur", animationLengthStr);
animateSkewY.setAttribute("repeatCount", "indefinite");
animateSkewY.setAttribute("attributeName", attribute);
animateSkewY.setAttribute("type", "skewY");
animateSkewY.setAttribute("additive", "sum");
animateSkewY.setAttribute("from", Double.toString(skewY));
animateSkewY.setAttribute("to", Double.toString(skewYEnd));
Element animateScale = exporter.createElement("animateTransform");
animateScale.setAttribute("dur", animationLengthStr);
animateScale.setAttribute("repeatCount", "indefinite");
animateScale.setAttribute("attributeName", attribute);
animateScale.setAttribute("type", "scale");
animateScale.setAttribute("additive", "sum");
animateScale.setAttribute("from", scaleX + " " + scaleY);
animateScale.setAttribute("to", scaleXEnd + " " + scaleYEnd);
Element animateSkewX = exporter.createElement("animateTransform");
animateSkewX.setAttribute("dur", animationLengthStr);
animateSkewX.setAttribute("repeatCount", "indefinite");
animateSkewX.setAttribute("attributeName", attribute);
animateSkewX.setAttribute("type", "skewX");
animateSkewX.setAttribute("additive", "sum");
animateSkewX.setAttribute("from", Double.toString(skewX));
animateSkewX.setAttribute("to", Double.toString(skewXEnd));
element.appendChild(animateClear);
element.appendChild(animateTranslate);
element.appendChild(animateSkewY);
element.appendChild(animateScale);
element.appendChild(animateSkewX);*/
}
protected void populateGradientElement(Element gradient, int type, GRADRECORD[] gradientRecords, GRADRECORD[] gradientRecordsEnd, Matrix matrix, Matrix matrixEnd, int spreadMethod, int interpolationMethod, float focalPointRatio) {
gradient.setAttribute("gradientUnits", "userSpaceOnUse");
if (type == FILLSTYLE.LINEAR_GRADIENT) {
gradient.setAttribute("x1", "-819.2");
gradient.setAttribute("x2", "819.2");
} else {
gradient.setAttribute("r", "819.2");
gradient.setAttribute("cx", "0");
gradient.setAttribute("cy", "0");
if (focalPointRatio != 0) {
gradient.setAttribute("fx", Double.toString(819.2 * focalPointRatio));
gradient.setAttribute("fy", "0");
}
}
switch (spreadMethod) {
case GRADIENT.SPREAD_PAD_MODE:
gradient.setAttribute("spreadMethod", "pad");
break;
case GRADIENT.SPREAD_REFLECT_MODE:
gradient.setAttribute("spreadMethod", "reflect");
break;
case GRADIENT.SPREAD_REPEAT_MODE:
gradient.setAttribute("spreadMethod", "repeat");
break;
}
if (interpolationMethod == GRADIENT.INTERPOLATION_LINEAR_RGB_MODE) {
gradient.setAttribute("color-interpolation", "linearRGB");
}
if (matrix != null) {
addMatrixAnimation(gradient, "gradientTransform", matrix, matrixEnd);
}
for (int i = 0; i < gradientRecords.length; i++) {
GRADRECORD record = gradientRecords[i];
GRADRECORD recordEnd = gradientRecordsEnd[i];
Element gradientEntry = exporter.createElement("stop");
gradientEntry.setAttribute("offset", Double.toString(record.ratio / 255.0));
gradientEntry.appendChild(createAnimateElement("offset", record.ratio / 255.0, recordEnd.ratio / 255.0));
RGB color = record.color;
RGB colorEnd = recordEnd.color;
//if(colors.get(i) != 0) {
gradientEntry.setAttribute("stop-color", color.toHexRGB());
gradientEntry.appendChild(createAnimateElement("stop-color", color.toHexRGB(), colorEnd.toHexRGB()));
//}
if (color instanceof RGBA) {
RGBA colorA = (RGBA) color;
if (colorA.alpha != 255) {
gradientEntry.setAttribute("stop-opacity", Float.toString(colorA.getAlphaFloat()));
}
RGBA colorAEnd = (RGBA) colorEnd;
gradientEntry.appendChild(createAnimateElement("stop-opacity", colorA.getAlphaFloat(), colorAEnd.getAlphaFloat()));
}
gradient.appendChild(gradientEntry);
}
}
protected double roundPixels400(double pixels) {
return Math.round(pixels * 10000) / 10000.0;
}
}