@breakside/jskit 2026.11.0 → 2026.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/Frameworks/DOM.jsframework/Info.json +2 -2
  2. package/Frameworks/DOM.jsframework/io.breakside.JSKit.DOM-bundle.js +2 -2
  3. package/Frameworks/FontKit.jsframework/Info.json +2 -2
  4. package/Frameworks/FontKit.jsframework/JS/FNTOpenTypeFont.js +331 -22
  5. package/Frameworks/FontKit.jsframework/io.breakside.JSKit.FontKit-bundle.js +2 -2
  6. package/Frameworks/Foundation.jsframework/Info.json +2 -2
  7. package/Frameworks/Foundation.jsframework/JS/JSAttributedString.js +13 -1
  8. package/Frameworks/Foundation.jsframework/JS/JSContext.js +44 -17
  9. package/Frameworks/Foundation.jsframework/JS/JSCubicBezier.js +54 -1
  10. package/Frameworks/Foundation.jsframework/JS/JSFont.js +8 -9
  11. package/Frameworks/Foundation.jsframework/JS/JSGradient.js +28 -2
  12. package/Frameworks/Foundation.jsframework/JS/JSImage+HTML.js +27 -1
  13. package/Frameworks/Foundation.jsframework/JS/JSNumberFormatter.js +23 -1
  14. package/Frameworks/Foundation.jsframework/JS/JSPath.js +192 -6
  15. package/Frameworks/Foundation.jsframework/JS/JSPoint.js +56 -0
  16. package/Frameworks/Foundation.jsframework/JS/JSTextRun.js +21 -4
  17. package/Frameworks/Foundation.jsframework/JS/JSTextStorage.js +5 -0
  18. package/Frameworks/Foundation.jsframework/JS/JSTextTypesetter.js +6 -1
  19. package/Frameworks/Foundation.jsframework/JS/JSXMLParser.js +1 -1
  20. package/Frameworks/Foundation.jsframework/io.breakside.JSKit.Foundation-bundle.js +2 -2
  21. package/Frameworks/Foundation.jsframework/sources.json +1 -0
  22. package/Frameworks/SecurityKit.jsframework/Info.json +2 -2
  23. package/Frameworks/SecurityKit.jsframework/io.breakside.JSKit.SecurityKit-bundle.js +2 -2
  24. package/Info.json +2 -2
  25. package/Node/io.breakside.jskit-bundle.js +2 -2
  26. package/Root/Frameworks/APIKit/Info.yaml +1 -1
  27. package/Root/Frameworks/APIKitTesting/Info.yaml +1 -1
  28. package/Root/Frameworks/AuthKit/Info.yaml +1 -1
  29. package/Root/Frameworks/CSSOM/Info.yaml +1 -1
  30. package/Root/Frameworks/ChartKit/Info.yaml +1 -1
  31. package/Root/Frameworks/ConferenceKit/Info.yaml +1 -1
  32. package/Root/Frameworks/DBKit/Info.yaml +1 -1
  33. package/Root/Frameworks/DOM/Info.yaml +1 -1
  34. package/Root/Frameworks/Dispatch/Info.yaml +1 -1
  35. package/Root/Frameworks/FontKit/FNTOpenTypeFont.js +331 -22
  36. package/Root/Frameworks/FontKit/Info.yaml +1 -1
  37. package/Root/Frameworks/Foundation/Info.yaml +1 -1
  38. package/Root/Frameworks/Foundation/JSAttributedString.js +13 -1
  39. package/Root/Frameworks/Foundation/JSContext.js +44 -17
  40. package/Root/Frameworks/Foundation/JSCubicBezier.js +54 -1
  41. package/Root/Frameworks/Foundation/JSFont.js +8 -9
  42. package/Root/Frameworks/Foundation/JSGradient.js +28 -2
  43. package/Root/Frameworks/Foundation/JSImage+HTML.js +27 -1
  44. package/Root/Frameworks/Foundation/JSNumberFormatter.js +23 -1
  45. package/Root/Frameworks/Foundation/JSPath.js +192 -6
  46. package/Root/Frameworks/Foundation/JSPoint.js +56 -0
  47. package/Root/Frameworks/Foundation/JSTextRun.js +21 -4
  48. package/Root/Frameworks/Foundation/JSTextStorage.js +5 -0
  49. package/Root/Frameworks/Foundation/JSTextTypesetter.js +6 -1
  50. package/Root/Frameworks/Foundation/JSXMLParser.js +1 -1
  51. package/Root/Frameworks/ImageKit/IKHTMLCanvasBitmapContext.js +187 -101
  52. package/Root/Frameworks/ImageKit/IKNodeBitmapContext.js +4 -4
  53. package/Root/Frameworks/ImageKit/Info.yaml +1 -1
  54. package/Root/Frameworks/MediaKit/Info.yaml +1 -1
  55. package/Root/Frameworks/MediaKitUI/Info.yaml +1 -1
  56. package/Root/Frameworks/NotificationKit/Info.yaml +1 -1
  57. package/Root/Frameworks/PDFKit/Info.yaml +1 -1
  58. package/Root/Frameworks/QRKit/Info.yaml +1 -1
  59. package/Root/Frameworks/SearchKit/Info.yaml +1 -1
  60. package/Root/Frameworks/SecurityKit/Info.yaml +1 -1
  61. package/Root/Frameworks/ServerKit/Info.yaml +1 -1
  62. package/Root/Frameworks/ServerKitTesting/Info.yaml +1 -1
  63. package/Root/Frameworks/TestKit/Info.yaml +1 -1
  64. package/Root/Frameworks/TestKit/TKAssert.js +4 -4
  65. package/Root/Frameworks/UIKit/Info.yaml +1 -1
  66. package/Root/Frameworks/UIKit/UIApplication.js +9 -1
  67. package/Root/Frameworks/UIKit/UICollectionView.js +4 -0
  68. package/Root/Frameworks/UIKit/UIColorPanel.js +46 -0
  69. package/Root/Frameworks/UIKit/UIColorWell.js +8 -0
  70. package/Root/Frameworks/UIKit/UIHTMLDisplayServer.js +21 -2
  71. package/Root/Frameworks/UIKit/UIHTMLDisplayServerCanvasContext.js +150 -69
  72. package/Root/Frameworks/UIKit/UIHTMLTextRun.js +5 -0
  73. package/Root/Frameworks/UIKit/UIHTMLTextTypesetter.js +18 -3
  74. package/Root/Frameworks/UIKit/UIListView.js +7 -5
  75. package/Root/Frameworks/UIKit/UIMenu.js +50 -32
  76. package/Root/Frameworks/UIKit/UIMenuItem.js +12 -0
  77. package/Root/Frameworks/UIKit/UIMenuView.js +27 -5
  78. package/Root/Frameworks/UIKit/UIPlatform.js +7 -0
  79. package/Root/Frameworks/UIKit/UIPopupButton.js +7 -1
  80. package/Root/Frameworks/UIKit/UIResponder.js +8 -0
  81. package/Root/Frameworks/UIKit/UITextEditor.js +14 -7
  82. package/Root/Frameworks/UIKit/UITokenField.js +65 -0
  83. package/Root/Frameworks/UIKit/UIToolbar.js +1 -1
  84. package/Root/Frameworks/UIKitTesting/Info.yaml +1 -1
  85. package/package.json +1 -1
