paper 0.15.0 → 0.17.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 (64) hide show
  1. package/CHANGELOG.md +158 -0
  2. package/README.md +24 -16
  3. package/dist/docs/assets/js/paper.js +4811 -808
  4. package/dist/docs/classes/AreaText.html +771 -243
  5. package/dist/docs/classes/ChangeFlag.html +1 -1
  6. package/dist/docs/classes/Color.html +218 -21
  7. package/dist/docs/classes/CompoundPath.html +1174 -274
  8. package/dist/docs/classes/Curve.html +2 -2
  9. package/dist/docs/classes/CurveLocation.html +1 -1
  10. package/dist/docs/classes/DrawEvent.html +69 -0
  11. package/dist/docs/classes/Effect.html +434 -0
  12. package/dist/docs/classes/EffectInstance.html +304 -0
  13. package/dist/docs/classes/Event.html +1 -1
  14. package/dist/docs/classes/Gradient.html +1 -1
  15. package/dist/docs/classes/GradientStop.html +1 -1
  16. package/dist/docs/classes/Group.html +842 -243
  17. package/dist/docs/classes/HitResult.html +1 -1
  18. package/dist/docs/classes/HostItem.html +770 -242
  19. package/dist/docs/classes/Item.html +827 -242
  20. package/dist/docs/classes/Key.html +1 -1
  21. package/dist/docs/classes/KeyEvent.html +1 -1
  22. package/dist/docs/classes/Layer.html +784 -185
  23. package/dist/docs/classes/Matrix.html +1 -1
  24. package/dist/docs/classes/MouseEvent.html +1 -1
  25. package/dist/docs/classes/PaperScope.html +1 -1
  26. package/dist/docs/classes/PaperScript.html +4 -2
  27. package/dist/docs/classes/Path.html +1366 -352
  28. package/dist/docs/classes/PathItem.html +1198 -298
  29. package/dist/docs/classes/PathText.html +772 -244
  30. package/dist/docs/classes/PatternDefinition.html +367 -0
  31. package/dist/docs/classes/Point.html +33 -5
  32. package/dist/docs/classes/PointText.html +771 -243
  33. package/dist/docs/classes/Project.html +251 -9
  34. package/dist/docs/classes/Raster.html +770 -242
  35. package/dist/docs/classes/Rectangle.html +50 -8
  36. package/dist/docs/classes/Segment.html +79 -2
  37. package/dist/docs/classes/Shape.html +770 -242
  38. package/dist/docs/classes/Size.html +31 -3
  39. package/dist/docs/classes/StrokeProfileStop.html +346 -0
  40. package/dist/docs/classes/Style.html +67 -6
  41. package/dist/docs/classes/SymbolDefinition.html +28 -2
  42. package/dist/docs/classes/SymbolItem.html +807 -242
  43. package/dist/docs/classes/TextItem.html +771 -243
  44. package/dist/docs/classes/TextRange.html +1 -1
  45. package/dist/docs/classes/TextStory.html +1 -1
  46. package/dist/docs/classes/Tool.html +2 -2
  47. package/dist/docs/classes/ToolEvent.html +1 -1
  48. package/dist/docs/classes/Tween.html +1 -1
  49. package/dist/docs/classes/View.html +2 -2
  50. package/dist/docs/classes/global.html +1 -1
  51. package/dist/docs/classes/index.html +2 -2
  52. package/dist/docs/index.html +1 -1
  53. package/dist/node/browser.js +15 -0
  54. package/dist/paper-core.js +4701 -728
  55. package/dist/paper-core.min.js +3 -3
  56. package/dist/paper-full.js +4811 -808
  57. package/dist/paper-full.min.js +3 -3
  58. package/dist/paper-hosted.js +5389 -892
  59. package/dist/paper.d.ts +1058 -69
  60. package/examples/Paperjs.org/LiveEffects.html +366 -0
  61. package/examples/Paperjs.org/Offsetting.html +379 -0
  62. package/examples/Paperjs.org/StrokeProfiles.html +706 -0
  63. package/examples/Scripts/Patterns.html +57 -0
  64. package/package.json +21 -11
