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.
- package/CHANGELOG.md +158 -0
- package/README.md +24 -16
- package/dist/docs/assets/js/paper.js +4811 -808
- package/dist/docs/classes/AreaText.html +771 -243
- package/dist/docs/classes/ChangeFlag.html +1 -1
- package/dist/docs/classes/Color.html +218 -21
- package/dist/docs/classes/CompoundPath.html +1174 -274
- package/dist/docs/classes/Curve.html +2 -2
- package/dist/docs/classes/CurveLocation.html +1 -1
- package/dist/docs/classes/DrawEvent.html +69 -0
- package/dist/docs/classes/Effect.html +434 -0
- package/dist/docs/classes/EffectInstance.html +304 -0
- package/dist/docs/classes/Event.html +1 -1
- package/dist/docs/classes/Gradient.html +1 -1
- package/dist/docs/classes/GradientStop.html +1 -1
- package/dist/docs/classes/Group.html +842 -243
- package/dist/docs/classes/HitResult.html +1 -1
- package/dist/docs/classes/HostItem.html +770 -242
- package/dist/docs/classes/Item.html +827 -242
- package/dist/docs/classes/Key.html +1 -1
- package/dist/docs/classes/KeyEvent.html +1 -1
- package/dist/docs/classes/Layer.html +784 -185
- package/dist/docs/classes/Matrix.html +1 -1
- package/dist/docs/classes/MouseEvent.html +1 -1
- package/dist/docs/classes/PaperScope.html +1 -1
- package/dist/docs/classes/PaperScript.html +4 -2
- package/dist/docs/classes/Path.html +1366 -352
- package/dist/docs/classes/PathItem.html +1198 -298
- package/dist/docs/classes/PathText.html +772 -244
- package/dist/docs/classes/PatternDefinition.html +367 -0
- package/dist/docs/classes/Point.html +33 -5
- package/dist/docs/classes/PointText.html +771 -243
- package/dist/docs/classes/Project.html +251 -9
- package/dist/docs/classes/Raster.html +770 -242
- package/dist/docs/classes/Rectangle.html +50 -8
- package/dist/docs/classes/Segment.html +79 -2
- package/dist/docs/classes/Shape.html +770 -242
- package/dist/docs/classes/Size.html +31 -3
- package/dist/docs/classes/StrokeProfileStop.html +346 -0
- package/dist/docs/classes/Style.html +67 -6
- package/dist/docs/classes/SymbolDefinition.html +28 -2
- package/dist/docs/classes/SymbolItem.html +807 -242
- package/dist/docs/classes/TextItem.html +771 -243
- package/dist/docs/classes/TextRange.html +1 -1
- package/dist/docs/classes/TextStory.html +1 -1
- package/dist/docs/classes/Tool.html +2 -2
- package/dist/docs/classes/ToolEvent.html +1 -1
- package/dist/docs/classes/Tween.html +1 -1
- package/dist/docs/classes/View.html +2 -2
- package/dist/docs/classes/global.html +1 -1
- package/dist/docs/classes/index.html +2 -2
- package/dist/docs/index.html +1 -1
- package/dist/node/browser.js +15 -0
- package/dist/paper-core.js +4701 -728
- package/dist/paper-core.min.js +3 -3
- package/dist/paper-full.js +4811 -808
- package/dist/paper-full.min.js +3 -3
- package/dist/paper-hosted.js +5389 -892
- package/dist/paper.d.ts +1058 -69
- package/examples/Paperjs.org/LiveEffects.html +366 -0
- package/examples/Paperjs.org/Offsetting.html +379 -0
- package/examples/Paperjs.org/StrokeProfiles.html +706 -0
- package/examples/Scripts/Patterns.html +57 -0
- 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>
|