@@ -65,6 +65,7 @@ JSClass("JSAttributedString", JSObject, {
65
65
  case JSAttributedString.Attribute.font:
66
66
  return JSAttributedString.AttributeDecoder.font(value);
67
67
  case JSAttributedString.Attribute.textColor:
68
+ case JSAttributedString.Attribute.strokeColor:
68
69
  case JSAttributedString.Attribute.backgroundColor:
69
70
  return JSAttributedString.AttributeDecoder.color(value);
70
71
  case JSAttributedString.Attribute.link:
@@ -90,6 +91,7 @@ JSClass("JSAttributedString", JSObject, {
90
91
  case JSAttributedString.Attribute.headIndent:
91
92
  case JSAttributedString.Attribute.tailIndent:
92
93
  case JSAttributedString.Attribute.minimumLineHeight:
94
+ case JSAttributedString.Attribute.strokeWidth:
93
95
  return JSAttributedString.AttributeDecoder.number(value);
94
96
  case JSAttributedString.Attribute.textAlignment:
95
97
  return JSAttributedString.AttributeDecoder.enumValue(JSTextAlignment)(value);
@@ -137,6 +139,7 @@ JSClass("JSAttributedString", JSObject, {
137
139
  case JSAttributedString.Attribute.font:
138
140
  return JSAttributedString.AttributeEncoder.font(value);
139
141
  case JSAttributedString.Attribute.textColor:
142
+ case JSAttributedString.Attribute.strokeColor:
140
143
  case JSAttributedString.Attribute.backgroundColor:
141
144
  return JSAttributedString.AttributeEncoder.color(value);
142
145
  case JSAttributedString.Attribute.link:
@@ -162,6 +165,7 @@ JSClass("JSAttributedString", JSObject, {
162
165
  case JSAttributedString.Attribute.lineBreakMode:
163
166
  case JSAttributedString.Attribute.maskCharacter:
164
167
  case JSAttributedString.Attribute.paragraphStyleName:
168
+ case JSAttributedString.Attribute.strokeWidth:
165
169
  default:
166
170
  return value;
167
171
  }
@@ -680,6 +684,7 @@ JSClass("JSAttributedString", JSObject, {
680
684
  JSAttributedString.Attribute = {
681
685
  font: "font",
682
686
  textColor: "textColor",
687
+ strokeColor: "strokeColor",
683
688
  backgroundColor: "backgroundColor",
684
689
  bold: "bold",
685
690
  italic: "italic",
@@ -689,6 +694,7 @@ JSAttributedString.Attribute = {
689
694
  maskCharacter: "maskCharacter",
690
695
  cursor: "cursor",
691
696
  link: "link",
697
+ strokeWidth: "strokeWidth",
692
698
 
693
699
  // Paragraph
694
700
  paragraphStyleName: "paragraphStyleName",
@@ -745,7 +751,13 @@ JSAttributedString.AttributeDecoder = {
745
751
  dataBase64URL: function(value){ return value.dataByDecodingBase64URL(); },
746
752
  dataHex: function(value){ return value.dataByDecodingHex(); },
747
753
  url: function(value){ return JSURL.initWithString(value); },
748
- font: function(value){ return JSFont.initFromDictionary(value); },
754
+ font: function(value){
755
+ var font = JSFont.initFromDictionary(value);
756
+ if (font === null){
757
+ font = JSFont.initWithDescriptor(JSFontDescriptor.system, value.size);
758
+ }
759
+ return font;
760
+ },
749
761
  color: function(value){ return JSColor.initFromDictionary(value); },
750
762
  boolean: function(value){ return typeof(value) === "boolean" ? value : undefined; },
751
763
  number: function(value){ return typeof(value) === "number" ? value : undefined; },
@@ -88,8 +88,7 @@ JSClass("JSContext", JSObject, {
88
88
 
89
89
  addQuadraticCurveToPoint: function(point, control){
90
90
  this.beginPathIfNeeded();
91
- // FIXME: not exactly a quadradic
92
- this.path.addCurveToPoint(point, control, point, this.state.transform);
91
+ this.path.addQuadraticCurveToPoint(point, control, this.state.transform);
93
92
  },
94
93
 
95
94
  addEllipseInRect: function(rect){
@@ -100,21 +99,17 @@ JSClass("JSContext", JSObject, {
100
99
  addPath: function(path){
101
100
  this.beginPathIfNeeded();
102
101
  var i, l;
103
- var j, k;
104
- var subpath;
105
- var segment;
106
- for (i = 0, l = path.subpaths.length; i < l; ++i){
107
- subpath = path.subpaths[i];
108
- this.moveToPoint(subpath.firstPoint.x, subpath.firstPoint.y);
109
- for (j = 0, k = subpath.segments.length; j < k; ++j){
110
- segment = subpath.segments[j];
111
- if (segment.type === JSPath.SegmentType.line){
112
- this.addLineToPoint(segment.end.x, segment.end.y);
113
- }else if (segment.type === JSPath.SegmentType.curve){
114
- this.addCurveToPoint(segment.curve.p2, segment.curve.cp1, segment.curve.cp2);
115
- }
116
- }
117
- if (subpath.closed){
102
+ var elements = path.elements;
103
+ var element;
104
+ for (i = 0, l = elements.length; i < l; ++i){
105
+ element = elements[i];
106
+ if (element.type === JSPathElement.Type.move){
107
+ this.moveToPoint(element.point.x, element.point.y);
108
+ }else if (element.type === JSPathElement.Type.line){
109
+ this.addLineToPoint(element.point.x, element.point.y);
110
+ }else if (element.type === JSPathElement.Type.cubicCurve){
111
+ this.addCurveToPoint(element.curve.p2, element.curve.cp1, element.curve.cp2);
112
+ }else if (element.type === JSPathElement.Type.close){
118
113
  this.closePath();
119
114
  }
120
115
  }
@@ -212,6 +207,12 @@ JSClass("JSContext", JSObject, {
212
207
  drawRadialGradient: function(gradient, rect){
213
208
  },
214
209
 
210
+ drawMaskedLinearGradient: function(gradient, rect, maskImage){
211
+ },
212
+
213
+ drawMaskedRadialGradient: function(gradient, rect, maskImage){
214
+ },
215
+
215
216
  drawLinearGradientStroke: function(gradient, path){
216
217
  // FIXME: gradient should be aligned to original path rect
217
218
  var strokePath = path.pathThatFillsStroke(this.state.lineWidth, this.state.lineCap, this.state.lineJoin, this.state.miterLimit);
@@ -282,6 +283,30 @@ JSClass("JSContext", JSObject, {
282
283
  this.state.shadowColor = color;
283
284
  },
284
285
 
286
+ // ----------------------------------------------------------------------
287
+ // MARK: - Transparency Layers
288
+
289
+ beginTransparencyLayer: function(){
290
+ this.save();
291
+ this.state.transparencyLayer = this.createTransparencyLayer();
292
+ this.setAlpha(1);
293
+ this.setShadow(JSPoint.Zero, 0, null);
294
+ },
295
+
296
+ endTransparencyLayer: function(){
297
+ if (this.state.transparencyLayer === null){
298
+ return;
299
+ }
300
+ this.drawTransparencyLayer(this.state.transparencyLayer);
301
+ this.restore();
302
+ },
303
+
304
+ createTransparencyLayer: function(){
305
+ },
306
+
307
+ drawTransparencyLayer: function(transparencyLayer){
308
+ },
309
+
285
310
  // ----------------------------------------------------------------------
286
311
  // MARK: - Clipping
287
312
 
@@ -411,6 +436,8 @@ JSContext.State = {
411
436
  characterSpacing: 0,
412
437
  textDrawingMode: JSContext.TextDrawingMode.fill,
413
438
 
439
+ transparencyLayer: null
440
+
414
441
  };
415
442
 
416
443
  })();
@@ -19,6 +19,9 @@
19
19
 
20
20
  JSGlobalObject.JSCubicBezier = function(p1, cp1, cp2, p2){
21
21
  if (this === undefined){
22
+ if (p1 === null){
23
+ return null;
24
+ }
22
25
  return new JSCubicBezier(p1, cp1, cp2, p2);
23
26
  }
24
27
  if (p1 instanceof JSCubicBezier){
@@ -126,6 +129,56 @@ JSCubicBezier.prototype = {
126
129
  points.push(this.pointAtInterval(t[1]));
127
130
  }
128
131
  return points;
132
+ },
133
+
134
+ curvesBySplittingAtInterval: function(t){
135
+ var q0 = this.p1.interpolation(this.cp1, t);
136
+ var q1 = this.cp1.interpolation(this.cp2, t);
137
+ var q2 = this.cp2.interpolation(this.p2, t);
138
+ var r0 = q0.interpolation(q1, t);
139
+ var r1 = q1.interpolation(q2, t);
140
+ var p = r0.interpolation(r1, t);
141
+ var cp1 = this.p1.interpolation(this.cp1, t);
142
+ var cp2 = this.p2.interpolation(this.cp2, (1 - t));
143
+ return [
144
+ JSCubicBezier(this.p1, cp1, r0, p),
145
+ JSCubicBezier(p, r1, cp2, this.p2)
146
+ ];
147
+ },
148
+
149
+ pointsWithFlatness: function(flatness){
150
+ var points = [JSPoint(this.p1)];
151
+ var dt = 0.1;
152
+ var t = 0;
153
+ var p0 = this.p1;
154
+ var p;
155
+ var q;
156
+ var mid;
157
+ var delta;
158
+ var iterationCount = 0;
159
+ var acceptable;
160
+ while (t < 1 && iterationCount < 1000){
161
+ p = this.pointAtInterval(t + dt);
162
+ q = this.pointAtInterval(t + dt / 2);
163
+ mid = p0.interpolation(p, 0.5);
164
+ acceptable = q.distanceToPoint(mid) <= flatness;
165
+ if (acceptable){
166
+ points.push(p);
167
+ t += dt;
168
+ dt = Math.min(dt * 2, 1 - t);
169
+ p0 = p;
170
+ }else{
171
+ dt /= 2;
172
+ }
173
+ ++iterationCount;
174
+ }
175
+ return points;
129
176
  }
130
177
 
131
- };
178
+ };
179
+
180
+ JSCubicBezier.fromQuadradicBezier = function(p1, cp, p2){
181
+ var cp1 = p1.interpolation(cp, 2/3);
182
+ var cp2 = p2.interpolation(cp, 2/3);
183
+ return JSCubicBezier(p1, cp1, cp2, p2);
184
+ };
@@ -88,12 +88,6 @@ JSClass("JSFont", JSObject, {
88
88
 
89
89
  initFromDictionary: function(dictionary){
90
90
  var descriptor = JSFontDescriptor.descriptorWithFamily(dictionary.family, dictionary.weight, dictionary.style);
91
- if (descriptor === null){
92
- if (dictionary.url){
93
- descriptor = JSURLFontDescriptor.initWithURL(JSURL.initWithString(dictionary.url), dictionary.family, dictionary.weight, dictionary.style);
94
- JSFontDescriptor.registerDescriptor(descriptor);
95
- }
96
- }
97
91
  if (descriptor !== null){
98
92
  return JSFont.initWithDescriptor(descriptor, dictionary.size);
99
93
  }
@@ -102,7 +96,6 @@ JSClass("JSFont", JSObject, {
102
96
 
103
97
  dictionaryRepresentation: function(){
104
98
  return {
105
- url: this.descriptor instanceof JSURLFontDescriptor ? this._descriptor.url : undefined,
106
99
  family: this._descriptor.family,
107
100
  weight: this._descriptor.weight,
108
101
  style: this._descriptor.style,
@@ -164,8 +157,14 @@ JSClass("JSFont", JSObject, {
164
157
  },
165
158
 
166
159
  _calculateDisplayMetrics: function(){
167
- this._displayAscender = Math.round(this._ascender);
168
- this._displayDescender = Math.round(this._descender);
160
+ // Math.round always rounds towards +Infinity when a given number
161
+ // has a fractional part exactly equal to 0.5.
162
+ // So, 2.5 rounds to 3 but -2.5 rounds to -2.
163
+ //
164
+ // For the purposes of this calculation, we want negative
165
+ // values like -2.5 to round to -3.
166
+ this._displayAscender = this._ascender < 0 ? -Math.round(-this._ascender) : Math.round(this._ascender);
167
+ this._displayDescender = this._descender < 0 ? -Math.round(-this._descender) : Math.round(this._descender);
169
168
  this._displayLineHeight = this._displayAscender - this._displayDescender;
170
169
  },
171
170
 
@@ -142,7 +142,17 @@ JSClass('JSGradient', JSObject, {
142
142
  },
143
143
 
144
144
  addStop: function(position, color){
145
- this.stops.push({position: position, color: color});
145
+ var l = this.stops.length;
146
+ var stop = {position: position, color: color};
147
+ var i = l - 1;
148
+ for (; i >= 0; --i){
149
+ if (this.stops[i].position <= position){
150
+ break;
151
+ }
152
+ }
153
+ i++;
154
+ this.stops.splice(i, 0, stop);
155
+ return i;
146
156
  },
147
157
 
148
158
  cssString: function(size){
@@ -167,6 +177,19 @@ JSClass('JSGradient', JSObject, {
167
177
  return 'linear-gradient(%fdeg, %s)'.sprintf(angle, cssStops.join(', '));
168
178
  },
169
179
 
180
+ radialCSSString: function(size){
181
+ var cssStops = [];
182
+ var stop;
183
+ for (var i = 0, l = this.stops.length; i < l; ++i){
184
+ stop = this.stops[i];
185
+ cssStops.push('%s %f%%'.sprintf(stop.color.cssString(), stop.position * 100));
186
+ }
187
+ var start = JSPoint(this.start.x * size.width, this.start.y * size.height);
188
+ var end = JSPoint(this.end.x * size.width, this.end.y * size.height);
189
+ var d = start.distanceToPoint(end);
190
+ return 'radial-gradient(%fpx at %f%% %f%%, %s)'.sprintf(d, this.start.x * 100, this.start.y * 100, cssStops.join(', '));
191
+ },
192
+
170
193
  colorAtPosition: function(position){
171
194
  if (position <= this.stops[0].position){
172
195
  return this.stops[0].color;
@@ -229,7 +252,10 @@ JSClass('JSGradient', JSObject, {
229
252
  var gradient = JSGradient.init();
230
253
  gradient.start = transform.convertPointFromTransform(this.start);
231
254
  gradient.end = transform.convertPointFromTransform(this.end);
232
- gradient.stops = JSCopy(this.stops);
255
+ var i, l;
256
+ for (i = 0, l = this.stops.length; i < l; ++i){
257
+ gradient.addStop(this.stops[i].position, this.stops[i].color);
258
+ }
233
259
  return gradient;
234
260
  }
235
261
 
@@ -32,4 +32,30 @@ _JSURLImage.definePropertiesFromExtensions({
32
32
  htmlURLString: function(){
33
33
  return this.url.encodedString;
34
34
  }
35
- });
35
+ });
36
+
37
+ JSImage.definePropertiesFromExtensions({
38
+
39
+ _htmlImage: null,
40
+ _htmlImagePromise: null,
41
+
42
+ preloadForHTMLContexts: function(){
43
+ if (this._htmlImagePromise === null){
44
+ var image = this;
45
+ this._htmlImagePromise = new Promise(function(resolve, reject){
46
+ var htmlImage = new JSGlobalObject.Image();
47
+ htmlImage.src = image.htmlURLString();
48
+ htmlImage.onload = function(){
49
+ resolve(htmlImage);
50
+ };
51
+ htmlImage.onerror = function(){
52
+ reject(new Error("failed to load image"));
53
+ };
54
+ }).then(function(htmlImage){
55
+ image._htmlImage = htmlImage;
56
+ });
57
+ }
58
+ return this._htmlImagePromise;
59
+ }
60
+
61
+ });
@@ -560,6 +560,8 @@ JSClass("JSNumberFormatter", JSObject, {
560
560
  var canSeePercent = false;
561
561
  var canSeeGroup = false;
562
562
  var seenDigit = false;
563
+ var prefix = "";
564
+ var suffix = "";
563
565
  var stringMatches = function(substr){
564
566
  if (substr.length === 0){
565
567
  return false;
@@ -588,6 +590,7 @@ JSClass("JSNumberFormatter", JSObject, {
588
590
  sign = -1;
589
591
  canSeeSign = false;
590
592
  i += this.minusSign.length;
593
+ prefix += this.minusSign;
591
594
  }else if (canSeeSign && stringMatches(this.plusSign)){
592
595
  canSeeSign = false;
593
596
  i += this.plusSign.length;
@@ -625,8 +628,12 @@ JSClass("JSNumberFormatter", JSObject, {
625
628
  i += this.groupingSeparator.length;
626
629
  }else if (c == 0x20){
627
630
  ++i;
631
+ }else if (!seenDigit){
632
+ prefix += string[i];
633
+ ++i;
628
634
  }else{
629
- return null;
635
+ suffix += string[i];
636
+ ++i;
630
637
  }
631
638
  }
632
639
  if (!seenDigit){
@@ -639,6 +646,21 @@ JSClass("JSNumberFormatter", JSObject, {
639
646
  if (sign < 0){
640
647
  n = -n;
641
648
  }
649
+ if (n >= 0){
650
+ if (prefix !== "" && prefix !== this.positivePrefix){
651
+ return null;
652
+ }
653
+ if (suffix !== "" && suffix !== this.positiveSuffix){
654
+ return null;
655
+ }
656
+ }else{
657
+ if (prefix !== "" && prefix !== this.negativePrefix){
658
+ return null;
659
+ }
660
+ if (suffix !== "" && suffix !== this.negativeSuffix){
661
+ return null;
662
+ }
663
+ }
642
664
  return n;
643
665
  },
644
666
 
@@ -53,6 +53,40 @@ JSClass("JSPath", JSObject, {
53
53
  return true;
54
54
  },
55
55
 
56
+ elements: JSReadOnlyProperty(),
57
+
58
+ _elements: null,
59
+
60
+ _invalidateElements: function(){
61
+ this._elements = null;
62
+ },
63
+
64
+ getElements: function(){
65
+ if (this._elements === null){
66
+ this._elements = [];
67
+ var i, l, j, k;
68
+ var subpath, segment;
69
+ for (i = 0, l = this.subpaths.length; i < l; ++i){
70
+ subpath = this.subpaths[i];
71
+ this._elements.push(JSPathElement(JSPathElement.Type.move, subpath.firstPoint));
72
+ if (subpath.segments.length > 0){
73
+ for (j = 0, k = subpath.segments.length; j < k; ++j){
74
+ segment = subpath.segments[j];
75
+ if (segment.type == JSPath.SegmentType.line){
76
+ this._elements.push(JSPathElement(JSPathElement.Type.line, segment.end));
77
+ }else if (segment.type == JSPath.SegmentType.curve){
78
+ this._elements.push(JSPathElement(JSPathElement.Type.cubicCurve, segment.curve.p2, segment.curve.p1, segment.curve.cp1, segment.curve.cp2));
79
+ }
80
+ }
81
+ }
82
+ if (subpath.closed){
83
+ this._elements.push(JSPathElement(JSPathElement.Type.close));
84
+ }
85
+ }
86
+ }
87
+ return this._elements;
88
+ },
89
+
56
90
  subpaths: null,
57
91
  currentPoint: JSReadOnlyProperty('_currentPoint', null),
58
92
  currentSubpath: null,
@@ -68,6 +102,7 @@ JSClass("JSPath", JSObject, {
68
102
  this.subpaths.push(this.currentSubpath);
69
103
  this._currentPoint = point;
70
104
  this._invalidateBoundingRect();
105
+ this._invalidateElements();
71
106
  },
72
107
 
73
108
  addLineToPoint: function(point, transform){
@@ -80,6 +115,7 @@ JSClass("JSPath", JSObject, {
80
115
  this.currentSubpath.segments.push({type: JSPath.SegmentType.line, end: point});
81
116
  this._currentPoint = point;
82
117
  this._invalidateBoundingRect();
118
+ this._invalidateElements();
83
119
  },
84
120
 
85
121
  addCurveToPoint: function(point, control1, control2, transform){
@@ -93,6 +129,15 @@ JSClass("JSPath", JSObject, {
93
129
  this.currentSubpath.segments.push({type: JSPath.SegmentType.curve, curve: curve});
94
130
  this._currentPoint = point;
95
131
  this._invalidateBoundingRect();
132
+ this._invalidateElements();
133
+ },
134
+
135
+ addQuadraticCurveToPoint: function(point, control, transform){
136
+ if (transform !== undefined){
137
+ point = transform.convertPointFromTransform(point);
138
+ control = transform.convertPointFromTransform(control);
139
+ }
140
+ this.addCurveToPoint(point, this._currentPoint.interpolation(control, 2/3), point.interpolation(control, 2/3));
96
141
  },
97
142
 
98
143
  addRect: function(rect, transform){
@@ -328,7 +373,9 @@ JSClass("JSPath", JSObject, {
328
373
  if (this.subpaths.length === 0){
329
374
  this.moveToPoint(p1, transform);
330
375
  }else{
331
- this.addLineToPoint(p1, transform);
376
+ if (Math.abs(p1.x - this._currentPoint.x) > 0.00001 || Math.abs(p1.y - this._currentPoint.y) > 0.00001){
377
+ this.addLineToPoint(p1, transform);
378
+ }
332
379
  }
333
380
 
334
381
  // If there's no radius, then there's nothing left to do
@@ -512,10 +559,10 @@ JSClass("JSPath", JSObject, {
512
559
  // distance is clockwise or counter clockwise.
513
560
  // Note: we could swap the angles and always arc in a fixed direction,
514
561
  // but I think it makes more sense to always arc from p0 to p2.
515
- if (startAngle < 0){
562
+ if (startAngle <= 0){
516
563
  startAngle += TWO_PI;
517
564
  }
518
- if (endAngle < 0){
565
+ if (endAngle <= 0){
519
566
  endAngle += TWO_PI;
520
567
  }
521
568
  var clockwise;
@@ -524,7 +571,9 @@ JSClass("JSPath", JSObject, {
524
571
  }else{
525
572
  clockwise = endAngle - startAngle <= Math.PI;
526
573
  }
527
- this.moveToPoint(t1);
574
+ if (Math.abs(t1.x - this._currentPoint.x) > 0.00001 || Math.abs(t1.y - this._currentPoint.y) > 0.00001){
575
+ this.moveToPoint(t1);
576
+ }
528
577
  this.addArc(center, radius, startAngle, endAngle, clockwise);
529
578
  },
530
579
 
@@ -533,6 +582,25 @@ JSClass("JSPath", JSObject, {
533
582
  this._currentPoint = this.currentSubpath.firstPoint;
534
583
  this.currentSubpath.closed = true;
535
584
  this.currentSubpath = null;
585
+ this._invalidateElements();
586
+ }
587
+ },
588
+
589
+ addPath: function(path, transform){
590
+ var i, l;
591
+ var elements = path.elements;
592
+ var element;
593
+ for (i = 0, l = elements.length; i < l; ++i){
594
+ element = elements[i];
595
+ if (element.type === JSPathElement.Type.move){
596
+ this.moveToPoint(element.point, transform);
597
+ }else if (element.type === JSPathElement.Type.line){
598
+ this.addLineToPoint(element.point, transform);
599
+ }else if (element.type === JSPathElement.Type.cubicCurve){
600
+ this.addCurveToPoint(element.curve.p2, element.curve.cp1, element.curve.cp2, transform);
601
+ }else if (element.type === JSPathElement.Type.close){
602
+ this.closeSubpath();
603
+ }
536
604
  }
537
605
  },
538
606
 
@@ -841,6 +909,9 @@ JSClass("JSPath", JSObject, {
841
909
  intersectsPath: function(other, fillRule){
842
910
  var boundingRect = this.boundingRect;
843
911
  // no need to check if bounding rect don't intersect
912
+ if (boundingRect === null){
913
+ return false;
914
+ }
844
915
  if (!boundingRect.intersectsRect(other.boundingRect)){
845
916
  return false;
846
917
  }
@@ -989,8 +1060,59 @@ JSClass("JSPath", JSObject, {
989
1060
  return path;
990
1061
  },
991
1062
 
1063
+ pathWithFlatness: function(flatness){
1064
+ var path = JSPath.init();
1065
+ var i, l, j, k;
1066
+ var element;
1067
+ var elements = this.elements;
1068
+ var points;
1069
+ for (i = 0, l = elements.length; i < l; ++i){
1070
+ element = elements[i];
1071
+ if (element.type === JSPathElement.Type.move){
1072
+ path.moveToPoint(element.point);
1073
+ }else if (element.type === JSPathElement.Type.line){
1074
+ path.addLineToPoint(element.point);
1075
+ }else if (element.type === JSPathElement.Type.cubicCurve){
1076
+ points = element.curve.pointsWithFlatness(flatness);
1077
+ for (j = 1, k = points.length; j < k; ++j){
1078
+ path.addLineToPoint(points[j]);
1079
+ }
1080
+ }else if (element.type === JSPathElement.Type.close){
1081
+ path.closeSubpath();
1082
+ }
1083
+ }
1084
+ return path;
1085
+ },
1086
+
1087
+ outlineContainsPoint: function(point, tolerance){
1088
+ // Only works on an already flattened path (we don't check curves)
1089
+ var firstPoint = JSPoint.Zero;
1090
+ var currentPoint = JSPoint.firstPoint;
1091
+ var elements = this.elements;
1092
+ var element;
1093
+ var i, l;
1094
+ for (i = 0, l = elements.length; i < l; ++i){
1095
+ element = elements[i];
1096
+ if (element.type === JSPathElement.Type.move){
1097
+ firstPoint = element.point;
1098
+ currentPoint = firstPoint;
1099
+ }else if (element.type === JSPathElement.Type.line){
1100
+ if (currentPoint.lineToPointContainsPoint(element.point, point, tolerance)){
1101
+ return true;
1102
+ }
1103
+ currentPoint = element.point;
1104
+ }else if (element.type === JSPathElement.Type.close){
1105
+ if (currentPoint.lineToPointContainsPoint(firstPoint, point, tolerance)){
1106
+ return true;
1107
+ }
1108
+ currentPoint = firstPoint;
1109
+ }
1110
+ }
1111
+ return false;
1112
+ },
1113
+
992
1114
  initWithSVGPathData: function(svgPathData){
993
- // TODO: Q, q, T, t, A, a
1115
+ // TODO: T, t, A, a
994
1116
  //
995
1117
  // - All instructions are expressed as one character (e.g., a moveto
996
1118
  // is expressed as an M).
@@ -1161,7 +1283,7 @@ JSClass("JSPath", JSObject, {
1161
1283
  break;
1162
1284
  }
1163
1285
  if (point !== null){
1164
- point = point.adding(JSPoint(command.values[0], command.values[1]))
1286
+ point = point.adding(JSPoint(command.values[0], command.values[1]));
1165
1287
  }else{
1166
1288
  point = JSPoint(command.values[0], command.values[1]);
1167
1289
  }
@@ -1291,6 +1413,42 @@ JSClass("JSPath", JSObject, {
1291
1413
  subpath.segments.push(segment);
1292
1414
  point = curve.p2;
1293
1415
  }
1416
+ }else if (command.name === "Q"){
1417
+ if (command.values.length < 4){
1418
+ break;
1419
+ }
1420
+ if (subpath === null){
1421
+ subpath = Subpath(point);
1422
+ this.subpaths.push(subpath);
1423
+ }
1424
+ for (v = 0; v < command.values.length - 3; v += 4){
1425
+ curve = JSCubicBezier.fromQuadradicBezier(
1426
+ point,
1427
+ JSPoint(command.values[v], command.values[v + 1]),
1428
+ JSPoint(command.values[v + 2], command.values[v + 3])
1429
+ );
1430
+ segment = {type: JSPath.SegmentType.curve, curve: curve};
1431
+ subpath.segments.push(segment);
1432
+ point = curve.p2;
1433
+ }
1434
+ }else if (command.name === "q"){
1435
+ if (command.values.length < 4){
1436
+ break;
1437
+ }
1438
+ if (subpath === null){
1439
+ subpath = Subpath(point);
1440
+ this.subpaths.push(subpath);
1441
+ }
1442
+ for (v = 0; v < command.values.length - 3; v += 4){
1443
+ curve = JSCubicBezier.fromQuadradicBezier(
1444
+ point,
1445
+ point.adding(JSPoint(command.values[v], command.values[v + 1])),
1446
+ point.adding(JSPoint(command.values[v + 2], command.values[v + 3]))
1447
+ );
1448
+ segment = {type: JSPath.SegmentType.curve, curve: curve};
1449
+ subpath.segments.push(segment);
1450
+ point = curve.p2;
1451
+ }
1294
1452
  }else if (command.name === "S"){
1295
1453
  if (command.values.length < 4){
1296
1454
  break;
@@ -1502,6 +1660,34 @@ JSPath.Sides = {
1502
1660
  }
1503
1661
  };
1504
1662
 
1663
+ JSGlobalObject.JSPathElement = function(type, point, point1, point2, point3){
1664
+ if (this === undefined){
1665
+ if (type === null){
1666
+ return null;
1667
+ }
1668
+ return new JSPathElement(type, point, point1, point2, point3);
1669
+ }
1670
+ if (type instanceof JSPathElement){
1671
+ this.type = type.type;
1672
+ this.point = JSPoint(type.point);
1673
+ this.curve = JSCubicBezier(type.curve);
1674
+ }else{
1675
+ this.type = type;
1676
+ this.point = JSPoint(point || null);
1677
+ if (type === JSPathElement.Type.cubicCurve){
1678
+ this.curve = JSCubicBezier(point1, point2, point3, point);
1679
+ }
1680
+ }
1681
+ };
1682
+
1683
+ JSPathElement.Type = {
1684
+ move: 1,
1685
+ line: 2,
1686
+ cubicCurve: 3,
1687
+ close: 4
1688
+ };
1689
+
1690
+
1505
1691
  JSPath.Sides.minYmaxY = JSPath.Sides.minY | JSPath.Sides.maxY;
1506
1692
  JSPath.Sides.minXmaxX = JSPath.Sides.minX | JSPath.Sides.maxX;
1507
1693