@@ -0,0 +1,706 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Stroke Profiles</title>
6
+ <link rel="stylesheet" href="../css/style.css">
7
+ <style>
8
+ body {
9
+ background: #f6f4f0;
10
+ font: 13px/1.4 -apple-system, 'Helvetica Neue', Helvetica, Arial, sans-serif;
11
+ color: #333;
12
+ }
13
+ .panel {
14
+ position: absolute;
15
+ top: 12px;
16
+ right: 12px;
17
+ width: 240px;
18
+ padding: 14px 16px;
19
+ background: rgba(255, 255, 255, 0.92);
20
+ border-radius: 12px;
21
+ box-shadow: 0 2px 16px rgba(0, 0, 0, 0.12);
22
+ }
23
+ .panel h1 {
24
+ margin: 0 0 10px;
25
+ font-size: 16px;
26
+ }
27
+ .panel label {
28
+ display: flex;
29
+ align-items: center;
30
+ justify-content: space-between;
31
+ gap: 8px;
32
+ margin: 6px 0;
33
+ }
34
+ .panel input[type=range] {
35
+ width: 120px;
36
+ }
37
+ .panel input[type=number] {
38
+ width: 64px;
39
+ }
40
+ .panel select {
41
+ width: 128px;
42
+ }
43
+ .panel .stop {
44
+ margin-top: 10px;
45
+ padding-top: 6px;
46
+ border-top: 1px solid #eee;
47
+ }
48
+ .panel .info {
49
+ margin-top: 10px;
50
+ color: #888;
51
+ font-size: 11px;
52
+ }
53
+ .panel a {
54
+ color: #e0306a;
55
+ }
56
+ </style>
57
+ <script type="text/javascript" src="../../dist/paper-full.js"></script>
58
+ <script type="text/paperscript" canvas="canvas">
59
+ var presets = {
60
+ 'Wave': function() {
61
+ var path = new Path([[100, 320], [260, 180], [420, 420],
62
+ [580, 180], [740, 320]]);
63
+ path.smooth({ type: 'catmull-rom' });
64
+ return path;
65
+ },
66
+ 'Spiral': function() {
67
+ var path = new Path();
68
+ for (var i = 0; i <= 24; i++) {
69
+ var angle = i * 30,
70
+ radius = 20 + i * 9;
71
+ path.add(new Point({ angle: angle, length: radius })
72
+ + [420, 300]);
73
+ }
74
+ path.smooth({ type: 'catmull-rom' });
75
+ return path;
76
+ },
77
+ 'Loop': function() {
78
+ return new Path('M120,380c180,-10 300,-220 220,-260c-80,-40 -150,120 -20,190c130,70 330,40 440,-120');
79
+ },
80
+ 'Circle': function() {
81
+ return new Path.Circle({ center: [420, 300], radius: 180 });
82
+ },
83
+ 'Star': function() {
84
+ return new Path.Star({ center: [420, 300], points: 6,
85
+ radius1: 90, radius2: 210 });
86
+ }
87
+ };
88
+
89
+ // The profiles, as stops at fractions of the path's length:
90
+ var profiles = {
91
+ 'Swell': [
92
+ { at: 0, width: 0.2 },
93
+ { at: 0.5, width: 1.6 },
94
+ { at: 1, width: 0.2 }
95
+ ],
96
+ 'Taper': [{ at: 0, width: 1.4 }, { at: 1, width: 0.05 }],
97
+ 'Brush': [
98
+ { at: 0, width: 0 },
99
+ { at: 0.12, width: 1.2 },
100
+ { at: 0.7, width: 0.8 },
101
+ { at: 1, width: 0 }
102
+ ],
103
+ 'Beads': [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1]
104
+ .map(function(at, i) {
105
+ return { at: at, width: i % 2 ? 1.4 : 0.4 };
106
+ }),
107
+ 'Steps': [
108
+ { at: 0, width: 0.5 },
109
+ { at: 0.33, width: 0.5 },
110
+ { at: 0.33, width: 1.2 },
111
+ { at: 0.66, width: 1.2 },
112
+ { at: 0.66, width: 0.3 },
113
+ { at: 1, width: 0.3 }
114
+ ],
115
+ 'Broad nib': [{ at: 0, width: 1.3, aspect: 0.12, angle: -35 }],
116
+ 'Pressure': [
117
+ { at: 0, width: 0.2, aspect: 0.15, angle: -35 },
118
+ { at: 0.3, width: 1.4 },
119
+ { at: 0.7, width: 1.1 },
120
+ { at: 1, width: 0.2 }
121
+ ],
122
+ 'Twist': [
123
+ { at: 0, width: 1.2, aspect: 0.15, angle: 0 },
124
+ { at: 1, angle: 360 }
125
+ ],
126
+ 'Uniform': []
127
+ };
128
+
129
+ var settings = {
130
+ preset: 'Wave',
131
+ profile: 'Pressure',
132
+ width: 60,
133
+ closed: false,
134
+ showStops: true,
135
+ showSelection: true,
136
+ selectionColor: '#00a0ff'
137
+ };
138
+
139
+ var envelope = new Group();
140
+ var path;
141
+ var drawing;
142
+ // What the mouse drags: 'width' (with the angle), 'aspect', 'stop',
143
+ // 'segment', 'handle-in', 'handle-out', or nothing:
144
+ var drag = null;
145
+ var selected = null;
146
+ var hover = null;
147
+ var isPanning = false;
148
+ var info = document.getElementById('info');
149
+ var inputs = {
150
+ width: document.getElementById('stop-width'),
151
+ aspect: document.getElementById('stop-aspect'),
152
+ angle: document.getElementById('stop-angle'),
153
+ offset: document.getElementById('stop-offset')
154
+ };
155
+ var smoothInput = document.getElementById('stop-smooth');
156
+ var colors = {
157
+ fill: '#ff6f91',
158
+ marker: '#1060ff',
159
+ ghost: 'rgba(16, 96, 255, 0.4)'
160
+ };
161
+
162
+ function createPreset(name) {
163
+ if (path)
164
+ path.remove();
165
+ path = presets[name]();
166
+ setupPath(path);
167
+ settings.closed = path.closed;
168
+ document.getElementById('closed').checked = path.closed;
169
+ applyProfile(settings.profile);
170
+ fitView();
171
+ }
172
+
173
+ function setupPath(path) {
174
+ // The stroke width is the base of the profile, and the path draws
175
+ // the area that its pens sweep, with a hairline of the path on top
176
+ // and the stops over its selection. Round caps and joins are the
177
+ // pens:
178
+ path.set({
179
+ strokeColor: colors.fill,
180
+ strokeWidth: settings.width,
181
+ strokeCap: 'round',
182
+ strokeJoin: 'round',
183
+ selectedColor: '#222',
184
+ onDrawSelection: drawStops
185
+ });
186
+ path.fullySelected = true;
187
+ }
188
+
189
+ function applyProfile(name) {
190
+ var length = path.length;
191
+ path.strokeProfile = profiles[name].map(function(entry) {
192
+ return {
193
+ offset: entry.at * length,
194
+ width: entry.width,
195
+ aspect: entry.aspect,
196
+ angle: entry.angle
197
+ };
198
+ });
199
+ select(null);
200
+ update();
201
+ }
202
+
203
+ function fitView() {
204
+ var bounds = path.bounds.expand(settings.width * 2 + 40),
205
+ size = view.viewSize - [280, 40];
206
+ view.zoom = 1;
207
+ view.zoom = Math.min(size.width / bounds.width,
208
+ size.height / bounds.height, 2);
209
+ view.center = bounds.center + [140 / view.zoom, 0];
210
+ update();
211
+ }
212
+
213
+ // The pen at the offset: its center, its half axes as vectors, and
214
+ // its values:
215
+ function getPen(offset, stop) {
216
+ var loc = path.getLocationAt(offset) || path.getLocationAt(
217
+ offset > 0 ? path.length : 0),
218
+ pen = stop || path.getStrokeProfileAt(offset),
219
+ radius = settings.width * pen.width / 2;
220
+ return {
221
+ point: loc.point,
222
+ major: new Point(radius, 0).rotate(pen.angle),
223
+ minor: new Point(0, radius * pen.aspect).rotate(pen.angle),
224
+ stop: pen
225
+ };
226
+ }
227
+
228
+ // Draws a hairline of the path over its stroke, and the curves of the
229
+ // stroke's outline when they're shown:
230
+ function drawEnvelope() {
231
+ envelope.removeChildren();
232
+ if (settings.showSelection) {
233
+ // The outline takes the path's attributes, including its
234
+ // selection, so select it on its own, fully with its handles:
235
+ var outline = path.outline({ insert: false });
236
+ outline.set({
237
+ fillColor: null,
238
+ selected: false,
239
+ fullySelected: true,
240
+ selectedColor: settings.selectionColor
241
+ });
242
+ envelope.addChild(outline);
243
+ }
244
+ var hairline = path.clone({ insert: false });
245
+ hairline.set({
246
+ strokeColor: '#222',
247
+ strokeWidth: 1,
248
+ strokeScaling: false,
249
+ strokeProfile: null,
250
+ selected: false,
251
+ onDrawSelection: null
252
+ });
253
+ envelope.addChild(hairline);
254
+ envelope.insertAbove(path);
255
+ }
256
+
257
+ // Draws the pen of a stop with the canvas context, in the pixels of
258
+ // the view: its ellipse, its center to move it, a handle at the end of
259
+ // its width to change it and the angle, and one at the end of its
260
+ // height to change the aspect. The values that the stop takes from the
261
+ // stops around it are faded:
262
+ function drawMarker(ctx, matrix, pen, stop, isSelected) {
263
+ var size = 9,
264
+ center = matrix.transform(pen.point),
265
+ major = matrix.transform(pen.point + pen.major) - center,
266
+ minor = matrix.transform(pen.point + pen.minor) - center,
267
+ angle = major.angle * Math.PI / 180;
268
+
269
+ function setStyle(key) {
270
+ var color = !stop || key && stop[key] == null ? colors.ghost
271
+ : colors.marker;
272
+ ctx.strokeStyle = color;
273
+ ctx.fillStyle = isSelected && (!key || stop[key] != null)
274
+ ? color : 'white';
275
+ }
276
+
277
+ function drawSquare(point, size, angle) {
278
+ ctx.save();
279
+ ctx.translate(point.x, point.y);
280
+ ctx.rotate(angle);
281
+ ctx.beginPath();
282
+ ctx.rect(-size / 2, -size / 2, size, size);
283
+ ctx.fill();
284
+ ctx.stroke();
285
+ ctx.restore();
286
+ }
287
+
288
+ ctx.lineWidth = 1.5;
289
+ setStyle('aspect');
290
+ ctx.setLineDash([4, 3]);
291
+ ctx.beginPath();
292
+ ctx.ellipse(center.x, center.y, major.length, minor.length, angle,
293
+ 0, 2 * Math.PI);
294
+ ctx.stroke();
295
+ ctx.setLineDash([]);
296
+ setStyle('angle');
297
+ ctx.beginPath();
298
+ ctx.moveTo(center.x - major.x, center.y - major.y);
299
+ ctx.lineTo(center.x + major.x, center.y + major.y);
300
+ ctx.stroke();
301
+ setStyle(null);
302
+ drawSquare(center, size, Math.PI / 4);
303
+ setStyle('width');
304
+ ctx.beginPath();
305
+ ctx.arc(center.x + major.x, center.y + major.y, 4.5, 0,
306
+ 2 * Math.PI);
307
+ ctx.fill();
308
+ ctx.stroke();
309
+ setStyle('aspect');
310
+ drawSquare(center + minor, size * 2 / 3, angle);
311
+ }
312
+
313
+ // Draws the stops over the selection of the path, beneath its handles,
314
+ // see Item#onDrawSelection:
315
+ function drawStops(event) {
316
+ var ctx = event.context,
317
+ matrix = event.matrix;
318
+ if (settings.showStops) {
319
+ (path.strokeProfile || []).forEach(function(stop) {
320
+ drawMarker(ctx, matrix, getPen(stop.offset,
321
+ getFullStop(stop)), stop, stop === selected);
322
+ });
323
+ }
324
+ if (hover)
325
+ drawMarker(ctx, matrix, getPen(hover.offset));
326
+ }
327
+
328
+ // The stop with the values that it leaves to the stops around it:
329
+ function getFullStop(stop) {
330
+ var pen = path.getStrokeProfileAt(stop.offset);
331
+ ['width', 'aspect', 'angle'].forEach(function(key) {
332
+ if (stop[key] != null)
333
+ pen[key] = stop[key];
334
+ });
335
+ return pen;
336
+ }
337
+
338
+ function update() {
339
+ var time = Date.now();
340
+ path.strokeWidth = settings.width;
341
+ path.closed = settings.closed;
342
+ // The path keeps its outline until it changes, and its stroke
343
+ // bounds measure it:
344
+ path.getStrokeBounds();
345
+ drawEnvelope();
346
+ info.textContent = (path.strokeProfile || []).length + ' stops, '
347
+ + path.curves.length + ' curves, outlined in '
348
+ + (Date.now() - time) + ' ms.';
349
+ updatePanel();
350
+ // The stops change without the items, so redraw them anyway:
351
+ view.update(true);
352
+ }
353
+
354
+ function select(stop) {
355
+ selected = stop;
356
+ updatePanel();
357
+ }
358
+
359
+ function updatePanel() {
360
+ var stop = selected && selected.path && getFullStop(selected);
361
+ smoothInput.disabled = !stop;
362
+ smoothInput.checked = !stop || selected.smooth;
363
+ for (var key in inputs) {
364
+ var input = inputs[key],
365
+ value = !stop ? ''
366
+ : key === 'width' || key === 'aspect'
367
+ ? Math.round(stop[key] * 100)
368
+ : Math.round(key === 'offset' ? selected.offset
369
+ : stop[key]),
370
+ // Values that the stop takes from the stops around it:
371
+ isBorrowed = stop && key !== 'offset'
372
+ && selected[key] == null;
373
+ input.disabled = !stop;
374
+ input.value = isBorrowed ? '' : value;
375
+ input.placeholder = isBorrowed ? value : '';
376
+ }
377
+ }
378
+
379
+ // Finds the stop or the handle of a stop at the point:
380
+ function hitStop(point) {
381
+ var tolerance = 8 / view.zoom,
382
+ stops = path.strokeProfile || [];
383
+ for (var i = stops.length - 1; i >= 0; i--) {
384
+ var stop = stops[i],
385
+ pen = getPen(stop.offset, getFullStop(stop));
386
+ if (point.getDistance(pen.point + pen.major) < tolerance)
387
+ return { type: 'width', stop: stop };
388
+ if (point.getDistance(pen.point + pen.minor) < tolerance)
389
+ return { type: 'aspect', stop: stop };
390
+ if (point.getDistance(pen.point) < tolerance)
391
+ return { type: 'stop', stop: stop };
392
+ }
393
+ return null;
394
+ }
395
+
396
+ // The location on the path within its stroke at the point, where a
397
+ // new stop would go:
398
+ function hitEnvelope(point) {
399
+ var hit = path.hitTest(point, {
400
+ stroke: true,
401
+ tolerance: 6 / view.zoom
402
+ });
403
+ return hit && hit.location;
404
+ }
405
+
406
+ function hitPath(point) {
407
+ return path.hitTest(point, {
408
+ segments: true,
409
+ handles: true,
410
+ tolerance: 6 / view.zoom
411
+ });
412
+ }
413
+
414
+ function onMouseMove(event) {
415
+ var previous = hover;
416
+ hover = null;
417
+ if (!isPanning && settings.showStops && !hitStop(event.point)
418
+ && !hitPath(event.point)) {
419
+ var loc = hitEnvelope(event.point);
420
+ if (loc)
421
+ hover = { offset: loc.offset };
422
+ }
423
+ if (hover || previous)
424
+ view.update(true);
425
+ }
426
+
427
+ function onMouseDown(event) {
428
+ drag = null;
429
+ hover = null;
430
+ if (isPanning)
431
+ return;
432
+ var hit = settings.showStops && hitStop(event.point);
433
+ if (hit) {
434
+ var stop = hit.stop;
435
+ if (hit.type === 'stop' && event.modifiers.alt) {
436
+ // Copy the stop and drag the copy, as in Illustrator:
437
+ stop = stop.clone();
438
+ path.strokeProfile = path.strokeProfile.concat(stop);
439
+ }
440
+ drag = { type: hit.type, stop: stop };
441
+ select(stop);
442
+ } else if ((hit = hitPath(event.point))) {
443
+ drag = {
444
+ type: hit.type,
445
+ segment: hit.segment
446
+ };
447
+ } else if (settings.showStops
448
+ && (hit = hitEnvelope(event.point))) {
449
+ // Dragging out from the stroke adds a stop there, with the
450
+ // width that the profile has there, and changes it:
451
+ var added = new StrokeProfileStop({
452
+ offset: hit.offset,
453
+ width: path.getStrokeProfileAt(hit.offset).width
454
+ });
455
+ path.strokeProfile = (path.strokeProfile || []).concat(added);
456
+ drag = { type: 'width', stop: added, keepAngle: true };
457
+ select(added);
458
+ } else {
459
+ select(null);
460
+ // Draw a new path by hand:
461
+ drawing = new Path({
462
+ segments: [event.point],
463
+ strokeColor: '#222',
464
+ strokeWidth: 1.5,
465
+ strokeScaling: false
466
+ });
467
+ }
468
+ update();
469
+ }
470
+
471
+ function onMouseDrag(event) {
472
+ if (isPanning) {
473
+ view.center -= event.delta;
474
+ // The view moved, so the event's next delta would be off:
475
+ event.point -= event.delta;
476
+ update();
477
+ } else if (drag) {
478
+ var stop = drag.stop;
479
+ if (drag.type === 'width') {
480
+ var pen = getPen(stop.offset, getFullStop(stop)),
481
+ vector = event.point - pen.point;
482
+ if (drag.keepAngle || event.modifiers.shift) {
483
+ // Only along the axis of the pen:
484
+ vector = vector.project(pen.major);
485
+ } else {
486
+ // Turn on from the current angle by the change in the
487
+ // direction, so that angles go on past 180° instead
488
+ // of wrapping around:
489
+ var angle = pen.stop.angle;
490
+ stop.angle = angle + ((vector.angle - angle) % 360
491
+ + 540) % 360 - 180;
492
+ }
493
+ stop.width = Math.round(2 * vector.length
494
+ / settings.width * 100) / 100;
495
+ } else if (drag.type === 'aspect') {
496
+ var pen = getPen(stop.offset, getFullStop(stop)),
497
+ height = (event.point - pen.point).project(pen.minor);
498
+ stop.aspect = Math.round(height.length
499
+ / pen.major.length * 100) / 100;
500
+ } else if (drag.type === 'stop') {
501
+ stop.offset = path.getNearestLocation(event.point).offset;
502
+ } else {
503
+ var name = {
504
+ 'segment': 'point',
505
+ 'handle-in': 'handleIn',
506
+ 'handle-out': 'handleOut'
507
+ }[drag.type];
508
+ if (name)
509
+ drag.segment[name] += event.delta;
510
+ }
511
+ update();
512
+ } else if (drawing) {
513
+ drawing.add(event.point);
514
+ }
515
+ }
516
+
517
+ function onMouseUp() {
518
+ drag = null;
519
+ if (drawing) {
520
+ if (drawing.segments.length > 2) {
521
+ drawing.simplify(10);
522
+ drawing.closed = settings.closed;
523
+ path.remove();
524
+ path = drawing;
525
+ setupPath(path);
526
+ applyProfile(settings.profile);
527
+ } else {
528
+ drawing.remove();
529
+ }
530
+ drawing = null;
531
+ }
532
+ update();
533
+ }
534
+
535
+ function onKeyDown(event) {
536
+ if (event.key === 'space') {
537
+ isPanning = true;
538
+ return false;
539
+ }
540
+ if ((event.key === 'backspace' || event.key === 'delete')
541
+ && selected && document.activeElement === document.body) {
542
+ path.strokeProfile = path.strokeProfile.filter(function(stop) {
543
+ return stop !== selected;
544
+ });
545
+ select(null);
546
+ update();
547
+ return false;
548
+ }
549
+ }
550
+
551
+ function onKeyUp(event) {
552
+ if (event.key === 'space')
553
+ isPanning = false;
554
+ }
555
+
556
+ document.getElementById('canvas').addEventListener('wheel',
557
+ function(event) {
558
+ var point = view.viewToProject(
559
+ DomEvent.getOffset(event, view.element));
560
+ view.scale(Math.pow(1.002, -event.deltaY), point);
561
+ event.preventDefault();
562
+ update();
563
+ });
564
+
565
+ function bind(id, key, convert) {
566
+ var input = document.getElementById(id);
567
+ input.addEventListener('input', function() {
568
+ settings[key] = convert ? convert(input) : input.value;
569
+ if (key === 'preset') {
570
+ createPreset(settings.preset);
571
+ } else if (key === 'profile') {
572
+ applyProfile(settings.profile);
573
+ } else {
574
+ update();
575
+ }
576
+ });
577
+ }
578
+
579
+ bind('preset', 'preset');
580
+ bind('profile', 'profile');
581
+ bind('width', 'width', function(input) { return +input.value; });
582
+ bind('closed', 'closed', function(input) { return input.checked; });
583
+ bind('show-stops', 'showStops', function(input) {
584
+ return input.checked;
585
+ });
586
+ bind('show-selection', 'showSelection', function(input) {
587
+ return input.checked;
588
+ });
589
+ bind('selection-color', 'selectionColor');
590
+
591
+
592
+ Object.keys(inputs).forEach(function(key) {
593
+ var input = inputs[key];
594
+ input.addEventListener('input', function() {
595
+ if (selected && input.value !== '') {
596
+ var value = +input.value;
597
+ selected[key] = key === 'width' || key === 'aspect'
598
+ ? value / 100 : value;
599
+ update();
600
+ }
601
+ });
602
+ });
603
+
604
+ // Logs what it takes to outline the path again, and copies it:
605
+ smoothInput.addEventListener('input', function() {
606
+ if (selected) {
607
+ selected.smooth = smoothInput.checked;
608
+ update();
609
+ }
610
+ });
611
+
612
+ document.getElementById('log').addEventListener('click', function() {
613
+ var json = JSON.stringify({
614
+ // In full precision, as small differences can matter:
615
+ pathData: path.getPathData(null, 15),
616
+ closed: path.closed,
617
+ strokeWidth: path.strokeWidth,
618
+ strokeCap: path.strokeCap,
619
+ strokeJoin: path.strokeJoin,
620
+ strokeProfile: (path.strokeProfile || []).map(function(stop) {
621
+ var entry = { offset: stop.offset };
622
+ ['width', 'aspect', 'angle'].forEach(function(key) {
623
+ if (stop[key] != null)
624
+ entry[key] = stop[key];
625
+ });
626
+ if (!stop.smooth)
627
+ entry.smooth = false;
628
+ return entry;
629
+ })
630
+ });
631
+ console.log(json);
632
+ // Copying needs permission, which the log doesn't:
633
+ if (navigator.clipboard)
634
+ navigator.clipboard.writeText(json).catch(function() {});
635
+ });
636
+
637
+ document.getElementById('export').addEventListener('click', function() {
638
+ var json = path.exportJSON();
639
+ this.href = URL.createObjectURL(new Blob([json],
640
+ { type: 'application/json' }));
641
+ });
642
+
643
+ createPreset(settings.preset);
644
+ </script>
645
+ </head>
646
+ <body>
647
+ <canvas id="canvas" resize></canvas>
648
+ <div class="panel">
649
+ <h1>Stroke Profiles</h1>
650
+ <label>Path
651
+ <select id="preset">
652
+ <option>Wave</option>
653
+ <option>Spiral</option>
654
+ <option>Loop</option>
655
+ <option>Circle</option>
656
+ <option>Star</option>
657
+ </select>
658
+ </label>
659
+ <label>Profile
660
+ <select id="profile">
661
+ <option>Pressure</option>
662
+ <option>Broad nib</option>
663
+ <option>Twist</option>
664
+ <option>Swell</option>
665
+ <option>Taper</option>
666
+ <option>Brush</option>
667
+ <option>Beads</option>
668
+ <option>Steps</option>
669
+ <option>Uniform</option>
670
+ </select>
671
+ </label>
672
+ <label>Stroke width <input id="width" type="range" min="4" max="160" value="60"></label>
673
+ <label>Closed <input id="closed" type="checkbox"></label>
674
+ <label>Stops <input id="show-stops" type="checkbox" checked></label>
675
+ <label>Outline <input id="show-selection" type="checkbox" checked></label>
676
+ <label>Selection <input id="selection-color" type="color" value="#00a0ff"></label>
677
+ <div class="stop">
678
+ <label>Width % <input id="stop-width" type="number" min="0" step="5" disabled></label>
679
+ <label>Aspect % <input id="stop-aspect" type="number" min="0" step="5" disabled></label>
680
+ <label>Angle ° <input id="stop-angle" type="number" step="5" disabled></label>
681
+ <label>Offset <input id="stop-offset" type="number" min="0" step="1" disabled></label>
682
+ <label>Smooth <input id="stop-smooth" type="checkbox" checked disabled></label>
683
+ </div>
684
+ <div class="info">
685
+ <p id="info"></p>
686
+ <p>Drag out from the stroke to add a stop. Drag the round handle
687
+ of a stop to change its width and angle, with Shift only its
688
+ width, the square one to change its aspect, and the stop itself
689
+ to move it along the path. Alt-drag copies a stop, Delete removes
690
+ the selected one. Drag the path's points and handles: the stops
691
+ stay between the same segments. Draw a new path in the empty
692
+ space. Zoom with the scroll wheel, pan with the space bar
693
+ held.</p>
694
+ <p>Faded parts of a stop are values it takes from the stops
695
+ around it, and grey values in the fields too; changing them makes
696
+ them its own.</p>
697
+ <p>The path draws the area that its pens sweep, the same as its
698
+ <code>outline()</code>, and the stops are drawn over its selection
699
+ through <code>onDrawSelection</code>.</p>
700
+ <p><button id="log">Log path</button> logs the path and its
701
+ profile to the console, and copies them.</p>
702
+ <a id="export" href="#" download="StrokeProfile.json">Export JSON</a>
703
+ </div>
704
+ </div>
705
+ </body>
706
+ </html>