cm-chessboard 8.7.8 → 8.7.10
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/assets/extensions/arrows/arrows.css +18 -0
- package/assets/extensions/markers/markers.css +11 -1
- package/examples/extensions/right-click-annotator.html +29 -0
- package/index.html +1 -0
- package/package.json +1 -1
- package/src/extensions/arrows/Arrows.js +28 -5
- package/src/extensions/right-click-annotator/RightClickAnnotator.js +251 -0
|
@@ -28,4 +28,22 @@
|
|
|
28
28
|
stroke-linecap: round;
|
|
29
29
|
opacity: 0.5; }
|
|
30
30
|
|
|
31
|
+
/* custom colors for RightClickAnnotator */
|
|
32
|
+
.cm-chessboard .arrow-green .arrow-head {
|
|
33
|
+
fill: #2e7d32; /* green */
|
|
34
|
+
fill-rule: nonzero;
|
|
35
|
+
fill-opacity: 1; }
|
|
36
|
+
.cm-chessboard .arrow-green .arrow-line {
|
|
37
|
+
stroke: #2e7d32;
|
|
38
|
+
stroke-linecap: round;
|
|
39
|
+
opacity: 0.5; }
|
|
40
|
+
.cm-chessboard .arrow-orange .arrow-head {
|
|
41
|
+
fill: #ff9800; /* orange */
|
|
42
|
+
fill-rule: nonzero;
|
|
43
|
+
fill-opacity: 1; }
|
|
44
|
+
.cm-chessboard .arrow-orange .arrow-line {
|
|
45
|
+
stroke: #ff9800;
|
|
46
|
+
stroke-linecap: round;
|
|
47
|
+
opacity: 0.5; }
|
|
48
|
+
|
|
31
49
|
/*# sourceMappingURL=arrows.css.map */
|
|
@@ -34,4 +34,14 @@
|
|
|
34
34
|
fill: black;
|
|
35
35
|
opacity: 0.2; }
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
/* custom colors for RightClickAnnotator */
|
|
38
|
+
.cm-chessboard .markers .marker.marker-circle-green {
|
|
39
|
+
stroke: #2e7d32; /* green */
|
|
40
|
+
stroke-width: 3px;
|
|
41
|
+
opacity: 0.4; }
|
|
42
|
+
.cm-chessboard .markers .marker.marker-circle-orange {
|
|
43
|
+
stroke: #ff9800; /* orange */
|
|
44
|
+
stroke-width: 3px;
|
|
45
|
+
opacity: 0.4; }
|
|
46
|
+
|
|
47
|
+
/*# sourceMappingURL=markers.css.map */
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>cm-chessboard</title>
|
|
6
|
+
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1.0"/>
|
|
7
|
+
<link rel="stylesheet" href="../styles/examples.css"/>
|
|
8
|
+
<link rel="stylesheet" href="../../assets/chessboard.css"/>
|
|
9
|
+
<link rel="stylesheet" href="../../assets/extensions/markers/markers.css"/>
|
|
10
|
+
<link rel="stylesheet" href="../../assets/extensions/arrows/arrows.css"/>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<h1><a href="../..">cm-chessboard</a></h1>
|
|
14
|
+
<h2>Example: RightClickAnnotator extension</h2>
|
|
15
|
+
<p>Right-click to toggle green markers. Right-click + drag to draw green arrows. Use Alt for blue, Shift for red, Shift+Alt for orange.</p>
|
|
16
|
+
<div class="board" id="board"></div>
|
|
17
|
+
<script type="module">
|
|
18
|
+
import {Chessboard} from "../../src/Chessboard.js"
|
|
19
|
+
import {FEN} from "../../src/model/Position.js"
|
|
20
|
+
import {RightClickAnnotator} from "../../src/extensions/right-click-annotator/RightClickAnnotator.js"
|
|
21
|
+
|
|
22
|
+
const board = new Chessboard(document.getElementById("board"), {
|
|
23
|
+
position: FEN.start,
|
|
24
|
+
assetsUrl: "../../assets/",
|
|
25
|
+
extensions: [{class: RightClickAnnotator}]
|
|
26
|
+
})
|
|
27
|
+
</script>
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
package/index.html
CHANGED
|
@@ -33,6 +33,7 @@ It works, it is cool and it is easy to use. 👍</p>
|
|
|
33
33
|
<li><a href="examples/extensions/html-layer-extension.html">HTML Layer extension</a></li>
|
|
34
34
|
<li><a href="examples/extensions/promotion-dialog-extension.html">PromotionDialog extension</a></li>
|
|
35
35
|
<li><a href="examples/extensions/accessibility-extension.html">Accessibility extension</a></li>
|
|
36
|
+
<li><a href="examples/extensions/right-click-annotator.html">RightClickAnnotator extension</a></li>
|
|
36
37
|
</ul>
|
|
37
38
|
</div>
|
|
38
39
|
<h2>Chessboard</h2>
|
package/package.json
CHANGED
|
@@ -23,7 +23,13 @@ export class Arrows extends Extension {
|
|
|
23
23
|
this.onRedrawBoard()
|
|
24
24
|
})
|
|
25
25
|
this.props = {
|
|
26
|
-
sprite: "extensions/arrows/arrows.svg"
|
|
26
|
+
sprite: "extensions/arrows/arrows.svg",
|
|
27
|
+
// offset factors relative to half the square size (0.0 .. 1.0)
|
|
28
|
+
// 0.0 = start/end in the center; 1.0 = at the outer edge of the inscribed circle (half the square)
|
|
29
|
+
// default chosen to preserve previous appearance where radius = 0.36 * min(squareWidth, squareHeight)
|
|
30
|
+
// which corresponds to 0.72 of half the square size (0.36 / 0.5 = 0.72)
|
|
31
|
+
offsetFrom: 0,
|
|
32
|
+
offsetTo: 0.72
|
|
27
33
|
}
|
|
28
34
|
Object.assign(this.props, props)
|
|
29
35
|
if (this.chessboard.props.assetsCache) {
|
|
@@ -70,10 +76,27 @@ export class Arrows extends Extension {
|
|
|
70
76
|
href: `${spriteUrl}#${arrow.type.slice}`,
|
|
71
77
|
})
|
|
72
78
|
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
const
|
|
79
|
+
// Compute centers of start and end squares
|
|
80
|
+
const cx1 = ptFrom.x + view.squareWidth / 2
|
|
81
|
+
const cy1 = ptFrom.y + view.squareHeight / 2
|
|
82
|
+
const cx2 = ptTo.x + view.squareWidth / 2
|
|
83
|
+
const cy2 = ptTo.y + view.squareHeight / 2
|
|
84
|
+
|
|
85
|
+
// Offset the line so it starts/ends at the edge of an invisible circle inside each square
|
|
86
|
+
const dx = cx2 - cx1
|
|
87
|
+
const dy = cy2 - cy1
|
|
88
|
+
const len = Math.hypot(dx, dy) || 1
|
|
89
|
+
const ux = dx / len
|
|
90
|
+
const uy = dy / len
|
|
91
|
+
// compute radii from props: factor relative to half of the square size
|
|
92
|
+
const halfMin = Math.min(view.squareWidth, view.squareHeight) / 2
|
|
93
|
+
const clamp01 = (v) => Math.max(0, Math.min(1, v))
|
|
94
|
+
const rFrom = halfMin * clamp01(this.props.offsetFrom)
|
|
95
|
+
const rTo = halfMin * clamp01(this.props.offsetTo)
|
|
96
|
+
const x1 = cx1 + ux * rFrom
|
|
97
|
+
const y1 = cy1 + uy * rFrom
|
|
98
|
+
const x2 = cx2 - ux * rTo
|
|
99
|
+
const y2 = cy2 - uy * rTo
|
|
77
100
|
|
|
78
101
|
const width = ((view.scalingX + view.scalingY) / 2) * 4
|
|
79
102
|
let lineFill = Svg.addElement(arrowsGroup, "line")
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extension: RightClickAnnotator
|
|
3
|
+
* Combines Arrows and Markers to draw/toggle arrows and circle markers with right-click + modifiers.
|
|
4
|
+
* Colors:
|
|
5
|
+
* - Green: Right-click
|
|
6
|
+
* - Blue: Alt + Right-click
|
|
7
|
+
* - Red: Shift + Right-click
|
|
8
|
+
* - Orange: Shift + Alt + Right-click
|
|
9
|
+
* Redrawing the same arrow or marker removes it.
|
|
10
|
+
*/
|
|
11
|
+
import {Extension, EXTENSION_POINT} from "../../model/Extension.js"
|
|
12
|
+
import {Arrows, ARROW_TYPE} from "../arrows/Arrows.js"
|
|
13
|
+
import {Markers, MARKER_TYPE} from "../markers/Markers.js"
|
|
14
|
+
import {Svg} from "../../lib/Svg.js"
|
|
15
|
+
|
|
16
|
+
// Define persistent type objects for matching (strict equality used in core extensions)
|
|
17
|
+
const ARROW_GREEN = {class: "arrow-green", slice: "arrowDefault", headSize: 7}
|
|
18
|
+
const ARROW_ORANGE = {class: "arrow-orange", slice: "arrowDefault", headSize: 7}
|
|
19
|
+
|
|
20
|
+
const CIRCLE_GREEN = {class: "marker-circle-green", slice: "markerCircle"}
|
|
21
|
+
const CIRCLE_ORANGE = {class: "marker-circle-orange", slice: "markerCircle"}
|
|
22
|
+
|
|
23
|
+
export class RightClickAnnotator extends Extension {
|
|
24
|
+
constructor(chessboard, props = {}) {
|
|
25
|
+
super(chessboard)
|
|
26
|
+
this.props = props || {}
|
|
27
|
+
|
|
28
|
+
// Ensure Arrows and Markers extensions are available
|
|
29
|
+
if (!this.chessboard.getExtension(Arrows)) {
|
|
30
|
+
this.chessboard.addExtension(Arrows)
|
|
31
|
+
}
|
|
32
|
+
if (!this.chessboard.getExtension(Markers)) {
|
|
33
|
+
this.chessboard.addExtension(Markers)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.onContextMenu = this.onContextMenu.bind(this)
|
|
37
|
+
this.onMouseDown = this.onMouseDown.bind(this)
|
|
38
|
+
this.onMouseMove = this.onMouseMove.bind(this)
|
|
39
|
+
this.onMouseUp = this.onMouseUp.bind(this)
|
|
40
|
+
|
|
41
|
+
this.dragStart = undefined // {square, modifiers}
|
|
42
|
+
this.previewGroup = undefined
|
|
43
|
+
this.previewActiveTo = undefined // cache last to-square for preview
|
|
44
|
+
|
|
45
|
+
this.chessboard.context.addEventListener("contextmenu", this.onContextMenu)
|
|
46
|
+
this.chessboard.context.addEventListener("mousedown", this.onMouseDown)
|
|
47
|
+
this.chessboard.context.addEventListener("mousemove", this.onMouseMove)
|
|
48
|
+
this.chessboard.context.addEventListener("mouseup", this.onMouseUp)
|
|
49
|
+
this.chessboard.context.addEventListener("mouseleave", this.onMouseUp)
|
|
50
|
+
|
|
51
|
+
// ensure preview group exists after redraws
|
|
52
|
+
this.registerExtensionPoint(EXTENSION_POINT.afterRedrawBoard, () => {
|
|
53
|
+
this.ensurePreviewGroup()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
this.registerExtensionPoint(EXTENSION_POINT.destroy, () => {
|
|
57
|
+
this.chessboard.context.removeEventListener("contextmenu", this.onContextMenu)
|
|
58
|
+
this.chessboard.context.removeEventListener("mousedown", this.onMouseDown)
|
|
59
|
+
this.chessboard.context.removeEventListener("mousemove", this.onMouseMove)
|
|
60
|
+
this.chessboard.context.removeEventListener("mouseup", this.onMouseUp)
|
|
61
|
+
this.chessboard.context.removeEventListener("mouseleave", this.onMouseUp)
|
|
62
|
+
})
|
|
63
|
+
// create preview group now
|
|
64
|
+
this.ensurePreviewGroup()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
onContextMenu(event) {
|
|
68
|
+
// prevent default context menu on chessboard
|
|
69
|
+
event.preventDefault()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
onMouseDown(event) {
|
|
73
|
+
// right button only
|
|
74
|
+
if (event.button !== 2) {
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
const square = this.findSquareFromEvent(event)
|
|
78
|
+
if (!square) {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
this.dragStart = {
|
|
82
|
+
square,
|
|
83
|
+
modifiers: {
|
|
84
|
+
alt: event.altKey,
|
|
85
|
+
shift: event.shiftKey
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
onMouseUp(event) {
|
|
91
|
+
// clear preview regardless of button, but only act on right-button release
|
|
92
|
+
const start = this.dragStart
|
|
93
|
+
this.dragStart = undefined
|
|
94
|
+
this.clearPreview()
|
|
95
|
+
if (!start || event.button !== 2) {
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
const endSquare = this.findSquareFromEvent(event) || start.square
|
|
99
|
+
const colorKey = this.modifiersToColorKey(start.modifiers)
|
|
100
|
+
const {arrowType, circleType} = this.typesForColorKey(colorKey)
|
|
101
|
+
|
|
102
|
+
if (start.square && endSquare && start.square !== endSquare) {
|
|
103
|
+
// toggle arrow
|
|
104
|
+
const existing = this.chessboard.getArrows(arrowType, start.square, endSquare)
|
|
105
|
+
if (existing && existing.length > 0) {
|
|
106
|
+
this.chessboard.removeArrows(arrowType, start.square, endSquare)
|
|
107
|
+
} else {
|
|
108
|
+
this.chessboard.removeArrows(undefined, start.square, endSquare)
|
|
109
|
+
this.chessboard.addArrow(arrowType, start.square, endSquare)
|
|
110
|
+
}
|
|
111
|
+
} else if (start.square) {
|
|
112
|
+
// toggle marker on start square
|
|
113
|
+
const existingMarkers = this.chessboard.getMarkers(circleType, start.square)
|
|
114
|
+
if (existingMarkers && existingMarkers.length > 0) {
|
|
115
|
+
this.chessboard.removeMarkers(circleType, start.square)
|
|
116
|
+
} else {
|
|
117
|
+
this.chessboard.removeMarkers(undefined, start.square)
|
|
118
|
+
this.chessboard.addMarker(circleType, start.square)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
findSquareFromEvent(event) {
|
|
124
|
+
const target = /** @type {HTMLElement} */(event.target)
|
|
125
|
+
if (!target) return undefined
|
|
126
|
+
if (target.getAttribute && target.getAttribute("data-square")) {
|
|
127
|
+
return target.getAttribute("data-square")
|
|
128
|
+
}
|
|
129
|
+
const el = target.closest && target.closest("[data-square]")
|
|
130
|
+
return el ? el.getAttribute("data-square") : undefined
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
ensurePreviewGroup() {
|
|
134
|
+
// create or reset preview group used for transient arrow drawing
|
|
135
|
+
if (!this.chessboard?.view?.markersTopLayer) {
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
if (this.previewGroup && this.previewGroup.parentNode) {
|
|
139
|
+
// keep and clear existing
|
|
140
|
+
this.clearPreview()
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
this.previewGroup = Svg.addElement(this.chessboard.view.markersTopLayer, "g", {class: "right-click-annotator-preview"})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
clearPreview() {
|
|
147
|
+
if (!this.previewGroup) return
|
|
148
|
+
while (this.previewGroup.firstChild) {
|
|
149
|
+
this.previewGroup.removeChild(this.previewGroup.firstChild)
|
|
150
|
+
}
|
|
151
|
+
this.previewActiveTo = undefined
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
onMouseMove(event) {
|
|
155
|
+
if (!this.dragStart) {
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
// Only show preview for right-button drag if still pressed (best-effort); some browsers may not keep buttons state reliably
|
|
159
|
+
// We rely mainly on our dragStart flag and clear on mouseup/mouseleave
|
|
160
|
+
const toSquare = this.findSquareFromEvent(event)
|
|
161
|
+
if (!toSquare || toSquare === this.dragStart.square) {
|
|
162
|
+
this.clearPreview()
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
if (this.previewActiveTo === toSquare) {
|
|
166
|
+
return // no change
|
|
167
|
+
}
|
|
168
|
+
this.previewActiveTo = toSquare
|
|
169
|
+
const colorKey = this.modifiersToColorKey(this.dragStart.modifiers)
|
|
170
|
+
const {arrowType} = this.typesForColorKey(colorKey)
|
|
171
|
+
this.drawPreviewArrow(this.dragStart.square, toSquare, arrowType)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
drawPreviewArrow(from, to, type) {
|
|
175
|
+
this.ensurePreviewGroup()
|
|
176
|
+
this.clearPreview()
|
|
177
|
+
const view = this.chessboard.view
|
|
178
|
+
const arrowsGroup = Svg.addElement(this.previewGroup, "g", {class: "arrow " + type.class})
|
|
179
|
+
const ptFrom = view.squareToPoint(from)
|
|
180
|
+
const ptTo = view.squareToPoint(to)
|
|
181
|
+
const defs = Svg.addElement(arrowsGroup, "defs")
|
|
182
|
+
const id = "arrow-preview-" + from + to
|
|
183
|
+
const marker = Svg.addElement(defs, "marker", {
|
|
184
|
+
id: id,
|
|
185
|
+
markerWidth: type.headSize,
|
|
186
|
+
markerHeight: type.headSize,
|
|
187
|
+
refX: 20,
|
|
188
|
+
refY: 20,
|
|
189
|
+
viewBox: "0 0 40 40",
|
|
190
|
+
orient: "auto",
|
|
191
|
+
class: "arrow-head",
|
|
192
|
+
})
|
|
193
|
+
const spriteUrl = this.chessboard.props.assetsCache ? "" : this.chessboard.getExtension(Arrows)?.getSpriteUrl?.() || this.chessboard.props.assetsUrl + "extensions/arrows/arrows.svg"
|
|
194
|
+
Svg.addElement(marker, "use", {href: `${spriteUrl}#${type.slice}`})
|
|
195
|
+
// Compute centers of start and end squares
|
|
196
|
+
const cx1 = ptFrom.x + view.squareWidth / 2
|
|
197
|
+
const cy1 = ptFrom.y + view.squareHeight / 2
|
|
198
|
+
const cx2 = ptTo.x + view.squareWidth / 2
|
|
199
|
+
const cy2 = ptTo.y + view.squareHeight / 2
|
|
200
|
+
|
|
201
|
+
// Offset the line so it starts/ends at the edge of an invisible circle inside each square
|
|
202
|
+
const dx = cx2 - cx1
|
|
203
|
+
const dy = cy2 - cy1
|
|
204
|
+
const len = Math.hypot(dx, dy) || 1
|
|
205
|
+
const ux = dx / len
|
|
206
|
+
const uy = dy / len
|
|
207
|
+
// get offsets from Arrows extension props to match final arrow rendering
|
|
208
|
+
const arrowsExt = this.chessboard.getExtension(Arrows)
|
|
209
|
+
const offsetFrom = arrowsExt?.props?.offsetFrom ?? 0.72
|
|
210
|
+
const offsetTo = arrowsExt?.props?.offsetTo ?? 0.72
|
|
211
|
+
const halfMin = Math.min(view.squareWidth, view.squareHeight) / 2
|
|
212
|
+
const clamp01 = (v) => Math.max(0, Math.min(1, v))
|
|
213
|
+
const rFrom = halfMin * clamp01(offsetFrom)
|
|
214
|
+
const rTo = halfMin * clamp01(offsetTo)
|
|
215
|
+
const x1 = cx1 + ux * rFrom
|
|
216
|
+
const y1 = cy1 + uy * rFrom
|
|
217
|
+
const x2 = cx2 - ux * rTo
|
|
218
|
+
const y2 = cy2 - uy * rTo
|
|
219
|
+
|
|
220
|
+
const width = ((view.scalingX + view.scalingY) / 2) * 4
|
|
221
|
+
let lineFill = Svg.addElement(arrowsGroup, "line")
|
|
222
|
+
lineFill.setAttribute('x1', x1.toString())
|
|
223
|
+
lineFill.setAttribute('x2', x2.toString())
|
|
224
|
+
lineFill.setAttribute('y1', y1.toString())
|
|
225
|
+
lineFill.setAttribute('y2', y2.toString())
|
|
226
|
+
lineFill.setAttribute('class', 'arrow-line')
|
|
227
|
+
lineFill.setAttribute("marker-end", "url(#" + id + ")")
|
|
228
|
+
lineFill.setAttribute('stroke-width', width + "px")
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
modifiersToColorKey(modifiers) {
|
|
232
|
+
if (modifiers.shift && modifiers.alt) return "orange"
|
|
233
|
+
if (modifiers.shift) return "red"
|
|
234
|
+
if (modifiers.alt) return "blue"
|
|
235
|
+
return "green"
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
typesForColorKey(colorKey) {
|
|
239
|
+
switch (colorKey) {
|
|
240
|
+
case "blue":
|
|
241
|
+
return {arrowType: ARROW_TYPE.default, circleType: MARKER_TYPE.circlePrimary}
|
|
242
|
+
case "red":
|
|
243
|
+
return {arrowType: ARROW_TYPE.danger, circleType: MARKER_TYPE.circleDanger}
|
|
244
|
+
case "orange":
|
|
245
|
+
return {arrowType: ARROW_ORANGE, circleType: CIRCLE_ORANGE}
|
|
246
|
+
case "green":
|
|
247
|
+
default:
|
|
248
|
+
return {arrowType: ARROW_GREEN, circleType: CIRCLE_GREEN}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|