Skip to content

Commit

Permalink
fixes #252
Browse files Browse the repository at this point in the history
  • Loading branch information
alexjlockwood committed May 6, 2018
1 parent 55e2ea2 commit 60f0104
Showing 1 changed file with 76 additions and 70 deletions.
146 changes: 76 additions & 70 deletions src/app/scripts/export/SvgSerializer.ts
Expand Up @@ -3,6 +3,7 @@ import {
GroupLayer,
Layer,
LayerUtil,
MorphableLayer,
PathLayer,
VectorLayer,
} from 'app/model/layers';
Expand All @@ -18,11 +19,7 @@ const SVG_NS = 'http://www.w3.org/2000/svg';
/**
* Serializes an VectorLayer to a SVG string.
*/
export function toSvgString(
vl: VectorLayer,
width?: number,
height?: number,
) {
export function toSvgString(vl: VectorLayer, width?: number, height?: number) {
const xmlDoc = document.implementation.createDocument(undefined, 'svg', undefined);
const rootNode = xmlDoc.documentElement;
rootNode.setAttributeNS(XMLNS_NS, 'xmlns', SVG_NS);
Expand Down Expand Up @@ -61,75 +58,82 @@ function vectorLayerToSvgNode(
withIds = true,
frameNumber = '',
) {
// TODO: would be better to have clip-paths reference other clip paths in order to reduce file size

// Create a map where the keys are all of the clip paths in the tree, and
// their corresponding values are any affected clipped siblings.
const clipPathToClippedSiblingsMap = new Map<string, Set<string>>();
(function recurseFn(layers: ReadonlyArray<Layer>) {
for (let i = 0; i < layers.length; i++) {
const layer = layers[i];
if (layer instanceof ClipPathLayer) {
const clippedSiblings = layers
.slice(i + 1)
.filter(l => !(l instanceof ClipPathLayer))
.map(l => l.id);
if (clippedSiblings.length) {
clipPathToClippedSiblingsMap.set(layer.id, new Set(clippedSiblings));
}
}
// Create a map where the keys are ClipPathLayer IDs and the values
// are their associated path data strings.
const clipPathToPathDataMap = new Map<string, string>();
// Create a map where the keys are non-ClipPathLayer IDs and the values are
// the in-order list of ClipPathLayers that are clipping the layer (nearest
// ClipPathLayer appears in the list last).
const clippedLayerToSeenClipPathsMap = new Map<string, ReadonlyArray<string>>();
(function recurseFn(layer: Layer) {
interface Entry {
readonly layer: Layer;
readonly seenClipPaths: ReadonlyArray<ClipPathLayer>;
}
layers.forEach(l => recurseFn(l.children));
})(vl.children);

// Create a map where the keys are clipped siblings, and their values
// are the list of clip paths that clipped them.
const clippedSiblingToClipPathsMap = new Map<string, Set<string>>();
clipPathToClippedSiblingsMap.forEach((clippedSiblingIds, clipPathId) => {
clippedSiblingIds.forEach(clippedSiblingId => {
const clipPathIds = clippedSiblingToClipPathsMap.has(clippedSiblingId)
? clippedSiblingToClipPathsMap.get(clippedSiblingId)
: new Set<string>();
clipPathIds.add(clipPathId);
clippedSiblingToClipPathsMap.set(clippedSiblingId, clipPathIds);
});
});
layer.children
.reduce(
(acc: ReadonlyArray<Entry>, curr) => {
const seenClipPaths = acc.length ? [..._.last(acc).seenClipPaths] : [];
// Ignore clip paths with empty path data strings.
if (curr instanceof ClipPathLayer && !!curr.pathData.getPathString()) {
clipPathToPathDataMap.set(curr.id, curr.pathData.getPathString());
seenClipPaths.push(curr);
}
return [...acc, { layer: curr, seenClipPaths }];
},
[] as ReadonlyArray<Entry>,
)
.filter(({ layer: l, seenClipPaths }) => {
// Keep the entry if the key isn't a ClipPathLayer and its
// associated list of seen clip paths isn't empty.
return !(l instanceof ClipPathLayer) && seenClipPaths.length > 0;
})
.map(({ layer: l, seenClipPaths }) => {
return { layerId: l.id, seenClipPaths: seenClipPaths.map(({ id }) => id) };
})
.forEach(({ layerId, seenClipPaths }) => {
clippedLayerToSeenClipPathsMap.set(layerId, seenClipPaths);
});
layer.children.forEach(recurseFn);
})(vl);

// Create a map of sibling IDs to the clip path name they should use when
// referencing the clip-path.
const clippedSiblingToClipPathReferencedIdMap = new Map<string, string>();
clippedSiblingToClipPathsMap.forEach((clipPaths, siblingId) => {
const siblingName = vl.findLayerById(siblingId).name;
clippedSiblingToClipPathReferencedIdMap.set(siblingId, `clip_${siblingName}${frameNumber}`);
// Create a map where the keys are non-ClipPathLayer IDs and the values are the
// clip path names they should use when referencing the clip-path.
const clippedLayerToClipPathNameMap = new Map<string, string>();
clippedLayerToSeenClipPathsMap.forEach((seenClipPaths, layerId) => {
const frameInfo = frameNumber ? `_frame${frameNumber}` : '';
const layerInfo = `_${vl.findLayerById(layerId).name}`;
const clipPathName = `clip${frameInfo}${layerInfo}`;
clippedLayerToClipPathNameMap.set(layerId, clipPathName);
});

if (clippedSiblingToClipPathsMap.size) {
const shouldCreateDefs = clippedLayerToSeenClipPathsMap.size > 0;
if (shouldCreateDefs) {
const defsNode = xmlDoc.createElement('defs');
clippedSiblingToClipPathsMap.forEach((clipPathIds, clippedSiblingId) => {
const clipPathNode = xmlDoc.createElement('clipPath');
const clipPathName = clippedSiblingToClipPathReferencedIdMap.get(clippedSiblingId);
conditionalAttr(clipPathNode, 'id', clipPathName);
clipPathIds.forEach(clipPathId => {
const clipPathData = (vl.findLayerById(clipPathId) as ClipPathLayer).pathData;
if (clipPathData && clipPathData.getPathString()) {
const pathNode = xmlDoc.createElement('path');
conditionalAttr(pathNode, 'd', clipPathData.getPathString());
clipPathNode.appendChild(pathNode);
clippedLayerToSeenClipPathsMap.forEach((seenClipPaths, layerId) => {
const clipPathName = clippedLayerToClipPathNameMap.get(layerId);
seenClipPaths.forEach((id, i) => {
const clipPathNode = xmlDoc.createElement('clipPath');
conditionalAttr(clipPathNode, 'id', clipPathName + (i ? '_' + i : ''));
const pathNode = xmlDoc.createElement('path');
conditionalAttr(pathNode, 'd', clipPathToPathDataMap.get(id));
if (i + 1 < seenClipPaths.length) {
// Build the intersection of all seen clip paths.
const nextClipPathName = clipPathName + '_' + (i + 1);
conditionalAttr(pathNode, 'clip-path', `url(#${nextClipPathName})`);
}
clipPathNode.appendChild(pathNode);
defsNode.appendChild(clipPathNode);
});
defsNode.appendChild(clipPathNode);
});
destinationNode.appendChild(defsNode);
}

const shouldSetClipPathForLayerFn = (layer: Layer) => clippedSiblingToClipPathsMap.has(layer.id);

const maybeSetClipPathForLayerFn = (layer: Layer, layerNode: HTMLElement) => {
if (!shouldSetClipPathForLayerFn(layer)) {
return;
const isLayerBeingClippedFn = (layerId: string) => clippedLayerToClipPathNameMap.has(layerId);
const maybeSetClipPathForLayerFn = (node: HTMLElement, layerId: string) => {
if (isLayerBeingClippedFn(layerId)) {
conditionalAttr(node, 'clip-path', `url(#${clippedLayerToClipPathNameMap.get(layerId)})`);
}
const clipPathAttrValue = `url(#${clippedSiblingToClipPathReferencedIdMap.get(layer.id)})`;
conditionalAttr(layerNode, 'clip-path', clipPathAttrValue);
};

walk(
Expand All @@ -143,13 +147,16 @@ function vectorLayerToSvgNode(
return parentNode;
}
if (layer instanceof PathLayer) {
const { pathData } = layer;
if (!pathData.getPathString()) {
return undefined;
}
const node = xmlDoc.createElement('path');
if (withIds) {
conditionalAttr(node, 'id', layer.name);
}
maybeSetClipPathForLayerFn(layer, node);
const path = layer.pathData;
conditionalAttr(node, 'd', path ? path.getPathString() : '');
maybeSetClipPathForLayerFn(node, layer.id);
conditionalAttr(node, 'd', pathData.getPathString());
if (layer.fillColor) {
conditionalAttr(node, 'fill', ColorUtil.androidToCssHexColor(layer.fillColor), '');
} else {
Expand All @@ -170,13 +177,13 @@ function vectorLayerToSvgNode(
let pathLength: number;
if (Math.abs(a) !== 1 || Math.abs(d) !== 1) {
// Then recompute the scaled path length.
pathLength = layer.pathData
pathLength = pathData
.mutate()
.transform(flattenedTransform)
.build()
.getSubPathLength(0);
} else {
pathLength = layer.pathData.getSubPathLength(0);
pathLength = pathData.getSubPathLength(0);
}
const strokeDashArray = PathUtil.toStrokeDashArray(
layer.trimPathStart,
Expand All @@ -203,7 +210,6 @@ function vectorLayerToSvgNode(
return parentNode;
}
if (layer instanceof GroupLayer) {
// TODO: create one node per group property being animated
const node = xmlDoc.createElement('g');
if (withIds) {
conditionalAttr(node, 'id', layer.name);
Expand All @@ -227,14 +233,14 @@ function vectorLayerToSvgNode(
let nodeToAttachToParent = node;
if (transformValues.length) {
node.setAttributeNS(undefined, 'transform', transformValues.join(' '));
if (shouldSetClipPathForLayerFn(layer)) {
if (isLayerBeingClippedFn(layer.id)) {
// Create a wrapper node so that the clip-path is applied before the transformations.
const wrapperNode = xmlDoc.createElement('g');
wrapperNode.appendChild(node);
nodeToAttachToParent = wrapperNode;
}
}
maybeSetClipPathForLayerFn(layer, nodeToAttachToParent);
maybeSetClipPathForLayerFn(nodeToAttachToParent, layer.id);
parentNode.appendChild(nodeToAttachToParent);
return node;
}
Expand Down

0 comments on commit 60f0104

Please sign in to comment.