js-draw 0.1.0 → 0.1.3
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 +12 -0
- package/README.md +2 -2
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.js +6 -3
- package/dist/src/EditorImage.d.ts +1 -1
- package/dist/src/EditorImage.js +6 -3
- package/dist/src/Pointer.d.ts +3 -2
- package/dist/src/Pointer.js +12 -3
- package/dist/src/SVGLoader.d.ts +11 -0
- package/dist/src/SVGLoader.js +113 -4
- package/dist/src/Viewport.d.ts +1 -1
- package/dist/src/Viewport.js +12 -2
- package/dist/src/components/AbstractComponent.d.ts +6 -0
- package/dist/src/components/AbstractComponent.js +11 -0
- package/dist/src/components/SVGGlobalAttributesObject.js +0 -1
- package/dist/src/components/Stroke.js +1 -1
- package/dist/src/components/Text.d.ts +30 -0
- package/dist/src/components/Text.js +109 -0
- package/dist/src/components/builders/FreehandLineBuilder.js +1 -1
- package/dist/src/components/localization.d.ts +1 -0
- package/dist/src/components/localization.js +1 -0
- package/dist/src/geometry/Mat33.d.ts +1 -0
- package/dist/src/geometry/Mat33.js +30 -0
- package/dist/src/geometry/Path.js +105 -67
- package/dist/src/geometry/Rect2.d.ts +2 -0
- package/dist/src/geometry/Rect2.js +25 -8
- package/dist/src/rendering/Display.js +4 -3
- package/dist/src/rendering/caching/CacheRecord.js +2 -1
- package/dist/src/rendering/caching/CacheRecordManager.js +2 -10
- package/dist/src/rendering/caching/RenderingCache.js +10 -4
- package/dist/src/rendering/caching/RenderingCacheNode.js +10 -3
- package/dist/src/rendering/caching/testUtils.js +1 -1
- package/dist/src/rendering/caching/types.d.ts +1 -0
- package/dist/src/rendering/renderers/AbstractRenderer.d.ts +7 -1
- package/dist/src/rendering/renderers/AbstractRenderer.js +13 -1
- package/dist/src/rendering/renderers/CanvasRenderer.d.ts +3 -0
- package/dist/src/rendering/renderers/CanvasRenderer.js +28 -8
- package/dist/src/rendering/renderers/DummyRenderer.d.ts +3 -0
- package/dist/src/rendering/renderers/DummyRenderer.js +5 -0
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +6 -2
- package/dist/src/rendering/renderers/SVGRenderer.js +50 -7
- package/dist/src/testing/loadExpectExtensions.js +1 -4
- package/dist/src/toolbar/HTMLToolbar.d.ts +2 -1
- package/dist/src/toolbar/HTMLToolbar.js +216 -154
- package/dist/src/toolbar/icons.d.ts +12 -0
- package/dist/src/toolbar/icons.js +197 -0
- package/dist/src/toolbar/localization.d.ts +4 -1
- package/dist/src/toolbar/localization.js +4 -1
- package/dist/src/toolbar/types.d.ts +4 -0
- package/dist/src/tools/PanZoom.d.ts +9 -6
- package/dist/src/tools/PanZoom.js +30 -21
- package/dist/src/tools/Pen.js +8 -3
- package/dist/src/tools/SelectionTool.js +1 -1
- package/dist/src/tools/TextTool.d.ts +29 -0
- package/dist/src/tools/TextTool.js +154 -0
- package/dist/src/tools/ToolController.d.ts +5 -5
- package/dist/src/tools/ToolController.js +10 -9
- package/dist/src/tools/localization.d.ts +3 -0
- package/dist/src/tools/localization.js +3 -0
- package/package.json +1 -1
- package/src/Editor.ts +7 -3
- package/src/EditorImage.ts +7 -3
- package/src/Pointer.ts +13 -4
- package/src/SVGLoader.ts +146 -5
- package/src/Viewport.ts +15 -3
- package/src/components/AbstractComponent.ts +16 -1
- package/src/components/SVGGlobalAttributesObject.ts +0 -1
- package/src/components/Stroke.ts +1 -1
- package/src/components/Text.ts +136 -0
- package/src/components/builders/FreehandLineBuilder.ts +1 -1
- package/src/components/localization.ts +2 -0
- package/src/geometry/Mat33.test.ts +44 -0
- package/src/geometry/Mat33.ts +41 -0
- package/src/geometry/Path.fromString.test.ts +94 -4
- package/src/geometry/Path.toString.test.ts +7 -3
- package/src/geometry/Path.ts +110 -68
- package/src/geometry/Rect2.test.ts +9 -0
- package/src/geometry/Rect2.ts +33 -8
- package/src/rendering/Display.ts +4 -3
- package/src/rendering/caching/CacheRecord.ts +2 -1
- package/src/rendering/caching/CacheRecordManager.ts +2 -12
- package/src/rendering/caching/RenderingCache.test.ts +1 -1
- package/src/rendering/caching/RenderingCache.ts +11 -4
- package/src/rendering/caching/RenderingCacheNode.ts +16 -3
- package/src/rendering/caching/testUtils.ts +1 -0
- package/src/rendering/caching/types.ts +4 -0
- package/src/rendering/renderers/AbstractRenderer.ts +18 -1
- package/src/rendering/renderers/CanvasRenderer.ts +34 -10
- package/src/rendering/renderers/DummyRenderer.ts +8 -0
- package/src/rendering/renderers/SVGRenderer.ts +57 -10
- package/src/testing/loadExpectExtensions.ts +1 -4
- package/src/toolbar/HTMLToolbar.ts +262 -170
- package/src/toolbar/icons.ts +226 -0
- package/src/toolbar/localization.ts +9 -2
- package/src/toolbar/toolbar.css +21 -8
- package/src/toolbar/types.ts +5 -0
- package/src/tools/PanZoom.ts +37 -27
- package/src/tools/Pen.ts +7 -3
- package/src/tools/SelectionTool.ts +1 -1
- package/src/tools/TextTool.ts +206 -0
- package/src/tools/ToolController.ts +7 -5
- package/src/tools/localization.ts +7 -0
@@ -0,0 +1,226 @@
|
|
1
|
+
import { ComponentBuilderFactory } from '../components/builders/types';
|
2
|
+
import { TextStyle } from '../components/Text';
|
3
|
+
import EventDispatcher from '../EventDispatcher';
|
4
|
+
import { Vec2 } from '../geometry/Vec2';
|
5
|
+
import SVGRenderer from '../rendering/renderers/SVGRenderer';
|
6
|
+
import Pen from '../tools/Pen';
|
7
|
+
import { StrokeDataPoint } from '../types';
|
8
|
+
import Viewport from '../Viewport';
|
9
|
+
|
10
|
+
const svgNamespace = 'http://www.w3.org/2000/svg';
|
11
|
+
const primaryForegroundFill = `
|
12
|
+
style='fill: var(--primary-foreground-color);'
|
13
|
+
`;
|
14
|
+
const primaryForegroundStrokeFill = `
|
15
|
+
style='fill: var(--primary-foreground-color); stroke: var(--primary-foreground-color);'
|
16
|
+
`;
|
17
|
+
|
18
|
+
export const makeUndoIcon = () => {
|
19
|
+
return makeRedoIcon(true);
|
20
|
+
};
|
21
|
+
|
22
|
+
export const makeRedoIcon = (mirror: boolean = false) => {
|
23
|
+
const icon = document.createElementNS(svgNamespace, 'svg');
|
24
|
+
icon.innerHTML = `
|
25
|
+
<style>
|
26
|
+
.toolbar-svg-undo-redo-icon {
|
27
|
+
stroke: var(--primary-foreground-color);
|
28
|
+
stroke-width: 12;
|
29
|
+
stroke-linejoin: round;
|
30
|
+
stroke-linecap: round;
|
31
|
+
fill: none;
|
32
|
+
|
33
|
+
transform-origin: center;
|
34
|
+
}
|
35
|
+
</style>
|
36
|
+
<path
|
37
|
+
d='M20,20 A15,15 0 0 1 70,80 L80,90 L60,70 L65,90 L87,90 L65,80'
|
38
|
+
class='toolbar-svg-undo-redo-icon'
|
39
|
+
style='${mirror ? 'transform: scale(-1, 1);' : ''}'/>
|
40
|
+
`;
|
41
|
+
icon.setAttribute('viewBox', '0 0 100 100');
|
42
|
+
return icon;
|
43
|
+
};
|
44
|
+
|
45
|
+
export const makeDropdownIcon = () => {
|
46
|
+
const icon = document.createElementNS(svgNamespace, 'svg');
|
47
|
+
icon.innerHTML = `
|
48
|
+
<g>
|
49
|
+
<path
|
50
|
+
d='M5,10 L50,90 L95,10 Z'
|
51
|
+
${primaryForegroundFill}
|
52
|
+
/>
|
53
|
+
</g>
|
54
|
+
`;
|
55
|
+
icon.setAttribute('viewBox', '0 0 100 100');
|
56
|
+
return icon;
|
57
|
+
};
|
58
|
+
|
59
|
+
export const makeEraserIcon = () => {
|
60
|
+
const icon = document.createElementNS(svgNamespace, 'svg');
|
61
|
+
|
62
|
+
// Draw an eraser-like shape
|
63
|
+
icon.innerHTML = `
|
64
|
+
<g>
|
65
|
+
<rect x=10 y=50 width=80 height=30 rx=10 fill='pink' />
|
66
|
+
<rect
|
67
|
+
x=10 y=10 width=80 height=50
|
68
|
+
${primaryForegroundFill}
|
69
|
+
/>
|
70
|
+
</g>
|
71
|
+
`;
|
72
|
+
icon.setAttribute('viewBox', '0 0 100 100');
|
73
|
+
return icon;
|
74
|
+
};
|
75
|
+
|
76
|
+
export const makeSelectionIcon = () => {
|
77
|
+
const icon = document.createElementNS(svgNamespace, 'svg');
|
78
|
+
|
79
|
+
// Draw a cursor-like shape
|
80
|
+
icon.innerHTML = `
|
81
|
+
<g>
|
82
|
+
<rect x=10 y=10 width=70 height=70 fill='pink' stroke='black'/>
|
83
|
+
<rect x=75 y=75 width=10 height=10 fill='white' stroke='black'/>
|
84
|
+
</g>
|
85
|
+
`;
|
86
|
+
icon.setAttribute('viewBox', '0 0 100 100');
|
87
|
+
|
88
|
+
return icon;
|
89
|
+
};
|
90
|
+
|
91
|
+
export const makeHandToolIcon = () => {
|
92
|
+
const icon = document.createElementNS(svgNamespace, 'svg');
|
93
|
+
|
94
|
+
// Draw a cursor-like shape
|
95
|
+
icon.innerHTML = `
|
96
|
+
<g>
|
97
|
+
<path d='
|
98
|
+
m 10,60
|
99
|
+
5,30
|
100
|
+
H 90
|
101
|
+
V 30
|
102
|
+
C 90,20 75,20 75,30
|
103
|
+
V 60
|
104
|
+
20
|
105
|
+
C 75,10 60,10 60,20
|
106
|
+
V 60
|
107
|
+
15
|
108
|
+
C 60,5 45,5 45,15
|
109
|
+
V 60
|
110
|
+
25
|
111
|
+
C 45,15 30,15 30,25
|
112
|
+
V 60
|
113
|
+
75
|
114
|
+
L 25,60
|
115
|
+
C 20,45 10,50 10,60
|
116
|
+
Z'
|
117
|
+
|
118
|
+
fill='none'
|
119
|
+
style='
|
120
|
+
stroke: var(--primary-foreground-color);
|
121
|
+
stroke-width: 2;
|
122
|
+
'
|
123
|
+
/>
|
124
|
+
</g>
|
125
|
+
`;
|
126
|
+
icon.setAttribute('viewBox', '0 0 100 100');
|
127
|
+
return icon;
|
128
|
+
};
|
129
|
+
|
130
|
+
export const makeTextIcon = (textStyle: TextStyle) => {
|
131
|
+
const icon = document.createElementNS(svgNamespace, 'svg');
|
132
|
+
icon.setAttribute('viewBox', '0 0 100 100');
|
133
|
+
|
134
|
+
const textNode = document.createElementNS(svgNamespace, 'text');
|
135
|
+
textNode.appendChild(document.createTextNode('T'));
|
136
|
+
|
137
|
+
textNode.style.fontFamily = textStyle.fontFamily;
|
138
|
+
textNode.style.fontWeight = textStyle.fontWeight ?? '';
|
139
|
+
textNode.style.fontVariant = textStyle.fontVariant ?? '';
|
140
|
+
textNode.style.fill = textStyle.renderingStyle.fill.toHexString();
|
141
|
+
|
142
|
+
textNode.style.textAnchor = 'middle';
|
143
|
+
textNode.setAttribute('x', '50');
|
144
|
+
textNode.setAttribute('y', '75');
|
145
|
+
textNode.style.fontSize = '65px';
|
146
|
+
|
147
|
+
icon.appendChild(textNode);
|
148
|
+
|
149
|
+
return icon;
|
150
|
+
};
|
151
|
+
|
152
|
+
export const makePenIcon = (tipThickness: number, color: string) => {
|
153
|
+
const icon = document.createElementNS(svgNamespace, 'svg');
|
154
|
+
icon.setAttribute('viewBox', '0 0 100 100');
|
155
|
+
|
156
|
+
const halfThickness = tipThickness / 2;
|
157
|
+
|
158
|
+
// Draw a pen-like shape
|
159
|
+
const primaryStrokeTipPath = `M14,63 L${50 - halfThickness},95 L${50 + halfThickness},90 L88,60 Z`;
|
160
|
+
const backgroundStrokeTipPath = `M14,63 L${50 - halfThickness},85 L${50 + halfThickness},83 L88,60 Z`;
|
161
|
+
icon.innerHTML = `
|
162
|
+
<defs>
|
163
|
+
<pattern
|
164
|
+
id='checkerboard'
|
165
|
+
viewBox='0,0,10,10'
|
166
|
+
width='20%'
|
167
|
+
height='20%'
|
168
|
+
patternUnits='userSpaceOnUse'
|
169
|
+
>
|
170
|
+
<rect x=0 y=0 width=10 height=10 fill='white'/>
|
171
|
+
<rect x=0 y=0 width=5 height=5 fill='gray'/>
|
172
|
+
<rect x=5 y=5 width=5 height=5 fill='gray'/>
|
173
|
+
</pattern>
|
174
|
+
</defs>
|
175
|
+
<g>
|
176
|
+
<!-- Pen grip -->
|
177
|
+
<path
|
178
|
+
d='M10,10 L90,10 L90,60 L${50 + halfThickness},80 L${50 - halfThickness},80 L10,60 Z'
|
179
|
+
${primaryForegroundStrokeFill}
|
180
|
+
/>
|
181
|
+
</g>
|
182
|
+
<g>
|
183
|
+
<!-- Checkerboard background for slightly transparent pens -->
|
184
|
+
<path d='${backgroundStrokeTipPath}' fill='url(#checkerboard)'/>
|
185
|
+
|
186
|
+
<!-- Actual pen tip -->
|
187
|
+
<path
|
188
|
+
d='${primaryStrokeTipPath}'
|
189
|
+
fill='${color}'
|
190
|
+
stroke='${color}'
|
191
|
+
/>
|
192
|
+
</g>
|
193
|
+
`;
|
194
|
+
return icon;
|
195
|
+
};
|
196
|
+
|
197
|
+
export const makeIconFromFactory = (pen: Pen, factory: ComponentBuilderFactory) => {
|
198
|
+
const toolThickness = pen.getThickness();
|
199
|
+
|
200
|
+
const nowTime = (new Date()).getTime();
|
201
|
+
const startPoint: StrokeDataPoint = {
|
202
|
+
pos: Vec2.of(10, 10),
|
203
|
+
width: toolThickness / 5,
|
204
|
+
color: pen.getColor(),
|
205
|
+
time: nowTime - 100,
|
206
|
+
};
|
207
|
+
const endPoint: StrokeDataPoint = {
|
208
|
+
pos: Vec2.of(90, 90),
|
209
|
+
width: toolThickness / 5,
|
210
|
+
color: pen.getColor(),
|
211
|
+
time: nowTime,
|
212
|
+
};
|
213
|
+
|
214
|
+
const viewport = new Viewport(new EventDispatcher());
|
215
|
+
const builder = factory(startPoint, viewport);
|
216
|
+
builder.addPoint(endPoint);
|
217
|
+
|
218
|
+
const icon = document.createElementNS(svgNamespace, 'svg');
|
219
|
+
icon.setAttribute('viewBox', '0 0 100 100');
|
220
|
+
viewport.updateScreenSize(Vec2.of(100, 100));
|
221
|
+
|
222
|
+
const renderer = new SVGRenderer(icon, viewport);
|
223
|
+
builder.preview(renderer);
|
224
|
+
|
225
|
+
return icon;
|
226
|
+
};
|
@@ -1,6 +1,8 @@
|
|
1
1
|
|
2
2
|
|
3
3
|
export interface ToolbarLocalization {
|
4
|
+
anyDevicePanning: string;
|
5
|
+
touchPanning: string;
|
4
6
|
outlinedRectanglePen: string;
|
5
7
|
filledRectanglePen: string;
|
6
8
|
linePen: string;
|
@@ -11,7 +13,7 @@ export interface ToolbarLocalization {
|
|
11
13
|
pen: string;
|
12
14
|
eraser: string;
|
13
15
|
select: string;
|
14
|
-
|
16
|
+
handTool: string;
|
15
17
|
thicknessLabel: string;
|
16
18
|
resizeImageToSelection: string;
|
17
19
|
deleteSelection: string;
|
@@ -20,13 +22,14 @@ export interface ToolbarLocalization {
|
|
20
22
|
|
21
23
|
dropdownShown: (toolName: string)=>string;
|
22
24
|
dropdownHidden: (toolName: string)=>string;
|
25
|
+
zoomLevel: (zoomPercentage: number)=> string;
|
23
26
|
}
|
24
27
|
|
25
28
|
export const defaultToolbarLocalization: ToolbarLocalization = {
|
26
29
|
pen: 'Pen',
|
27
30
|
eraser: 'Eraser',
|
28
31
|
select: 'Select',
|
29
|
-
|
32
|
+
handTool: 'Pan',
|
30
33
|
thicknessLabel: 'Thickness: ',
|
31
34
|
colorLabel: 'Color: ',
|
32
35
|
resizeImageToSelection: 'Resize image to selection',
|
@@ -35,6 +38,9 @@ export const defaultToolbarLocalization: ToolbarLocalization = {
|
|
35
38
|
redo: 'Redo',
|
36
39
|
selectObjectType: 'Object type: ',
|
37
40
|
|
41
|
+
touchPanning: 'Touchscreen panning',
|
42
|
+
anyDevicePanning: 'Any device panning',
|
43
|
+
|
38
44
|
freehandPen: 'Freehand',
|
39
45
|
arrowPen: 'Arrow',
|
40
46
|
linePen: 'Line',
|
@@ -43,4 +49,5 @@ export const defaultToolbarLocalization: ToolbarLocalization = {
|
|
43
49
|
|
44
50
|
dropdownShown: (toolName) => `Dropdown for ${toolName} shown`,
|
45
51
|
dropdownHidden: (toolName) => `Dropdown for ${toolName} hidden`,
|
52
|
+
zoomLevel: (zoomPercent: number) => `Zoom: ${zoomPercent}%`,
|
46
53
|
};
|
package/src/toolbar/toolbar.css
CHANGED
@@ -32,6 +32,7 @@
|
|
32
32
|
|
33
33
|
min-width: 40px;
|
34
34
|
max-width: 70px;
|
35
|
+
width: min-content;
|
35
36
|
font-size: 11pt;
|
36
37
|
|
37
38
|
cursor: pointer;
|
@@ -49,16 +50,12 @@
|
|
49
50
|
box-shadow: 0px 2px 4px var(--primary-foreground-color);
|
50
51
|
}
|
51
52
|
|
52
|
-
.toolbar-root button {
|
53
|
-
height: auto;
|
54
|
-
}
|
55
|
-
|
56
53
|
.toolbar-root button:disabled {
|
57
54
|
cursor: inherit;
|
58
55
|
filter: opacity(0.5);
|
59
56
|
}
|
60
57
|
|
61
|
-
.toolbar-
|
58
|
+
.toolbar-root .toolbar-icon {
|
62
59
|
flex-shrink: 1;
|
63
60
|
min-width: 30px;
|
64
61
|
}
|
@@ -68,11 +65,11 @@
|
|
68
65
|
color: var(--secondary-foreground-color);
|
69
66
|
}
|
70
67
|
|
71
|
-
.toolbar-toolContainer:not(.selected) .toolbar-showHideDropdownIcon {
|
68
|
+
.toolbar-toolContainer:not(.selected):not(.dropdownShowable) .toolbar-showHideDropdownIcon {
|
72
69
|
display: none;
|
73
70
|
}
|
74
71
|
|
75
|
-
.toolbar-toolContainer
|
72
|
+
.toolbar-toolContainer .toolbar-showHideDropdownIcon {
|
76
73
|
height: 10px;
|
77
74
|
transition: transform 0.5s ease;
|
78
75
|
}
|
@@ -81,7 +78,8 @@
|
|
81
78
|
transform: rotate(180deg);
|
82
79
|
}
|
83
80
|
|
84
|
-
.toolbar-dropdown.hidden,
|
81
|
+
.toolbar-dropdown.hidden,
|
82
|
+
.toolbar-toolContainer:not(.selected):not(.dropdownShowable) > .toolbar-dropdown {
|
85
83
|
display: none;
|
86
84
|
}
|
87
85
|
|
@@ -98,6 +96,7 @@
|
|
98
96
|
.toolbar-buttonGroup {
|
99
97
|
display: flex;
|
100
98
|
flex-direction: row;
|
99
|
+
justify-content: center;
|
101
100
|
}
|
102
101
|
|
103
102
|
.toolbar-closeColorPickerOverlay {
|
@@ -120,3 +119,17 @@
|
|
120
119
|
margin-left: 0;
|
121
120
|
margin-right: 0;
|
122
121
|
}
|
122
|
+
|
123
|
+
.toolbar-root .toolbar-zoomLevelEditor {
|
124
|
+
display: flex;
|
125
|
+
flex-direction: row;
|
126
|
+
}
|
127
|
+
|
128
|
+
.toolbar-root .toolbar-zoomLevelEditor .zoomDisplay {
|
129
|
+
flex-grow: 1;
|
130
|
+
}
|
131
|
+
|
132
|
+
.toolbar-root .toolbar-zoomLevelEditor button {
|
133
|
+
width: min-content;
|
134
|
+
height: min-content;
|
135
|
+
}
|
package/src/toolbar/types.ts
CHANGED
package/src/tools/PanZoom.ts
CHANGED
@@ -4,7 +4,7 @@ import Mat33 from '../geometry/Mat33';
|
|
4
4
|
import { Point2, Vec2 } from '../geometry/Vec2';
|
5
5
|
import Vec3 from '../geometry/Vec3';
|
6
6
|
import Pointer, { PointerDevice } from '../Pointer';
|
7
|
-
import { KeyPressEvent, PointerEvt, WheelEvt } from '../types';
|
7
|
+
import { EditorEventType, KeyPressEvent, PointerEvt, WheelEvt } from '../types';
|
8
8
|
import { Viewport } from '../Viewport';
|
9
9
|
import BaseTool from './BaseTool';
|
10
10
|
import { ToolType } from './ToolController';
|
@@ -17,18 +17,14 @@ interface PinchData {
|
|
17
17
|
}
|
18
18
|
|
19
19
|
export enum PanZoomMode {
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
TwoFingerGestures = 0x1 << 1,
|
25
|
-
|
26
|
-
// / Handle gestures from any device, rather than just touch
|
27
|
-
AnyDevice = 0x1 << 2,
|
20
|
+
OneFingerTouchGestures = 0x1,
|
21
|
+
TwoFingerTouchGestures = 0x1 << 1,
|
22
|
+
RightClickDrags = 0x1 << 2,
|
23
|
+
SinglePointerGestures = 0x1 << 3,
|
28
24
|
}
|
29
25
|
|
30
26
|
export default class PanZoom extends BaseTool {
|
31
|
-
public readonly kind: ToolType.PanZoom
|
27
|
+
public readonly kind: ToolType.PanZoom = ToolType.PanZoom;
|
32
28
|
private transform: Viewport.ViewportTransform|null = null;
|
33
29
|
|
34
30
|
private lastAngle: number;
|
@@ -37,10 +33,6 @@ export default class PanZoom extends BaseTool {
|
|
37
33
|
|
38
34
|
public constructor(private editor: Editor, private mode: PanZoomMode, description: string) {
|
39
35
|
super(editor.notifier, description);
|
40
|
-
|
41
|
-
if (mode === PanZoomMode.OneFingerGestures) {
|
42
|
-
this.kind = ToolType.TouchPanZoom;
|
43
|
-
}
|
44
36
|
}
|
45
37
|
|
46
38
|
// Returns information about the pointers in a gesture
|
@@ -54,25 +46,28 @@ export default class PanZoom extends BaseTool {
|
|
54
46
|
return { canvasCenter, screenCenter, angle, dist };
|
55
47
|
}
|
56
48
|
|
57
|
-
private
|
58
|
-
return
|
59
|
-
pointer => pointer.device === PointerDevice.Touch
|
60
|
-
);
|
49
|
+
private allPointersAreOfType(pointers: Pointer[], kind: PointerDevice) {
|
50
|
+
return pointers.every(pointer => pointer.device === kind);
|
61
51
|
}
|
62
52
|
|
63
|
-
public onPointerDown({ allPointers }: PointerEvt): boolean {
|
53
|
+
public onPointerDown({ allPointers: pointers }: PointerEvt): boolean {
|
64
54
|
let handlingGesture = false;
|
65
55
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
56
|
+
const allAreTouch = this.allPointersAreOfType(pointers, PointerDevice.Touch);
|
57
|
+
const isRightClick = this.allPointersAreOfType(pointers, PointerDevice.RightButtonMouse);
|
58
|
+
|
59
|
+
if (allAreTouch && pointers.length === 2 && this.mode & PanZoomMode.TwoFingerTouchGestures) {
|
60
|
+
const { screenCenter, angle, dist } = this.computePinchData(pointers[0], pointers[1]);
|
70
61
|
this.lastAngle = angle;
|
71
62
|
this.lastDist = dist;
|
72
63
|
this.lastScreenCenter = screenCenter;
|
73
64
|
handlingGesture = true;
|
74
|
-
} else if (
|
75
|
-
this.
|
65
|
+
} else if (pointers.length === 1 && (
|
66
|
+
(this.mode & PanZoomMode.OneFingerTouchGestures && allAreTouch)
|
67
|
+
|| (isRightClick && this.mode & PanZoomMode.RightClickDrags)
|
68
|
+
|| (this.mode & PanZoomMode.SinglePointerGestures)
|
69
|
+
)) {
|
70
|
+
this.lastScreenCenter = pointers[0].screenPos;
|
76
71
|
handlingGesture = true;
|
77
72
|
}
|
78
73
|
|
@@ -122,9 +117,9 @@ export default class PanZoom extends BaseTool {
|
|
122
117
|
this.transform ??= new Viewport.ViewportTransform(Mat33.identity);
|
123
118
|
|
124
119
|
const lastTransform = this.transform;
|
125
|
-
if (allPointers.length === 2
|
120
|
+
if (allPointers.length === 2) {
|
126
121
|
this.handleTwoFingerMove(allPointers);
|
127
|
-
} else if (allPointers.length === 1
|
122
|
+
} else if (allPointers.length === 1) {
|
128
123
|
this.handleOneFingerMove(allPointers[0]);
|
129
124
|
}
|
130
125
|
lastTransform.unapply(this.editor);
|
@@ -253,4 +248,19 @@ export default class PanZoom extends BaseTool {
|
|
253
248
|
|
254
249
|
return true;
|
255
250
|
}
|
251
|
+
|
252
|
+
public setMode(mode: PanZoomMode) {
|
253
|
+
if (mode !== this.mode) {
|
254
|
+
this.mode = mode;
|
255
|
+
|
256
|
+
this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
|
257
|
+
kind: EditorEventType.ToolUpdated,
|
258
|
+
tool: this,
|
259
|
+
});
|
260
|
+
}
|
261
|
+
}
|
262
|
+
|
263
|
+
public getMode(): PanZoomMode {
|
264
|
+
return this.mode;
|
265
|
+
}
|
256
266
|
}
|
package/src/tools/Pen.ts
CHANGED
@@ -91,9 +91,13 @@ export default class Pen extends BaseTool {
|
|
91
91
|
const stroke = this.builder.build();
|
92
92
|
this.previewStroke();
|
93
93
|
|
94
|
-
|
95
|
-
|
96
|
-
|
94
|
+
if (stroke.getBBox().area > 0) {
|
95
|
+
const canFlatten = true;
|
96
|
+
const action = new EditorImage.AddElementCommand(stroke, canFlatten);
|
97
|
+
this.editor.dispatch(action);
|
98
|
+
} else {
|
99
|
+
console.warn('Pen: Not adding empty stroke', stroke, 'to the canvas.');
|
100
|
+
}
|
97
101
|
}
|
98
102
|
this.builder = null;
|
99
103
|
this.editor.clearWetInk();
|
@@ -494,7 +494,7 @@ export default class SelectionTool extends BaseTool {
|
|
494
494
|
);
|
495
495
|
|
496
496
|
const selectionRect = this.selectionBox.region;
|
497
|
-
this.editor.viewport.zoomTo(selectionRect).apply(this.editor);
|
497
|
+
this.editor.viewport.zoomTo(selectionRect, false).apply(this.editor);
|
498
498
|
}
|
499
499
|
}
|
500
500
|
|
@@ -0,0 +1,206 @@
|
|
1
|
+
import Color4 from '../Color4';
|
2
|
+
import Text, { TextStyle } from '../components/Text';
|
3
|
+
import Editor from '../Editor';
|
4
|
+
import EditorImage from '../EditorImage';
|
5
|
+
import Mat33 from '../geometry/Mat33';
|
6
|
+
import { Vec2 } from '../geometry/Vec2';
|
7
|
+
import { PointerDevice } from '../Pointer';
|
8
|
+
import { EditorEventType, PointerEvt } from '../types';
|
9
|
+
import BaseTool from './BaseTool';
|
10
|
+
import { ToolLocalization } from './localization';
|
11
|
+
import { ToolType } from './ToolController';
|
12
|
+
|
13
|
+
const overlayCssClass = 'textEditorOverlay';
|
14
|
+
export default class TextTool extends BaseTool {
|
15
|
+
public kind: ToolType = ToolType.Text;
|
16
|
+
private textStyle: TextStyle;
|
17
|
+
|
18
|
+
private textEditOverlay: HTMLElement;
|
19
|
+
private textInputElem: HTMLInputElement|null = null;
|
20
|
+
private textTargetPosition: Vec2|null = null;
|
21
|
+
private textMeasuringCtx: CanvasRenderingContext2D|null = null;
|
22
|
+
|
23
|
+
public constructor(private editor: Editor, description: string, private localizationTable: ToolLocalization) {
|
24
|
+
super(editor.notifier, description);
|
25
|
+
this.textStyle = {
|
26
|
+
size: 32,
|
27
|
+
fontFamily: 'sans-serif',
|
28
|
+
renderingStyle: {
|
29
|
+
fill: Color4.purple,
|
30
|
+
},
|
31
|
+
};
|
32
|
+
|
33
|
+
this.textEditOverlay = document.createElement('div');
|
34
|
+
this.textEditOverlay.classList.add(overlayCssClass);
|
35
|
+
this.editor.addStyleSheet(`
|
36
|
+
.${overlayCssClass} {
|
37
|
+
height: 0;
|
38
|
+
overflow: visible;
|
39
|
+
}
|
40
|
+
|
41
|
+
.${overlayCssClass} input {
|
42
|
+
background-color: rgba(0, 0, 0, 0);
|
43
|
+
border: none;
|
44
|
+
padding: 0;
|
45
|
+
}
|
46
|
+
`);
|
47
|
+
this.editor.createHTMLOverlay(this.textEditOverlay);
|
48
|
+
this.editor.notifier.on(EditorEventType.ViewportChanged, () => this.updateTextInput());
|
49
|
+
}
|
50
|
+
|
51
|
+
private getTextAscent(text: string, style: TextStyle): number {
|
52
|
+
this.textMeasuringCtx ??= document.createElement('canvas').getContext('2d');
|
53
|
+
if (this.textMeasuringCtx) {
|
54
|
+
Text.applyTextStyles(this.textMeasuringCtx, style);
|
55
|
+
return this.textMeasuringCtx.measureText(text).actualBoundingBoxAscent;
|
56
|
+
}
|
57
|
+
|
58
|
+
// Estimate
|
59
|
+
return style.size * 2 / 3;
|
60
|
+
}
|
61
|
+
|
62
|
+
private flushInput() {
|
63
|
+
if (this.textInputElem && this.textTargetPosition) {
|
64
|
+
const content = this.textInputElem.value;
|
65
|
+
this.textInputElem.remove();
|
66
|
+
this.textInputElem = null;
|
67
|
+
|
68
|
+
if (content === '') {
|
69
|
+
return;
|
70
|
+
}
|
71
|
+
|
72
|
+
const textTransform = Mat33.translation(
|
73
|
+
this.textTargetPosition
|
74
|
+
).rightMul(
|
75
|
+
Mat33.scaling2D(this.editor.viewport.getSizeOfPixelOnCanvas())
|
76
|
+
);
|
77
|
+
|
78
|
+
const textComponent = new Text(
|
79
|
+
[ content ],
|
80
|
+
textTransform,
|
81
|
+
this.textStyle,
|
82
|
+
);
|
83
|
+
|
84
|
+
const action = new EditorImage.AddElementCommand(textComponent);
|
85
|
+
this.editor.dispatch(action);
|
86
|
+
}
|
87
|
+
}
|
88
|
+
|
89
|
+
private updateTextInput() {
|
90
|
+
if (!this.textInputElem || !this.textTargetPosition) {
|
91
|
+
this.textInputElem?.remove();
|
92
|
+
return;
|
93
|
+
}
|
94
|
+
|
95
|
+
const textScreenPos = this.editor.viewport.canvasToScreen(this.textTargetPosition);
|
96
|
+
this.textInputElem.type = 'text';
|
97
|
+
this.textInputElem.placeholder = this.localizationTable.enterTextToInsert;
|
98
|
+
this.textInputElem.style.fontFamily = this.textStyle.fontFamily;
|
99
|
+
this.textInputElem.style.fontVariant = this.textStyle.fontVariant ?? '';
|
100
|
+
this.textInputElem.style.fontWeight = this.textStyle.fontWeight ?? '';
|
101
|
+
this.textInputElem.style.fontSize = `${this.textStyle.size}px`;
|
102
|
+
this.textInputElem.style.color = this.textStyle.renderingStyle.fill.toHexString();
|
103
|
+
|
104
|
+
this.textInputElem.style.position = 'relative';
|
105
|
+
this.textInputElem.style.left = `${textScreenPos.x}px`;
|
106
|
+
const ascent = this.getTextAscent(this.textInputElem.value || 'W', this.textStyle);
|
107
|
+
this.textInputElem.style.top = `${textScreenPos.y - ascent}px`;
|
108
|
+
}
|
109
|
+
|
110
|
+
private startTextInput(textCanvasPos: Vec2, initialText: string) {
|
111
|
+
this.flushInput();
|
112
|
+
|
113
|
+
this.textInputElem = document.createElement('input');
|
114
|
+
this.textInputElem.value = initialText;
|
115
|
+
this.textTargetPosition = textCanvasPos;
|
116
|
+
this.updateTextInput();
|
117
|
+
|
118
|
+
this.textInputElem.oninput = () => {
|
119
|
+
if (this.textInputElem) {
|
120
|
+
this.textInputElem.size = this.textInputElem?.value.length || 10;
|
121
|
+
}
|
122
|
+
};
|
123
|
+
this.textInputElem.onblur = () => {
|
124
|
+
this.flushInput();
|
125
|
+
};
|
126
|
+
this.textInputElem.onkeyup = (evt) => {
|
127
|
+
if (evt.key === 'Enter') {
|
128
|
+
this.flushInput();
|
129
|
+
}
|
130
|
+
};
|
131
|
+
|
132
|
+
this.textEditOverlay.replaceChildren(this.textInputElem);
|
133
|
+
setTimeout(() => this.textInputElem!.focus(), 100);
|
134
|
+
}
|
135
|
+
|
136
|
+
public setEnabled(enabled: boolean) {
|
137
|
+
super.setEnabled(enabled);
|
138
|
+
|
139
|
+
if (!enabled) {
|
140
|
+
this.flushInput();
|
141
|
+
}
|
142
|
+
|
143
|
+
this.textEditOverlay.style.display = enabled ? 'block' : 'none';
|
144
|
+
}
|
145
|
+
|
146
|
+
public onPointerDown({ current, allPointers }: PointerEvt): boolean {
|
147
|
+
if (current.device === PointerDevice.Eraser) {
|
148
|
+
return false;
|
149
|
+
}
|
150
|
+
|
151
|
+
if (allPointers.length === 1) {
|
152
|
+
this.startTextInput(current.canvasPos, '');
|
153
|
+
return true;
|
154
|
+
}
|
155
|
+
|
156
|
+
return false;
|
157
|
+
}
|
158
|
+
|
159
|
+
private dispatchUpdateEvent() {
|
160
|
+
this.updateTextInput();
|
161
|
+
this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
|
162
|
+
kind: EditorEventType.ToolUpdated,
|
163
|
+
tool: this,
|
164
|
+
});
|
165
|
+
}
|
166
|
+
|
167
|
+
public setFontFamily(fontFamily: string) {
|
168
|
+
if (fontFamily !== this.textStyle.fontFamily) {
|
169
|
+
this.textStyle = {
|
170
|
+
...this.textStyle,
|
171
|
+
fontFamily: fontFamily,
|
172
|
+
};
|
173
|
+
|
174
|
+
this.dispatchUpdateEvent();
|
175
|
+
}
|
176
|
+
}
|
177
|
+
|
178
|
+
public setColor(color: Color4) {
|
179
|
+
if (!color.eq(this.textStyle.renderingStyle.fill)) {
|
180
|
+
this.textStyle = {
|
181
|
+
...this.textStyle,
|
182
|
+
renderingStyle: {
|
183
|
+
...this.textStyle.renderingStyle,
|
184
|
+
fill: color,
|
185
|
+
},
|
186
|
+
};
|
187
|
+
|
188
|
+
this.dispatchUpdateEvent();
|
189
|
+
}
|
190
|
+
}
|
191
|
+
|
192
|
+
public setFontSize(size: number) {
|
193
|
+
if (size !== this.textStyle.size) {
|
194
|
+
this.textStyle = {
|
195
|
+
...this.textStyle,
|
196
|
+
size,
|
197
|
+
};
|
198
|
+
|
199
|
+
this.dispatchUpdateEvent();
|
200
|
+
}
|
201
|
+
}
|
202
|
+
|
203
|
+
public getTextStyle(): TextStyle {
|
204
|
+
return this.textStyle;
|
205
|
+
}
|
206
|
+
}
|