Skip to content

Instantly share code, notes, and snippets.

@ivanpopelyshev
Last active March 31, 2019 11:41
Show Gist options
  • Save ivanpopelyshev/eb5709ce4c2718204c3c12a5591ca0b8 to your computer and use it in GitHub Desktop.
Save ivanpopelyshev/eb5709ce4c2718204c3c12a5591ca0b8 to your computer and use it in GitHub Desktop.
Faster SegmentedPath for Shumway
import PathCommand = Shumway.PathCommand;
import GradientType = Shumway.GradientType;
import GradientSpreadMethod = Shumway.GradientSpreadMethod;
import GradientInterpolationMethod = Shumway.GradientInterpolationMethod;
import Bounds = Shumway.Bounds;
import DataBuffer = Shumway.ArrayUtilities.DataBuffer;
import ShapeData = Shumway.ShapeData;
import ShapeMatrix = Shumway.ShapeMatrix;
import clamp = Shumway.NumberUtilities.clamp;
import assert = Shumway.Debug.assert;
import assertUnreachable = Shumway.Debug.assertUnreachable;
let push = Array.prototype.push;
enum FillType {
Solid = 0,
LinearGradient = 0x10,
RadialGradient = 0x12,
FocalRadialGradient = 0x13,
RepeatingBitmap = 0x40,
ClippedBitmap = 0x41,
NonsmoothedRepeatingBitmap = 0x42,
NonsmoothedClippedBitmap = 0x43,
}
/*
* Applies the current segment to the paths of all styles specified in the last
* style-change record.
*
* For fill0, we have to apply commands and their data in reverse order, to turn
* left fills into right ones.
*
* If we have more than one style, we only recorded commands for the first one
* and have to duplicate them for the other styles. The order is: fill1, line,
* fill0. (That means we only ever recorded into fill0 if that's the only style.)
*/
function applySegmentToStyles(segment: PathSegment, styles: any,
linePaths: SegmentedPath[], fillPaths: SegmentedPath[]) {
if (!segment) {
return;
}
let path: SegmentedPath;
if (styles.fill0) {
path = fillPaths[styles.fill0 - 1];
// If fill0 is the only style, we have pushed the segment to its stack. In
// that case, just mark it as reversed and move on.
if (!(styles.fill1 || styles.line)) {
segment.isReversed = true;
return;
} else {
path.addSegment(segment.toReversed());
}
}
if (styles.line && styles.fill1) {
path = linePaths[styles.line - 1];
path.addSegment(segment.clone());
}
}
/*
* Converts records from the space-optimized format they're stored in to a
* format that's more amenable to fast rendering.
*
* See http://blogs.msdn.com/b/mswanson/archive/2006/02/27/539749.aspx and
* http://wahlers.com.br/claus/blog/hacking-swf-1-shapes-in-flash/ for details.
*/
function convertRecordsToShapeData(records: ShapeRecord[], fillPaths: SegmentedPath[],
linePaths: SegmentedPath[], dependencies: number[],
recordsMorph: ShapeRecord[]): ShapeData {
clearShared();
let isMorph = recordsMorph !== null;
let styles = {fill0: 0, fill1: 0, line: 0};
let segment: PathSegment = null;
// Fill- and line styles can be added by style change records in the middle of
// a shape records list. This also causes the previous paths to be treated as
// a group, so the lines don't get moved on top of any following fills.
// To support this, we just append all current fill and line paths to a list
// when new styles are introduced.
let allPaths: SegmentedPath[];
// If no style is set for a segment of a path, a 1px transparent line is used.
let defaultPath: SegmentedPath;
let numRecords = records.length;
let x: number = 0;
let y: number = 0;
let morphX: number = 0;
let morphY: number = 0;
let path: SegmentedPath;
for (let i = 0, j = 0; i < numRecords; i++) {
let record = records[i];
let morphRecord: ShapeRecord;
if (isMorph) {
morphRecord = recordsMorph[j++];
}
// type 0 is a StyleChange record
if (record.type === 0) {
//TODO: make the `has*` fields bitflags
if (segment) {
applySegmentToStyles(segment, styles, linePaths, fillPaths);
}
if (record.flags & ShapeRecordFlags.HasNewStyles) {
if (!allPaths) {
allPaths = [];
}
push.apply(allPaths, fillPaths);
fillPaths = createPathsList(record.fillStyles, false, isMorph, dependencies);
push.apply(allPaths, linePaths);
linePaths = createPathsList(record.lineStyles, true, isMorph, dependencies);
if (defaultPath) {
allPaths.push(defaultPath);
defaultPath = null;
}
styles = {fill0: 0, fill1: 0, line: 0};
}
if ((record.flags & ShapeRecordFlags.HasFillStyle0) !== 0) {
styles.fill0 = record.fillStyle0;
}
if ((record.flags & ShapeRecordFlags.HasFillStyle1) !== 0) {
styles.fill1 = record.fillStyle1;
}
if ((record.flags & ShapeRecordFlags.HasLineStyle) !== 0) {
styles.line = record.lineStyle;
}
if (styles.fill1) {
path = fillPaths[styles.fill1 - 1];
} else if (styles.line) {
path = linePaths[styles.line - 1];
} else if (styles.fill0) {
path = fillPaths[styles.fill0 - 1];
}
if (record.flags & ShapeRecordFlags.Move) {
x = record.moveX | 0;
y = record.moveY | 0;
// When morphed, StyleChangeRecords/MoveTo might not have a
// corresponding record in the start or end shape --
// processing morphRecord below before converting type 1 records.
}
// Very first record can be just fill/line-style definition record.
if (path) {
segment = PathSegment.FromDefaults(isMorph);
path.addSegment(segment);
// Move or not, we want this path segment to start where the last one
// left off. Even if the last one belonged to a different style.
// "Huh," you say? Yup.
if (!isMorph) {
segment.moveTo(x, y);
} else {
if (morphRecord.type === 0) {
morphX = morphRecord.moveX | 0;
morphY = morphRecord.moveY | 0;
} else {
morphX = x;
morphY = y;
// Not all moveTos are reflected in morph data.
// In that case, decrease morph data index.
j--;
}
segment.morphMoveTo(x, y, morphX, morphY);
}
}
}
// type 1 is a StraightEdge or CurvedEdge record
else {
release || assert(record.type === 1);
if (!segment) {
if (!defaultPath) {
let style = {color: {red: 0, green: 0, blue: 0, alpha: 0}, width: 20};
defaultPath = new SegmentedPath(null, processStyle(style, true, isMorph, dependencies));
}
segment = PathSegment.FromDefaults(isMorph);
defaultPath.addSegment(segment);
if (!isMorph) {
segment.moveTo(x, y);
} else {
segment.morphMoveTo(x, y, morphX, morphY);
}
}
if (isMorph) {
// An invalid SWF might contain a move in the EndEdges list where the
// StartEdges list contains an edge. The Flash Player seems to skip it,
// so we do, too.
while (morphRecord && morphRecord.type === 0) {
morphRecord = recordsMorph[j++];
}
// The EndEdges list might be shorter than the StartEdges list. Reuse
// start edges as end edges in that case.
if (!morphRecord) {
morphRecord = record;
}
}
if (record.flags & ShapeRecordFlags.IsStraight &&
(!isMorph || (morphRecord.flags & ShapeRecordFlags.IsStraight) !== 0)) {
x += record.deltaX | 0;
y += record.deltaY | 0;
if (!isMorph) {
segment.lineTo(x, y);
} else {
morphX += morphRecord.deltaX | 0;
morphY += morphRecord.deltaY | 0;
segment.morphLineTo(x, y, morphX, morphY);
}
} else {
let cx, cy;
let deltaX, deltaY;
if ((record.flags & ShapeRecordFlags.IsStraight) === 0) {
cx = x + record.controlDeltaX | 0;
cy = y + record.controlDeltaY | 0;
x = cx + record.anchorDeltaX | 0;
y = cy + record.anchorDeltaY | 0;
} else {
deltaX = record.deltaX | 0;
deltaY = record.deltaY | 0;
cx = x + (deltaX >> 1);
cy = y + (deltaY >> 1);
x += deltaX;
y += deltaY;
}
if (!isMorph) {
segment.curveTo(cx, cy, x, y);
} else {
let morphCX, morphCY;
if ((morphRecord.flags & ShapeRecordFlags.IsStraight) === 0) {
morphCX = morphX + morphRecord.controlDeltaX | 0;
morphCY = morphY + morphRecord.controlDeltaY | 0;
morphX = morphCX + morphRecord.anchorDeltaX | 0;
morphY = morphCY + morphRecord.anchorDeltaY | 0;
} else {
deltaX = morphRecord.deltaX | 0;
deltaY = morphRecord.deltaY | 0;
morphCX = morphX + (deltaX >> 1);
morphCY = morphY + (deltaY >> 1);
morphX += deltaX;
morphY += deltaY;
}
segment.morphCurveTo(cx, cy, x, y, morphCX, morphCY, morphX, morphY);
}
}
}
}
applySegmentToStyles(segment, styles, linePaths, fillPaths);
// All current paths get appended to the allPaths list at the end. First fill,
// then line paths.
if (allPaths) {
push.apply(allPaths, fillPaths);
} else {
allPaths = fillPaths;
}
push.apply(allPaths, linePaths);
if (defaultPath) {
allPaths.push(defaultPath);
}
let shape: ShapeData = new ShapeData();
if (isMorph) {
shape.morphCoordinates = new Int32Array(shape.coordinates.length);
shape.morphStyles = new DataBuffer(16);
}
for (let i = 0; i < allPaths.length; i++) {
allPaths[i].serialize(shape);
}
return shape;
}
interface ShapeStyle {
type: number;
fillType?: number;
width?: number;
pixelHinting?: boolean;
noHscale?: boolean;
noVscale?: boolean;
endCapsStyle?: number;
jointStyle?: number;
miterLimit?: number;
color?: number;
transform?: ShapeMatrix;
colors?: number[];
ratios?: number[];
spreadMethod?: number;
interpolationMode?: number;
focalPoint?: number;
bitmapId?: number;
bitmapIndex?: number;
repeat?: boolean;
smooth?: boolean;
morph: ShapeStyle
}
let IDENTITY_MATRIX: ShapeMatrix = {a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0};
function processStyle(style: any, isLineStyle: boolean, isMorph: boolean,
dependencies: number[]): ShapeStyle {
let shapeStyle: ShapeStyle = style;
if (isMorph) {
shapeStyle.morph = processMorphStyle(style, isLineStyle, dependencies);
}
if (isLineStyle) {
shapeStyle.miterLimit = (style.miterLimitFactor || 1.5) * 2;
if (!style.color && style.hasFill) {
let fillStyle = processStyle(style.fillStyle, false, false, dependencies);
shapeStyle.type = fillStyle.type;
shapeStyle.transform = fillStyle.transform;
shapeStyle.colors = fillStyle.colors;
shapeStyle.ratios = fillStyle.ratios;
shapeStyle.focalPoint = fillStyle.focalPoint;
shapeStyle.bitmapId = fillStyle.bitmapId;
shapeStyle.bitmapIndex = fillStyle.bitmapIndex;
shapeStyle.repeat = fillStyle.repeat;
style.fillStyle = null;
return shapeStyle;
} else {
shapeStyle.type = FillType.Solid;
return shapeStyle;
}
}
if (style.type === undefined || style.type === FillType.Solid) {
return shapeStyle;
}
let scale;
switch (style.type) {
case FillType.LinearGradient:
case FillType.RadialGradient:
case FillType.FocalRadialGradient:
let records = style.records;
let colors: Array<any> = shapeStyle.colors = [];
let ratios: Array<any> = shapeStyle.ratios = [];
for (let i = 0; i < records.length; i++) {
let record = records[i];
colors.push(record.color);
ratios.push(record.ratio);
}
scale = 819.2;
break;
case FillType.RepeatingBitmap:
case FillType.ClippedBitmap:
case FillType.NonsmoothedRepeatingBitmap:
case FillType.NonsmoothedClippedBitmap:
shapeStyle.smooth = style.type !== FillType.NonsmoothedRepeatingBitmap &&
style.type !== FillType.NonsmoothedClippedBitmap;
shapeStyle.repeat = style.type !== FillType.ClippedBitmap &&
style.type !== FillType.NonsmoothedClippedBitmap;
let index = dependencies.indexOf(style.bitmapId);
if (index === -1) {
index = dependencies.length;
dependencies.push(style.bitmapId);
}
shapeStyle.bitmapIndex = index;
scale = 0.05;
break;
default:
Debug.warning('shape parser encountered invalid fill style ' + style.type);
}
if (!style.matrix) {
shapeStyle.transform = IDENTITY_MATRIX;
return shapeStyle;
}
let matrix = style.matrix;
shapeStyle.transform = {
a: (matrix.a * scale),
b: (matrix.b * scale),
c: (matrix.c * scale),
d: (matrix.d * scale),
tx: matrix.tx / 20,
ty: matrix.ty / 20
};
// null data that's unused from here on out
style.matrix = null;
return shapeStyle;
}
function processMorphStyle(style: any, isLineStyle: boolean, dependencies: number[]): ShapeStyle {
let morphStyle: ShapeStyle = Object.create(style);
if (isLineStyle) {
morphStyle.width = style.widthMorph;
if (!style.color && style.hasFill) {
let fillStyle = processMorphStyle(style.fillStyle, false, dependencies);
morphStyle.transform = fillStyle.transform;
morphStyle.colors = fillStyle.colors;
morphStyle.ratios = fillStyle.ratios;
return morphStyle;
} else {
morphStyle.color = style.colorMorph;
return morphStyle;
}
}
if (style.type === undefined) {
return morphStyle;
}
if (style.type === FillType.Solid) {
morphStyle.color = style.colorMorph;
return morphStyle;
}
let scale;
switch (style.type) {
case FillType.LinearGradient:
case FillType.RadialGradient:
case FillType.FocalRadialGradient:
let records = style.records;
let colors: Array<any> = morphStyle.colors = [];
let ratios: Array<any> = morphStyle.ratios = [];
for (let i = 0; i < records.length; i++) {
let record = records[i];
colors.push(record.colorMorph);
ratios.push(record.ratioMorph);
}
scale = 819.2;
break;
case FillType.RepeatingBitmap:
case FillType.ClippedBitmap:
case FillType.NonsmoothedRepeatingBitmap:
case FillType.NonsmoothedClippedBitmap:
scale = 0.05;
break;
default:
release || assertUnreachable('shape parser encountered invalid fill style');
}
if (!style.matrix) {
morphStyle.transform = IDENTITY_MATRIX;
return morphStyle;
}
let matrix = style.matrixMorph;
morphStyle.transform = {
a: (matrix.a * scale),
b: (matrix.b * scale),
c: (matrix.c * scale),
d: (matrix.d * scale),
tx: matrix.tx / 20,
ty: matrix.ty / 20
};
return morphStyle;
}
/*
* Paths are stored in 2-dimensional arrays. Each of the inner arrays contains
* all the paths for a certain fill or line style.
*/
function createPathsList(styles: any[], isLineStyle: boolean, isMorph: boolean,
dependencies: number[]): SegmentedPath[] {
let paths: SegmentedPath[] = [];
for (let i = 0; i < styles.length; i++) {
let style = processStyle(styles[i], isLineStyle, isMorph, dependencies);
if (!isLineStyle) {
paths[i] = new SegmentedPath(style, null);
} else {
paths[i] = new SegmentedPath(null, style);
}
}
return paths;
}
export function defineShape(tag: ShapeTag) {
let dependencies: Array<any> = [];
let fillPaths = createPathsList(tag.fillStyles, false, !!tag.recordsMorph, dependencies);
let linePaths = createPathsList(tag.lineStyles, true, !!tag.recordsMorph, dependencies);
let shape = convertRecordsToShapeData(tag.records, fillPaths, linePaths,
dependencies, tag.recordsMorph || null);
return {
type: tag.flags & ShapeFlags.IsMorph ? 'morphshape' : 'shape',
id: tag.id,
fillBounds: tag.fillBounds,
lineBounds: tag.lineBounds,
morphFillBounds: tag.fillBoundsMorph || null,
morphLineBounds: tag.lineBoundsMorph || null,
shape: shape.toPlainObject(),
transferables: shape.buffers,
require: dependencies.length ? dependencies : null
};
}
let sharedCommands: DataBuffer = null;
let sharedData: DataBuffer = null;
let sharedMorphData: DataBuffer = null;
let segmentsPool: Array<PathSegment> = [];
let segmentsCounter: number = 0;
function clearShared() {
if (!sharedCommands) {
sharedCommands = new DataBuffer();
sharedData = new DataBuffer();
sharedMorphData = new DataBuffer();
sharedCommands.endian = sharedData.endian = sharedMorphData.endian = 'auto';
}
sharedCommands.clear();
sharedData.clear();
sharedMorphData.clear();
segmentsCounter = 0;
}
class PathSegment {
// public startPoint: string;
// public endPoint: string;
public startPoint: number;
public endPoint: number;
public flag: boolean;
commandsStart: number;
commandsEnd: number;
dataStart: number;
dataEnd: number;
morphDataStart: number;
morphDataEnd: number;
prev: PathSegment;
next: PathSegment;
isMorph: boolean;
isReversed: boolean;
reset() {
this.commandsStart = sharedCommands.position;
this.commandsEnd = sharedCommands.position;
this.dataStart = sharedData.position;
this.dataEnd = sharedData.position;
this.morphDataStart = sharedMorphData.position;
this.morphDataEnd = sharedMorphData.position;
this.isMorph = false;
this.isReversed = false;
this.prev = null;
this.next = null;
this.startPoint = 0;
this.endPoint = 0;
this.flag = false;
}
constructor() {
this.reset();
}
static FromDefaults(isMorph: boolean) {
let segment = segmentsPool[segmentsCounter];
if (!segment) {
segment = new PathSegment();
segmentsPool.push(segment);
} else {
segment.reset();
}
segmentsCounter++;
segment.isMorph = isMorph;
return segment;
}
moveTo(x: number, y: number) {
sharedCommands.writeUnsignedByte(PathCommand.MoveTo);
sharedData.write2Ints(x, y);
this.commandsEnd = sharedCommands.position;
this.dataEnd = sharedData.position;
}
morphMoveTo(x: number, y: number, mx: number, my: number) {
this.moveTo(x, y);
sharedMorphData.write2Ints(mx, my);
this.morphDataEnd = sharedMorphData.position;
}
lineTo(x: number, y: number) {
sharedCommands.writeUnsignedByte(PathCommand.LineTo);
sharedData.write2Ints(x, y);
this.commandsEnd = sharedCommands.position;
this.dataEnd = sharedData.position;
}
morphLineTo(x: number, y: number, mx: number, my: number) {
this.lineTo(x, y);
sharedMorphData.write2Ints(mx, my);
this.morphDataEnd = sharedMorphData.position;
}
curveTo(cpx: number, cpy: number, x: number, y: number) {
sharedCommands.writeUnsignedByte(PathCommand.CurveTo);
sharedData.write4Ints(cpx, cpy, x, y);
;
this.commandsEnd = sharedCommands.position;
this.dataEnd = sharedData.position;
}
morphCurveTo(cpx: number, cpy: number, x: number, y: number,
mcpx: number, mcpy: number, mx: number, my: number) {
this.curveTo(cpx, cpy, x, y);
sharedMorphData.write4Ints(mcpx, mcpy, mx, my);
this.morphDataEnd = sharedMorphData.position;
}
/**
* Returns a shallow copy of the segment with the "isReversed" flag set.
* Reversed segments play themselves back in reverse when they're merged into the final
* non-segmented path.
* Note: Don't modify the original, or the reversed copy, after this operation!
*/
toReversed(): PathSegment {
release || assert(!this.isReversed);
let segment = this.clone();
segment.isReversed = true;
return segment;
}
clone(): PathSegment {
let segment = PathSegment.FromDefaults(this.isMorph);
segment.commandsStart = this.commandsStart;
segment.commandsEnd = this.commandsEnd;
segment.dataStart = this.dataStart;
segment.dataEnd = this.dataEnd;
segment.morphDataStart = this.morphDataStart;
segment.morphDataEnd = this.morphDataEnd;
segment.isReversed = this.isReversed;
return segment;
}
storeStartAndEnd() {
let data = sharedData.ints;
let pos = this.dataStart >> 2;
let endPoint1 = data[pos] + data[pos + 1] * (1 << 24);
pos = (this.dataEnd >> 2) - 2;
let endPoint2 = data[pos] + data[pos + 1] * (1 << 24);
if (!this.isReversed) {
this.startPoint = endPoint1;
this.endPoint = endPoint2;
} else {
this.startPoint = endPoint2;
this.endPoint = endPoint1;
}
this.flag = false;
}
connectsTo(other: PathSegment): boolean {
release || assert(other !== this);
release || assert(this.endPoint);
release || assert(other.startPoint);
return this.endPoint === other.startPoint;
}
startConnectsTo(other: PathSegment): boolean {
release || assert(other !== this);
return this.startPoint === other.startPoint;
}
flipDirection() {
let tempPoint = 0;
tempPoint = this.startPoint;
this.startPoint = this.endPoint;
this.endPoint = tempPoint;
this.isReversed = !this.isReversed;
}
serialize(shape: ShapeData, lastPosition: { x: number; y: number }) {
if (this.isReversed) {
this._serializeReversed(shape, lastPosition);
return;
}
let commands = sharedCommands.bytes;
// Note: this *must* use `this.data.length`, because buffers will have padding.
let dataEnd = this.dataEnd >> 2;
let morphData = this.isMorph ? sharedMorphData.ints : null;
let data = sharedData.ints;
let cPos = this.commandsStart;
release || assert(commands[cPos] === PathCommand.MoveTo);
// If the segment's first moveTo goes to the current coordinates, we have to skip it.
let dataPosition = this.dataStart >> 2;
let morphOffset = (this.morphDataStart >> 2) - dataPosition;
let offset = 0;
if (data[dataPosition] === lastPosition.x && data[dataPosition + 1] === lastPosition.y) {
offset++;
}
let commandsCount = this.commandsEnd - this.commandsStart;
dataPosition += offset * 2;
for (let i = offset; i < commandsCount; i++) {
dataPosition = this._writeCommand(commands[cPos + i], dataPosition, data, morphOffset, morphData, shape);
}
release || assert(dataPosition === dataEnd);
lastPosition.x = data[dataEnd - 2];
lastPosition.y = data[dataEnd - 1];
}
private _serializeReversed(shape: ShapeData, lastPosition: { x: number; y: number }) {
// For reversing the fill0 segments, we rely on the fact that each segment
// starts with a moveTo. We first write a new moveTo with the final drawing command's
// target coordinates (if we don't skip it, see below). For each of the following
// commands, we take the coordinates of the command originally *preceding*
// it as the new target coordinates. The final coordinates we target will be
// the ones from the original first moveTo.
// Note: these *must* use `this.{data,commands}.length`, because buffers will have padding.
let commandsCount = this.commandsEnd - this.commandsStart;
let dataPosition = (this.dataEnd >> 2) - 2;
let morphOffset = (this.morphDataEnd - this.dataEnd) >> 2;
let commands = sharedCommands.bytes;
let cPos = this.commandsStart;
release || assert(commands[cPos] === PathCommand.MoveTo);
let data = sharedData.ints;
let morphData = this.isMorph ? sharedMorphData.ints : null;
// Only write the first moveTo if it doesn't go to the current coordinates.
if (data[dataPosition] !== lastPosition.x || data[dataPosition + 1] !== lastPosition.y) {
this._writeCommand(PathCommand.MoveTo, dataPosition, data, morphOffset, morphData, shape);
}
if (commandsCount === 1) {
lastPosition.x = data[this.dataStart >> 2];
lastPosition.y = data[(this.dataStart >> 2) + 1];
return;
}
for (let i = commandsCount; i-- > 1;) {
dataPosition -= 2;
let command: PathCommand = commands[cPos + i];
shape.writeCommandAndCoordinates(command, data[dataPosition], data[dataPosition + 1]);
if (morphData) {
shape.writeMorphCoordinates(morphData[dataPosition + morphOffset], morphData[dataPosition + 1 + morphOffset]);
}
if (command === PathCommand.CurveTo) {
dataPosition -= 2;
shape.writeCoordinates(data[dataPosition], data[dataPosition + 1]);
if (morphData) {
shape.writeMorphCoordinates(morphData[dataPosition + morphOffset], morphData[dataPosition + 1 + morphOffset]);
}
} else {
}
}
const dataStart = this.dataStart >> 2;
release || assert(dataPosition === dataStart);
lastPosition.x = data[dataStart];
lastPosition.y = data[dataStart + 1];
}
private _writeCommand(command: PathCommand, position: number, data: Uint32Array,
morphOffset: number, morphData: Uint32Array, shape: ShapeData): number {
shape.writeCommandAndCoordinates(command, data[position++], data[position++]);
if (morphData) {
shape.writeMorphCoordinates(morphData[position - 2 + morphOffset], morphData[position - 1 + morphOffset]);
}
if (command === PathCommand.CurveTo) {
shape.writeCoordinates(data[position++], data[position++]);
if (morphData) {
shape.writeMorphCoordinates(morphData[position - 2 + morphOffset], morphData[position - 1 + morphOffset]);
}
}
return position;
}
}
let absCounter = 0;
class SlowSegmentedPath {
private _head: PathSegment;
debugCount: number;
constructor(public fillStyle: any, public lineStyle: any) {
this._head = null;
this.debugCount = 0;
}
addSegment(segment: PathSegment) {
release || assert(segment);
release || assert(segment.next === null);
release || assert(segment.prev === null);
let currentHead = this._head;
if (currentHead) {
release || assert(segment !== currentHead);
currentHead.next = segment;
segment.prev = currentHead;
}
this._head = segment;
this.debugCount++;
}
// Does *not* reset the segment's prev and next pointers!
removeSegment(segment: PathSegment) {
if (segment.prev) {
segment.prev.next = segment.next;
}
if (segment.next) {
segment.next.prev = segment.prev;
}
this.debugCount--;
}
insertSegment(segment: PathSegment, next: PathSegment) {
let prev = next.prev;
segment.prev = prev;
segment.next = next;
if (prev) {
prev.next = segment;
}
next.prev = segment;
this.debugCount++;
}
head(): PathSegment {
return this._head;
}
serialize(shape: ShapeData) {
let segment = this.head();
if (!segment) {
// Path is empty.
return null;
}
let debugOn = this.debugCount > 1000;
let iterCount = 0, joinsCount = 0;
console.log(`=== SEGMENTED PATH ${absCounter}===`);
absCounter++;
let counter = 0;
while (segment) {
segment.storeStartAndEnd();
console.log(`${counter} : ${segment.startPoint} - ${segment.endPoint}`);
segment = segment.prev;
counter++;
}
let start = this.head();
let end = start;
let finalRoot: PathSegment = null;
let finalHead: PathSegment = null;
// Path segments for one style can appear in arbitrary order in the tag's list
// of edge records.
// Before we linearize them, we have to identify all pairs of segments where
// one ends at a coordinate the other starts at.
// The following loop does that, by creating ever-growing runs of matching
// segments. If no more segments are found that match the current run (either
// at the beginning, or at the end), the current run is complete, and a new
// one is started. Rinse, repeat, until no solitary segments remain.
let current = start.prev;
while (start) {
while (current) {
iterCount++;
if (current.startConnectsTo(start)) {
current.flipDirection();
}
if (current.connectsTo(start)) {
if (current.next !== start) {
joinsCount++;
this.removeSegment(current);
this.insertSegment(current, start);
}
start = current;
current = start.prev;
continue;
}
if (current.startConnectsTo(end)) {
current.flipDirection();
}
if (end.connectsTo(current)) {
this.removeSegment(current);
end.next = current;
current = current.prev;
end.next.prev = end;
end.next.next = null;
end = end.next;
continue;
}
current = current.prev;
}
// This run of segments is finished. Store and forget it (for this loop).
current = start.prev;
if (!finalRoot) {
finalRoot = start;
finalHead = end;
} else {
finalHead.next = start;
start.prev = finalHead;
finalHead = end;
finalHead.next = null;
}
if (!current) {
break;
}
start = end = current;
current = start.prev;
}
if (debugOn) {
console.log(`Debug SegmentPath serialize segments=${this.debugCount}, iterations=${iterCount}, joins = ${joinsCount}`);
}
if (this.fillStyle) {
let style = this.fillStyle;
let morph = style.morph;
switch (style.type) {
case FillType.Solid:
shape.beginFill(style.color);
if (morph) {
shape.writeMorphFill(morph.color);
}
break;
case FillType.LinearGradient:
case FillType.RadialGradient:
case FillType.FocalRadialGradient:
writeGradient(PathCommand.BeginGradientFill, style, shape);
if (morph) {
writeMorphGradient(morph, shape);
}
break;
case FillType.ClippedBitmap:
case FillType.RepeatingBitmap:
case FillType.NonsmoothedClippedBitmap:
case FillType.NonsmoothedRepeatingBitmap:
writeBitmap(PathCommand.BeginBitmapFill, style, shape);
if (morph) {
writeMorphBitmap(morph, shape);
}
break;
default:
release || assertUnreachable('Invalid fill style type: ' + style.type);
}
} else {
let style = this.lineStyle;
let morph = style.morph;
release || assert(style);
switch (style.type) {
case FillType.Solid:
writeLineStyle(style, shape);
if (morph) {
writeMorphLineStyle(morph, shape);
}
break;
case FillType.LinearGradient:
case FillType.RadialGradient:
case FillType.FocalRadialGradient:
writeLineStyle(style, shape);
writeGradient(PathCommand.LineStyleGradient, style, shape);
if (morph) {
writeMorphLineStyle(morph, shape);
writeMorphGradient(morph, shape);
}
break;
case FillType.ClippedBitmap:
case FillType.RepeatingBitmap:
case FillType.NonsmoothedClippedBitmap:
case FillType.NonsmoothedRepeatingBitmap:
writeLineStyle(style, shape);
writeBitmap(PathCommand.LineStyleBitmap, style, shape);
if (morph) {
writeMorphLineStyle(morph, shape);
writeMorphBitmap(morph, shape);
}
break;
default:
//console.error('Line style type not yet supported: ' + style.type);
}
}
let lastPosition = {x: 0, y: 0};
current = finalRoot;
counter = 0;
console.log("--- AFTER SORT");
while (current) {
current.serialize(shape, lastPosition);
console.log(`${counter} : ${current.startPoint} - ${current.endPoint}`);
current = current.next;
counter++;
}
if (this.fillStyle) {
shape.endFill();
} else {
shape.endLine();
}
return shape;
}
}
function writeLineStyle(style: ShapeStyle, shape: ShapeData): void {
// No scaling == 0, normal == 1, vertical only == 2, horizontal only == 3.
let scaleMode = style.noHscale ?
(style.noVscale ? 0 : 2) :
style.noVscale ? 3 : 1;
// TODO: Figure out how to handle startCapsStyle
let thickness = clamp(style.width, 0, 0xff * 20) | 0;
shape.lineStyle(thickness, style.color,
style.pixelHinting, scaleMode, style.endCapsStyle,
style.jointStyle, style.miterLimit);
}
function writeMorphLineStyle(style: ShapeStyle, shape: ShapeData): void {
// TODO: Figure out how to handle startCapsStyle
let thickness = clamp(style.width, 0, 0xff * 20) | 0;
shape.writeMorphLineStyle(thickness, style.color);
}
function writeGradient(command: PathCommand, style: ShapeStyle, shape: ShapeData): void {
let gradientType = style.type === FillType.LinearGradient ?
GradientType.Linear :
GradientType.Radial;
shape.beginGradient(command, style.colors, style.ratios,
gradientType, style.transform, style.spreadMethod,
style.interpolationMode, style.focalPoint / 2 | 0);
}
function writeMorphGradient(style: ShapeStyle, shape: ShapeData) {
shape.writeMorphGradient(style.colors, style.ratios, style.transform);
}
function writeBitmap(command: PathCommand, style: ShapeStyle, shape: ShapeData): void {
shape.beginBitmap(command, style.bitmapIndex, style.transform, style.repeat, style.smooth);
}
function writeMorphBitmap(style: ShapeStyle, shape: ShapeData) {
shape.writeMorphBitmap(style.transform);
}
class SegmentedPath {
segments: Array<PathSegment>;
match: Map<number, PathSegment>;
constructor(public fillStyle: any, public lineStyle: any) {
this.segments = [];
this.match = new Map();
}
addSegment(segment: PathSegment) {
this.segments.push(segment);
}
checkSegment(segment: PathSegment) {
const match = this.match;
segment.storeStartAndEnd();
segment.prev = null;
segment.next = null;
segment.flag = true;
let p = match.get(segment.startPoint);
if (p) {
segment.prev = p;
if (p.prev) {
p.next = segment;
} else {
p.prev = segment;
}
match.delete(segment.startPoint);
} else {
match.set(segment.startPoint, segment);
}
p = match.get(segment.endPoint);
if (p) {
segment.next = p;
if (p.prev) {
p.next = segment;
} else {
p.prev = segment;
}
match.delete(segment.endPoint);
} else {
match.set(segment.endPoint, segment);
}
}
serialize(shape: ShapeData) {
const segments = this.segments;
if (segments.length === 0) {
return null;
}
this.serializeStyle(shape);
let lastPosition = {x: 0, y: 0};
// console.log(`=== SEGMENTED PATH ${absCounter}===`);
// absCounter++;
for (let i = segments.length - 1; i >= 0; i--) {
this.checkSegment(segments[i]);
// console.log(`${i} : ${segments[i].startPoint} - ${segments[i].endPoint}`);
}
// let counter = 0;
// console.log("--- AFTER SORT");
for (let i = segments.length - 1; i >= 0; i--) {
let seg = segments[i];
if (!seg.flag) {
continue;
}
let matchEnd = 0;
if (seg.prev && (seg.prev.startPoint === seg.endPoint
|| seg.prev.endPoint === seg.endPoint)) {
matchEnd = -1;
}
if (seg.next && (seg.next.startPoint === seg.endPoint
|| seg.next.endPoint === seg.endPoint)) {
matchEnd = 1;
}
// find the start of sequence
if (seg.next && seg.prev) {
let current = matchEnd === -1 ? seg.prev : seg.next;
if (seg.prev && (seg.prev.startPoint === seg.endPoint
|| seg.prev.endPoint === seg.endPoint)) {
current = seg.next;
}
let prev = seg;
while (current !== seg && current) {
let next = current.next === prev ? current.prev : current.next;
if (!next) {
break;
}
prev = current;
current = next;
}
if (current !== seg) {
seg = current;
matchEnd = 0;
if (seg.prev && (seg.prev.startPoint === seg.endPoint
|| seg.prev.endPoint === seg.endPoint)) {
matchEnd = -1;
}
if (seg.next && (seg.next.startPoint === seg.endPoint
|| seg.next.endPoint === seg.endPoint)) {
matchEnd = 1;
}
}
}
if (matchEnd === 0 && (seg.next || seg.prev)) {
seg.flipDirection();
}
let current = seg.next;
if (seg.prev && (seg.prev.startPoint === seg.endPoint
|| seg.prev.endPoint === seg.endPoint)) {
current = seg.prev;
}
seg.serialize(shape, lastPosition);
//console.log(`${counter} : ${seg.startPoint} - ${seg.endPoint}`);
// counter++;
seg.flag = false;
let prev = seg;
let prevPoint = seg.endPoint;
while (current && current.flag) {
current.flag = false;
if (current.endPoint === prevPoint) current.flipDirection();
current.serialize(shape, lastPosition);
// console.log(`${counter} : ${current.startPoint} - ${current.endPoint}`);
// counter++;
prevPoint = current.endPoint;
let next = current.next === prev ? current.prev : current.next;
prev = current;
current = next;
}
}
this.match = new Map();
if (this.fillStyle) {
shape.endFill();
} else {
shape.endLine();
}
return shape;
}
serializeStyle(shape: ShapeData) {
if (this.fillStyle) {
let style = this.fillStyle;
let morph = style.morph;
switch (style.type) {
case FillType.Solid:
shape.beginFill(style.color);
if (morph) {
shape.writeMorphFill(morph.color);
}
break;
case FillType.LinearGradient:
case FillType.RadialGradient:
case FillType.FocalRadialGradient:
writeGradient(PathCommand.BeginGradientFill, style, shape);
if (morph) {
writeMorphGradient(morph, shape);
}
break;
case FillType.ClippedBitmap:
case FillType.RepeatingBitmap:
case FillType.NonsmoothedClippedBitmap:
case FillType.NonsmoothedRepeatingBitmap:
writeBitmap(PathCommand.BeginBitmapFill, style, shape);
if (morph) {
writeMorphBitmap(morph, shape);
}
break;
default:
release || assertUnreachable('Invalid fill style type: ' + style.type);
}
} else {
let style = this.lineStyle;
let morph = style.morph;
release || assert(style);
switch (style.type) {
case FillType.Solid:
writeLineStyle(style, shape);
if (morph) {
writeMorphLineStyle(morph, shape);
}
break;
case FillType.LinearGradient:
case FillType.RadialGradient:
case FillType.FocalRadialGradient:
writeLineStyle(style, shape);
writeGradient(PathCommand.LineStyleGradient, style, shape);
if (morph) {
writeMorphLineStyle(morph, shape);
writeMorphGradient(morph, shape);
}
break;
case FillType.ClippedBitmap:
case FillType.RepeatingBitmap:
case FillType.NonsmoothedClippedBitmap:
case FillType.NonsmoothedRepeatingBitmap:
writeLineStyle(style, shape);
writeBitmap(PathCommand.LineStyleBitmap, style, shape);
if (morph) {
writeMorphLineStyle(morph, shape);
writeMorphBitmap(morph, shape);
}
break;
default:
//console.error('Line style type not yet supported: ' + style.type);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment