@mirasen/chessboard 1.2.4 → 1.3.1
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 +50 -0
- package/README.md +72 -1
- package/dist/animation/planner.js +2 -1
- package/dist/extensions/build/index.d.ts +1 -1
- package/dist/extensions/first-party/annotations/update.js +1 -1
- package/dist/extensions/first-party/board-events/factory.js +2 -2
- package/dist/extensions/first-party/main-renderer/animation/factory.d.ts +2 -1
- package/dist/extensions/first-party/main-renderer/animation/factory.js +3 -2
- package/dist/extensions/first-party/main-renderer/animation/types.d.ts +2 -0
- package/dist/extensions/first-party/main-renderer/animation/update.js +4 -2
- package/dist/extensions/first-party/main-renderer/board/factory.d.ts +1 -1
- package/dist/extensions/first-party/main-renderer/board/factory.js +4 -4
- package/dist/extensions/first-party/main-renderer/board/render.js +1 -1
- package/dist/extensions/first-party/main-renderer/board/types.d.ts +1 -1
- package/dist/extensions/first-party/main-renderer/coordinates/factory.d.ts +1 -1
- package/dist/extensions/first-party/main-renderer/coordinates/factory.js +2 -2
- package/dist/extensions/first-party/main-renderer/coordinates/render.js +1 -1
- package/dist/extensions/first-party/main-renderer/coordinates/types.d.ts +1 -1
- package/dist/extensions/first-party/main-renderer/denormalize.d.ts +3 -0
- package/dist/extensions/first-party/main-renderer/denormalize.js +18 -0
- package/dist/extensions/first-party/main-renderer/drag/factory.d.ts +2 -1
- package/dist/extensions/first-party/main-renderer/drag/factory.js +2 -1
- package/dist/extensions/first-party/main-renderer/drag/render.js +18 -11
- package/dist/extensions/first-party/main-renderer/drag/types.d.ts +2 -0
- package/dist/extensions/first-party/main-renderer/drag/update.js +4 -2
- package/dist/extensions/first-party/main-renderer/factory.d.ts +2 -1
- package/dist/extensions/first-party/main-renderer/factory.js +46 -12
- package/dist/extensions/first-party/main-renderer/normalize.d.ts +3 -3
- package/dist/extensions/first-party/main-renderer/normalize.js +19 -7
- package/dist/extensions/first-party/main-renderer/pieces/suppress.js +2 -1
- package/dist/extensions/first-party/main-renderer/public-defaults.d.ts +3 -0
- package/dist/extensions/first-party/main-renderer/public-defaults.js +4 -0
- package/dist/extensions/first-party/main-renderer/types/extension.d.ts +8 -5
- package/dist/extensions/first-party/main-renderer/types/instance.d.ts +10 -8
- package/dist/extensions/first-party/main-renderer/types/internal.d.ts +6 -7
- package/dist/extensions/first-party/main-renderer/types/internal.js +24 -1
- package/dist/extensions/first-party/main-renderer/types/public.d.ts +9 -0
- package/dist/extensions/first-party/main-renderer/types/template.d.ts +12 -1
- package/dist/extensions/first-party/promotion/types/input.d.ts +2 -2
- package/dist/extensions/index.d.ts +1 -0
- package/dist/extensions/index.js +1 -0
- package/dist/extensions/types/basic/events.d.ts +30 -16
- package/dist/extensions/types/basic/interaction.d.ts +12 -2
- package/dist/extensions/types/surface/commands.d.ts +4 -2
- package/dist/index.d.ts +10 -0
- package/dist/index.js +11 -0
- package/dist/runtime/factory/input.js +50 -24
- package/dist/runtime/factory/main.js +11 -0
- package/dist/runtime/input/adapter/types.js +2 -1
- package/dist/runtime/input/controller/factory.js +36 -9
- package/dist/runtime/input/controller/helpers.d.ts +5 -0
- package/dist/runtime/input/controller/helpers.js +11 -0
- package/dist/runtime/input/controller/pointer.js +48 -21
- package/dist/runtime/input/controller/types.d.ts +13 -4
- package/dist/runtime/mutation/interaction.js +1 -1
- package/dist/runtime/mutation/types.d.ts +2 -2
- package/dist/state/interaction/config.d.ts +6 -0
- package/dist/state/interaction/config.js +30 -0
- package/dist/state/interaction/factory.js +15 -3
- package/dist/state/interaction/helpers.d.ts +18 -2
- package/dist/state/interaction/helpers.js +12 -0
- package/dist/state/interaction/mutation.d.ts +2 -0
- package/dist/state/interaction/reducers.d.ts +3 -0
- package/dist/state/interaction/reducers.js +22 -7
- package/dist/state/interaction/types/config.d.ts +9 -0
- package/dist/state/interaction/types/config.js +1 -0
- package/dist/state/interaction/types/input.d.ts +3 -0
- package/dist/state/interaction/types/internal.d.ts +14 -4
- package/dist/state/interaction/types/main.d.ts +11 -5
- package/dist/wrapper/factory.js +2 -0
- package/dist/wrapper/types.d.ts +1 -1
- package/package.json +5 -6
- package/dist/extensions/first-party/main-renderer/types/input.d.ts +0 -4
- /package/dist/extensions/first-party/main-renderer/types/{input.js → public.js} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# @mirasen/chessboard
|
|
2
2
|
|
|
3
|
+
## 1.3.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- be0386f: Calibrate the default mobile drag piece scale and add board-level desktop/mobile config presets.
|
|
8
|
+
|
|
9
|
+
The mobile main-renderer drag preset now uses a larger lifted piece scale while preserving the existing bottom anchor and offset.
|
|
10
|
+
|
|
11
|
+
The root package now also exports `DefaultChessboardDesktopConfig` and `DefaultChessboardMobileConfig`, which combine the matching interaction defaults with the matching main-renderer defaults for easier desktop/mobile preset usage.
|
|
12
|
+
|
|
13
|
+
- be0386f: Calibrate the default mobile drag piece scale.
|
|
14
|
+
|
|
15
|
+
The mobile main-renderer drag preset now uses a larger lifted piece scale, making touch dragging feel closer to common mobile chessboard behavior while keeping the existing bottom anchor and offset.
|
|
16
|
+
|
|
17
|
+
## 1.3.0
|
|
18
|
+
|
|
19
|
+
### Minor Changes
|
|
20
|
+
|
|
21
|
+
- eaed694: Add configurable desktop and mobile drag behavior.
|
|
22
|
+
|
|
23
|
+
The board now exposes interaction config APIs through `setInteractionConfig()` and `getInteractionConfig()`, including `drag.liftedActivation.thresholdPx`. This enables desktop-style immediate drag activation and mobile-style delayed drag activation from the same interaction model.
|
|
24
|
+
|
|
25
|
+
The first-party main renderer now exposes runtime config APIs through `renderer.setConfig()` and `renderer.getConfig()`. Renderer config now includes dragged-piece visual settings and animation duration:
|
|
26
|
+
- `drag.pieceScale`
|
|
27
|
+
- `drag.pieceAnchor`
|
|
28
|
+
- `drag.pieceAnchorOffsetY`
|
|
29
|
+
- `animation.durationMs`
|
|
30
|
+
|
|
31
|
+
The package also exports public desktop/mobile main-renderer defaults from `@mirasen/chessboard/extensions` as `DefaultMainRendererDesktopConfig` and `DefaultMainRendererMobileConfig`.
|
|
32
|
+
|
|
33
|
+
Set `animation.durationMs` to `0` to disable piece movement animation by skipping animation creation.
|
|
34
|
+
|
|
35
|
+
Custom extension authors may need to update runtime interaction action names and payload fields to the new drag-session naming.
|
|
36
|
+
|
|
37
|
+
### Patch Changes
|
|
38
|
+
|
|
39
|
+
- dde200f: dependabot: directory '/', update eslint
|
|
40
|
+
- dfe6882: dependabot: directory '/', update typescript from 5.9.3 to 6.0.3
|
|
41
|
+
- eb1fcb5: dependabot: directory '/', update @types/node
|
|
42
|
+
- eb1fcb5: dependabot: directory '/', update @vitest/browser-playwright
|
|
43
|
+
- eb1fcb5: dependabot: directory '/', update @vitest/coverage-istanbul
|
|
44
|
+
- eb1fcb5: dependabot: directory '/', update svelte
|
|
45
|
+
- eb1fcb5: dependabot: directory '/', update typescript-eslint
|
|
46
|
+
- eb1fcb5: dependabot: directory '/', update vitest
|
|
47
|
+
- d1da3c1: Prevent fast mouse dragging from triggering native browser drag-and-drop on SVG piece images.
|
|
48
|
+
|
|
49
|
+
Browsers can treat SVG images inside pieces as draggable content during quick mouse movement. When that native drag fallback starts, it can interrupt the board's pointer-driven drag flow by causing `pointercancel` / `lostpointercapture` and, in some cases, preventing the board from receiving the expected `pointerup`.
|
|
50
|
+
|
|
51
|
+
The board now suppresses native `dragstart` by default when no extension consumes the event, reducing one common source of interrupted fast mouse drags while keeping the existing `lostpointercapture` recovery path for other platform edge cases. `dragstart` remains observable by extensions and does not produce a core interaction action.
|
|
52
|
+
|
|
3
53
|
## 1.2.4
|
|
4
54
|
|
|
5
55
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ A framework-agnostic TypeScript chessboard platform with real chess interaction
|
|
|
11
11
|
## Try it live (including annotations)
|
|
12
12
|
|
|
13
13
|
- [Play a chess.js game](https://mirasen.io/chessboard/examples/chessjs.html)
|
|
14
|
+
- [Watch 12 live boards](https://mirasen.io/chessboard/examples/live-games-grid.html)
|
|
14
15
|
- [Try the promotion flow](https://mirasen.io/chessboard/examples/promotion.html)
|
|
15
16
|
- [Try the minimal example](https://mirasen.io/chessboard/examples/minimal.html)
|
|
16
17
|
|
|
@@ -18,6 +19,14 @@ A framework-agnostic TypeScript chessboard platform with real chess interaction
|
|
|
18
19
|
|
|
19
20
|
Use [`@mirasen/react-chessboard`](https://www.npmjs.com/package/@mirasen/react-chessboard) for a React component with the same built-in interaction, annotations, promotion, animation, and chess.js integration.
|
|
20
21
|
|
|
22
|
+
## Native mobile apps
|
|
23
|
+
|
|
24
|
+
`@mirasen/chessboard` is not tied to React or any other web framework. It is plain TypeScript/JavaScript on top of DOM and SVG, so it should fit WebView-based native iOS or Android integrations.
|
|
25
|
+
|
|
26
|
+
A common integration pattern is to bundle a WebView host page, mount the board there, and communicate with the native app through the platform bridge.
|
|
27
|
+
|
|
28
|
+
This keeps the integration model lightweight while avoiding a separate native chessboard UI implementation.
|
|
29
|
+
|
|
21
30
|
## Why this exists
|
|
22
31
|
|
|
23
32
|
Most chessboards fall into one of two buckets:
|
|
@@ -65,7 +74,7 @@ Rules and legality still belong to your game layer. Use the built-in `chess.js`
|
|
|
65
74
|
- Real built-in chess interaction
|
|
66
75
|
- Built-in circles and arrows with live add/remove previews
|
|
67
76
|
- **Easy game/rules integration** — connect your own game model, or use the built-in `chess.js` adapter
|
|
68
|
-
- Works on desktop and mobile out of the box, with pointer-based mouse and
|
|
77
|
+
- Works on desktop and mobile out of the box, with pointer-based mouse/touch interaction and tunable drag behavior
|
|
69
78
|
- State-diff animation beyond simple move events
|
|
70
79
|
- Extension-driven architecture
|
|
71
80
|
- First-party baseline extensions
|
|
@@ -257,6 +266,68 @@ Movability controls how the board accepts user-initiated moves.
|
|
|
257
266
|
- `free` — user moves are accepted without legal destination filtering
|
|
258
267
|
- `strict` — user moves are accepted only when the target is allowed by the destinations provided by your app/rules layer
|
|
259
268
|
|
|
269
|
+
### Desktop and mobile drag tuning
|
|
270
|
+
|
|
271
|
+
The board exposes interaction-level and renderer-level drag configuration so you can tune mouse-first and touch-first surfaces without replacing the built-in interaction model.
|
|
272
|
+
|
|
273
|
+
Interaction config controls when lifted-piece drag becomes active:
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
const board = createBoard({
|
|
277
|
+
element
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
board.setInteractionConfig({
|
|
281
|
+
drag: {
|
|
282
|
+
liftedActivation: {
|
|
283
|
+
thresholdPx: 5
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Use `thresholdPx: 0` for immediate desktop-style drag activation, or a small threshold for mobile/touch UIs where the piece should not jump on the first tiny pointer movement.
|
|
290
|
+
|
|
291
|
+
The first-party renderer controls how the lifted piece looks while dragging:
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
board.extensions.renderer.setConfig({
|
|
295
|
+
drag: {
|
|
296
|
+
pieceScale: 2,
|
|
297
|
+
pieceAnchor: 'bottom',
|
|
298
|
+
pieceAnchorOffsetY: 0.14
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
For convenience, the root package ships board-level presets that combine the interaction feel and renderer visuals for a desktop- or mobile-style board. Apply both parts together:
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
import { DefaultChessboardMobileConfig } from '@mirasen/chessboard';
|
|
307
|
+
|
|
308
|
+
board.setInteractionConfig(DefaultChessboardMobileConfig.interaction);
|
|
309
|
+
|
|
310
|
+
board.extensions.renderer.setConfig({
|
|
311
|
+
drag: DefaultChessboardMobileConfig.renderer.drag
|
|
312
|
+
});
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
`DefaultChessboardDesktopConfig` is the matching desktop preset. If you only want the renderer half, the lower-level `DefaultMainRendererDesktopConfig` and `DefaultMainRendererMobileConfig` building blocks are still exported from `@mirasen/chessboard/extensions`.
|
|
316
|
+
|
|
317
|
+
### Animation tuning
|
|
318
|
+
|
|
319
|
+
The first-party renderer exposes animation duration config:
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
board.extensions.renderer.setConfig({
|
|
323
|
+
animation: {
|
|
324
|
+
durationMs: 0
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Set `animation.durationMs` to `0` when you want position updates to render instantly without creating movement animations.
|
|
330
|
+
|
|
260
331
|
### Advanced: explicit extension lists
|
|
261
332
|
|
|
262
333
|
Most users can rely on the default first-party extension baseline. Use an explicit extension list only when you want to control which built-in or custom extensions are installed.
|
|
@@ -3,6 +3,7 @@ import { isNonEmptyPieceCode } from '../state/board/check.js';
|
|
|
3
3
|
import { fileOf, rankOf } from '../state/board/coords.js';
|
|
4
4
|
import { SQUARE_COUNT } from '../state/board/types/internal.js';
|
|
5
5
|
import { baseMovesEqual } from '../state/change/helpers.js';
|
|
6
|
+
import { isDragSessionActiveLiftedPiece } from '../state/interaction/helpers.js';
|
|
6
7
|
// ---------------------------------------------------------------------------
|
|
7
8
|
// Precomputed squared distance: (dFile² + dRank²) for all square pairs.
|
|
8
9
|
// Max value = 7² + 7² = 98, fits in Uint8. Index: a * 64 + b.
|
|
@@ -31,7 +32,7 @@ const EMPTY_EXCLUDE = {
|
|
|
31
32
|
*/
|
|
32
33
|
function detectLiftedPieceDrop(previous, current) {
|
|
33
34
|
const prevDrag = previous.interaction.dragSession;
|
|
34
|
-
if (!prevDrag || prevDrag
|
|
35
|
+
if (!prevDrag || !isDragSessionActiveLiftedPiece(prevDrag))
|
|
35
36
|
return null;
|
|
36
37
|
if (current.interaction.dragSession !== null)
|
|
37
38
|
return null;
|
|
@@ -10,8 +10,8 @@ export type { ColorCode, EmptyPieceCode, NonEmptyPieceCode, PieceCode, RoleCode,
|
|
|
10
10
|
export type { MovabilityModeCode } from '../../state/interaction/types/internal.js';
|
|
11
11
|
export { extensionCreateInternalBase, extensionDestroyBase, extensionIsDestroyedBase, extensionIsMountedBase, extensionMountBase, extensionUnmountBase } from '../first-party/common/helpers.js';
|
|
12
12
|
export type { ExtensionInternalBase, OpaqueColor } from '../first-party/common/types.js';
|
|
13
|
-
export type { PieceUrlsInput } from '../first-party/main-renderer/types/input.js';
|
|
14
13
|
export type { PieceUrls } from '../first-party/main-renderer/types/internal.js';
|
|
14
|
+
export type { PieceUrlsPublic } from '../first-party/main-renderer/types/public.js';
|
|
15
15
|
export type { PromotionPieceCode } from '../first-party/promotion/types/internal.js';
|
|
16
16
|
export type { ExtensionSlotName, ExtensionSlotSvgRoots } from '../types/basic/mount.js';
|
|
17
17
|
export { isFrameMounted, isFrameRenderable } from '../types/basic/update.js';
|
|
@@ -5,7 +5,7 @@ export function annotationsOnUpdate(state, context) {
|
|
|
5
5
|
if (state.config.clearOnCoreInteraction &&
|
|
6
6
|
hasCommittedAnnotations(state) &&
|
|
7
7
|
context.mutation.hasMutation({
|
|
8
|
-
causes: ['runtime.interaction.
|
|
8
|
+
causes: ['runtime.interaction.completeCoreDragSessionTo']
|
|
9
9
|
})) {
|
|
10
10
|
clearCommittedAnnotations(state);
|
|
11
11
|
context.invalidation.markDirty(1 /* DirtyLayer.COMMITTED */);
|
|
@@ -45,8 +45,8 @@ function createBoardEventsInstance(options) {
|
|
|
45
45
|
});
|
|
46
46
|
const interactionCompleted = context.mutation.hasMutation({
|
|
47
47
|
causes: [
|
|
48
|
-
'runtime.interaction.
|
|
49
|
-
'runtime.interaction.
|
|
48
|
+
'runtime.interaction.completeCoreDragSessionTo',
|
|
49
|
+
'runtime.interaction.completeExtensionDragSession',
|
|
50
50
|
'runtime.interaction.resolveDeferredUIMoveRequest'
|
|
51
51
|
]
|
|
52
52
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionRuntimeSurface } from '../../../types/surface/main.js';
|
|
2
2
|
import type { PieceSymbolResolver } from '../piece-symbols.js';
|
|
3
|
+
import type { MainRendererConfigAnimation } from '../types/template.js';
|
|
3
4
|
import type { MainRendererAnimation } from './types.js';
|
|
4
|
-
export declare function createMainRendererAnimation(runtimeSurface: ExtensionRuntimeSurface, resolver: PieceSymbolResolver): MainRendererAnimation;
|
|
5
|
+
export declare function createMainRendererAnimation(runtimeSurface: ExtensionRuntimeSurface, resolver: PieceSymbolResolver, getAnimationConfig: () => MainRendererConfigAnimation): MainRendererAnimation;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { cleanAnimationPlan } from '../../../../animation/render/plan.js';
|
|
2
2
|
import { getAnimationSuppressedSquares, rendererAnimationClean, rendererAnimationPrepare, rendererAnimationRender } from './render.js';
|
|
3
3
|
import { rendererAnimationOnUpdate } from './update.js';
|
|
4
|
-
export function createMainRendererAnimation(runtimeSurface, resolver) {
|
|
4
|
+
export function createMainRendererAnimation(runtimeSurface, resolver, getAnimationConfig) {
|
|
5
5
|
const state = {
|
|
6
6
|
runtimeSurface,
|
|
7
7
|
resolver,
|
|
8
|
-
entries: new Map()
|
|
8
|
+
entries: new Map(),
|
|
9
|
+
getAnimationConfig
|
|
9
10
|
};
|
|
10
11
|
return {
|
|
11
12
|
onUpdate(context) {
|
|
@@ -5,6 +5,7 @@ import { ExtensionCleanAnimationContext, ExtensionPrepareAnimationContext, Exten
|
|
|
5
5
|
import { ExtensionUpdateContext } from '../../../types/context/update.js';
|
|
6
6
|
import { ExtensionRuntimeSurface } from '../../../types/surface/main.js';
|
|
7
7
|
import type { PieceSymbolResolver } from '../piece-symbols.js';
|
|
8
|
+
import type { MainRendererConfigAnimation } from '../types/template.js';
|
|
8
9
|
export interface MainRendererAnimationEntry {
|
|
9
10
|
plan: AnimationPlan;
|
|
10
11
|
nodes: PreparedNodeMap | null;
|
|
@@ -13,6 +14,7 @@ export interface MainRendererAnimationInternal {
|
|
|
13
14
|
readonly runtimeSurface: ExtensionRuntimeSurface;
|
|
14
15
|
readonly resolver: PieceSymbolResolver;
|
|
15
16
|
readonly entries: Map<number, MainRendererAnimationEntry>;
|
|
17
|
+
readonly getAnimationConfig: () => MainRendererConfigAnimation;
|
|
16
18
|
}
|
|
17
19
|
export interface MainRendererAnimation {
|
|
18
20
|
onUpdate(context: ExtensionUpdateContext): void;
|
|
@@ -2,7 +2,6 @@ import { calculateAnimationPlan } from '../../../../animation/planner.js';
|
|
|
2
2
|
import { piecePositionsEqual } from '../../../../state/board/check.js';
|
|
3
3
|
import { changeStatesEqual } from '../../../../state/change/helpers.js';
|
|
4
4
|
import { isUpdateContextRenderable } from '../../../types/context/update.js';
|
|
5
|
-
const DEFAULT_ANIMATION_DURATION_MS = 180;
|
|
6
5
|
export function rendererAnimationOnUpdate(state, context) {
|
|
7
6
|
if (!isUpdateContextRenderable(context) ||
|
|
8
7
|
!context.mutation.hasMutation({ prefixes: ['state.board.', 'state.change.'] }) ||
|
|
@@ -12,6 +11,9 @@ export function rendererAnimationOnUpdate(state, context) {
|
|
|
12
11
|
if (piecePositionsEqual(context.previousFrame.state.board, context.currentFrame.state.board) &&
|
|
13
12
|
changeStatesEqual(context.previousFrame.state.change, context.currentFrame.state.change))
|
|
14
13
|
return;
|
|
14
|
+
const durationMs = state.getAnimationConfig().durationMs;
|
|
15
|
+
if (durationMs === 0)
|
|
16
|
+
return;
|
|
15
17
|
// Pass raw snapshots to the planner — it normalizes internally
|
|
16
18
|
const plan = calculateAnimationPlan({
|
|
17
19
|
previous: context.previousFrame.state,
|
|
@@ -20,7 +22,7 @@ export function rendererAnimationOnUpdate(state, context) {
|
|
|
20
22
|
if (plan.tracks.length === 0)
|
|
21
23
|
return;
|
|
22
24
|
const session = state.runtimeSurface.animation.submit({
|
|
23
|
-
duration:
|
|
25
|
+
duration: durationMs
|
|
24
26
|
});
|
|
25
27
|
state.entries.set(session.id, { plan, nodes: null });
|
|
26
28
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { ConfigColorPair } from '../types/internal.js';
|
|
2
2
|
import { MainRendererBoard } from './types.js';
|
|
3
|
-
export declare function createMainRendererBoard(
|
|
3
|
+
export declare function createMainRendererBoard(getColorConfig: () => ConfigColorPair): MainRendererBoard;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { rendererBoardRender } from './render.js';
|
|
2
2
|
import { rendererBoardOnUpdate } from './update.js';
|
|
3
|
-
function createMainRendererBoardInternal(
|
|
4
|
-
return {
|
|
3
|
+
function createMainRendererBoardInternal(getColorConfig) {
|
|
4
|
+
return { getColorConfig };
|
|
5
5
|
}
|
|
6
|
-
export function createMainRendererBoard(
|
|
7
|
-
const internalState = createMainRendererBoardInternal(
|
|
6
|
+
export function createMainRendererBoard(getColorConfig) {
|
|
7
|
+
const internalState = createMainRendererBoardInternal(getColorConfig);
|
|
8
8
|
return {
|
|
9
9
|
onUpdate(context) {
|
|
10
10
|
rendererBoardOnUpdate(internalState, context);
|
|
@@ -8,7 +8,7 @@ export function rendererBoardRender(state, context, slot) {
|
|
|
8
8
|
}
|
|
9
9
|
const geometry = context.currentFrame.layout.geometry;
|
|
10
10
|
clearSvgElementChildren(slot);
|
|
11
|
-
const { light, dark } = state.
|
|
11
|
+
const { light, dark } = state.getColorConfig();
|
|
12
12
|
for (let sq = 0; sq < SQUARE_COUNT; sq++) {
|
|
13
13
|
const r = geometry.getSquareRect(sq);
|
|
14
14
|
createSvgElement(slot, 'rect', {
|
|
@@ -2,7 +2,7 @@ import { ExtensionRenderContext } from '../../../types/context/render.js';
|
|
|
2
2
|
import { ExtensionUpdateContext } from '../../../types/context/update.js';
|
|
3
3
|
import { ConfigColorPair } from '../types/internal.js';
|
|
4
4
|
export interface MainRendererBoardInternal {
|
|
5
|
-
readonly
|
|
5
|
+
readonly getColorConfig: () => ConfigColorPair;
|
|
6
6
|
}
|
|
7
7
|
export interface MainRendererBoard {
|
|
8
8
|
onUpdate(context: ExtensionUpdateContext): void;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { ConfigColorPair } from '../types/internal.js';
|
|
2
2
|
import { MainRendererCoordinates } from './types.js';
|
|
3
|
-
export declare function createMainRendererCoordinates(
|
|
3
|
+
export declare function createMainRendererCoordinates(getColorConfig: () => ConfigColorPair): MainRendererCoordinates;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { rendererCoordinatesRender } from './render.js';
|
|
2
|
-
export function createMainRendererCoordinates(
|
|
3
|
-
const internalState = {
|
|
2
|
+
export function createMainRendererCoordinates(getColorConfig) {
|
|
3
|
+
const internalState = { getColorConfig };
|
|
4
4
|
return {
|
|
5
5
|
render(context, slot) {
|
|
6
6
|
rendererCoordinatesRender(internalState, context, slot);
|
|
@@ -10,7 +10,7 @@ export function rendererCoordinatesRender(state, context, slot) {
|
|
|
10
10
|
}
|
|
11
11
|
const geometry = context.currentFrame.layout.geometry;
|
|
12
12
|
clearSvgElementChildren(slot);
|
|
13
|
-
const coords = state.
|
|
13
|
+
const coords = state.getColorConfig();
|
|
14
14
|
const fontSize = geometry.squareSize * 0.12;
|
|
15
15
|
const offset = 3;
|
|
16
16
|
// Rank labels on the visual left edge
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ExtensionRenderContext } from '../../../types/context/render.js';
|
|
2
2
|
import { ConfigColorPair } from '../types/internal.js';
|
|
3
3
|
export interface MainRendererCoordinatesInternal {
|
|
4
|
-
readonly
|
|
4
|
+
readonly getColorConfig: () => ConfigColorPair;
|
|
5
5
|
}
|
|
6
6
|
export interface MainRendererCoordinates {
|
|
7
7
|
render(context: ExtensionRenderContext, slot: SVGGElement): void;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { cloneDeep } from 'es-toolkit';
|
|
2
|
+
import { denormalizePieceString } from '../../../state/board/denormalize.js';
|
|
3
|
+
function denormalizePieceUrls(pieceUrls) {
|
|
4
|
+
const result = {};
|
|
5
|
+
for (const [codeKey, url] of Object.entries(pieceUrls)) {
|
|
6
|
+
const code = Number(codeKey);
|
|
7
|
+
result[denormalizePieceString(code)] = url;
|
|
8
|
+
}
|
|
9
|
+
return result;
|
|
10
|
+
}
|
|
11
|
+
export function denormalizeMainRendererConfig(config) {
|
|
12
|
+
return {
|
|
13
|
+
colors: cloneDeep(config.colors),
|
|
14
|
+
drag: cloneDeep(config.drag),
|
|
15
|
+
animation: cloneDeep(config.animation),
|
|
16
|
+
pieceUrls: denormalizePieceUrls(config.pieceUrls)
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ExtensionRuntimeSurface } from '../../../types/surface/main.js';
|
|
2
2
|
import type { PieceSymbolResolver } from '../piece-symbols.js';
|
|
3
|
+
import type { MainRendererConfigDrag } from '../types/template.js';
|
|
3
4
|
import { MainRendererDrag } from './types.js';
|
|
4
|
-
export declare function createMainRendererDrag(runtimeSurface: ExtensionRuntimeSurface, resolver: PieceSymbolResolver): MainRendererDrag;
|
|
5
|
+
export declare function createMainRendererDrag(runtimeSurface: ExtensionRuntimeSurface, resolver: PieceSymbolResolver, getDragConfig: () => MainRendererConfigDrag): MainRendererDrag;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { rendererDragRenderTransientVisuals } from './render.js';
|
|
2
2
|
import { rendererDragOnUpdate } from './update.js';
|
|
3
|
-
export function createMainRendererDrag(runtimeSurface, resolver) {
|
|
3
|
+
export function createMainRendererDrag(runtimeSurface, resolver, getDragConfig) {
|
|
4
4
|
const internalState = {
|
|
5
5
|
runtimeSurface,
|
|
6
6
|
resolver,
|
|
7
|
+
getDragConfig,
|
|
7
8
|
isDragActive: false,
|
|
8
9
|
pieceCode: null,
|
|
9
10
|
pieceNode: null
|
|
@@ -1,28 +1,35 @@
|
|
|
1
1
|
import { createSvgElement, updateSvgElementAttributes } from '../../../../render/svg/helpers.js';
|
|
2
2
|
export function rendererDragRenderTransientVisuals(state, context, slot) {
|
|
3
|
+
const config = state.getDragConfig();
|
|
3
4
|
const geometry = context.currentFrame.layout.geometry;
|
|
4
5
|
const point = context.transientInput.boardClampedPoint;
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
6
|
+
const renderedSize = geometry.squareSize * config.pieceScale;
|
|
7
|
+
const anchorOffsetY = geometry.squareSize * config.pieceAnchorOffsetY;
|
|
8
|
+
const x = point.x - renderedSize / 2;
|
|
9
|
+
const y = config.pieceAnchor === 'bottom'
|
|
10
|
+
? point.y - renderedSize + anchorOffsetY
|
|
11
|
+
: point.y - renderedSize / 2 + anchorOffsetY;
|
|
12
|
+
const renderedSizeStr = renderedSize.toString();
|
|
13
|
+
const xStr = x.toString();
|
|
14
|
+
const yStr = y.toString();
|
|
8
15
|
if (state.isDragActive && state.pieceCode && !state.pieceNode) {
|
|
9
16
|
// Ok this is first render pass, let's create the piece node
|
|
10
17
|
const href = state.resolver.getHref(state.pieceCode);
|
|
11
18
|
state.pieceNode = createSvgElement(slot, 'use', {
|
|
12
19
|
'data-chessboard-id': 'dragged-piece',
|
|
13
|
-
width:
|
|
14
|
-
height:
|
|
15
|
-
x,
|
|
16
|
-
y,
|
|
20
|
+
width: renderedSizeStr,
|
|
21
|
+
height: renderedSizeStr,
|
|
22
|
+
x: xStr,
|
|
23
|
+
y: yStr,
|
|
17
24
|
href
|
|
18
25
|
});
|
|
19
26
|
}
|
|
20
27
|
else if (state.isDragActive && state.pieceNode) {
|
|
21
28
|
updateSvgElementAttributes(state.pieceNode, {
|
|
22
|
-
width:
|
|
23
|
-
height:
|
|
24
|
-
x,
|
|
25
|
-
y
|
|
29
|
+
width: renderedSizeStr,
|
|
30
|
+
height: renderedSizeStr,
|
|
31
|
+
x: xStr,
|
|
32
|
+
y: yStr
|
|
26
33
|
});
|
|
27
34
|
}
|
|
28
35
|
}
|
|
@@ -3,9 +3,11 @@ import { ExtensionRenderTransientVisualsContext } from '../../../types/context/t
|
|
|
3
3
|
import { ExtensionUpdateContext } from '../../../types/context/update.js';
|
|
4
4
|
import { ExtensionRuntimeSurface } from '../../../types/surface/main.js';
|
|
5
5
|
import type { PieceSymbolResolver } from '../piece-symbols.js';
|
|
6
|
+
import type { MainRendererConfigDrag } from '../types/template.js';
|
|
6
7
|
export interface MainRendererDragInternal {
|
|
7
8
|
readonly runtimeSurface: ExtensionRuntimeSurface;
|
|
8
9
|
readonly resolver: PieceSymbolResolver;
|
|
10
|
+
readonly getDragConfig: () => MainRendererConfigDrag;
|
|
9
11
|
isDragActive: boolean;
|
|
10
12
|
pieceCode: NonEmptyPieceCode | null;
|
|
11
13
|
pieceNode: SVGUseElement | null;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import assert from '@ktarmyshov/assert';
|
|
2
2
|
import { isNonEmptyPieceCode } from '../../../../state/board/check.js';
|
|
3
|
+
import { isDragSessionActiveLiftedPiece } from '../../../../state/interaction/helpers.js';
|
|
3
4
|
export function rendererDragOnUpdate(state, context) {
|
|
4
|
-
const
|
|
5
|
+
const dragSession = context.currentFrame.state.interaction.dragSession;
|
|
6
|
+
const isLiftedDragActive = dragSession !== null && isDragSessionActiveLiftedPiece(dragSession);
|
|
5
7
|
if (isLiftedDragActive) {
|
|
6
8
|
if (!state.isDragActive) {
|
|
7
|
-
const pieceCode =
|
|
9
|
+
const pieceCode = dragSession.sourcePieceCode;
|
|
8
10
|
assert(isNonEmptyPieceCode(pieceCode), 'Invalid piece code in drag session');
|
|
9
11
|
state.pieceCode = pieceCode;
|
|
10
12
|
state.runtimeSurface.transientVisuals.subscribe();
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import { MainRendererDefinition
|
|
1
|
+
import { MainRendererDefinition } from './types/extension.js';
|
|
2
|
+
import type { MainRendererInitOptions } from './types/public.js';
|
|
2
3
|
export declare function createMainRenderer(options?: MainRendererInitOptions): MainRendererDefinition;
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { toMerged } from 'es-toolkit';
|
|
2
1
|
import { extensionCreateInternalBase, extensionDestroyBase, extensionMountBase, extensionUnmountBase } from '../common/helpers.js';
|
|
3
2
|
import { createMainRendererAnimation } from './animation/factory.js';
|
|
4
3
|
import { createMainRendererBoard } from './board/factory.js';
|
|
5
4
|
import { createMainRendererCoordinates } from './coordinates/factory.js';
|
|
5
|
+
import { denormalizeMainRendererConfig } from './denormalize.js';
|
|
6
6
|
import { createMainRendererDrag } from './drag/factory.js';
|
|
7
7
|
import { normalizeMainRendererConfig } from './normalize.js';
|
|
8
8
|
import { createPieceSymbolResolver, ensurePieceSymbolsDefined } from './piece-symbols.js';
|
|
9
9
|
import { createMainRendererPieces } from './pieces/factory.js';
|
|
10
|
-
import { EXTENSION_ID, EXTENSION_SLOTS } from './types/extension.js';
|
|
11
|
-
import {
|
|
12
|
-
export function createMainRenderer(options
|
|
13
|
-
const config =
|
|
10
|
+
import { DirtyLayer, EXTENSION_ID, EXTENSION_SLOTS } from './types/extension.js';
|
|
11
|
+
import { DefaultMainRendererDesktopConfig } from './types/internal.js';
|
|
12
|
+
export function createMainRenderer(options) {
|
|
13
|
+
const config = normalizeMainRendererConfig(options, DefaultMainRendererDesktopConfig);
|
|
14
14
|
return {
|
|
15
15
|
id: EXTENSION_ID,
|
|
16
16
|
slots: EXTENSION_SLOTS,
|
|
@@ -21,12 +21,15 @@ export function createMainRenderer(options = {}) {
|
|
|
21
21
|
}
|
|
22
22
|
function createMainRendererInternal(options, config) {
|
|
23
23
|
const pieceSymbolResolver = createPieceSymbolResolver(options.svgIds);
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
// Forward reference: subsystems read color config through getters that close over the
|
|
25
|
+
// internal state, so a runtime setConfig({ colors }) update is observed on the next render
|
|
26
|
+
// without re-creating the subsystems.
|
|
27
|
+
const board = createMainRendererBoard(() => internalState.config.colors.board);
|
|
28
|
+
const coordinates = createMainRendererCoordinates(() => internalState.config.colors.coordinates);
|
|
26
29
|
const pieces = createMainRendererPieces(pieceSymbolResolver);
|
|
27
|
-
const drag = createMainRendererDrag(options.runtimeSurface, pieceSymbolResolver);
|
|
28
|
-
const animation = createMainRendererAnimation(options.runtimeSurface, pieceSymbolResolver);
|
|
29
|
-
|
|
30
|
+
const drag = createMainRendererDrag(options.runtimeSurface, pieceSymbolResolver, () => internalState.config.drag);
|
|
31
|
+
const animation = createMainRendererAnimation(options.runtimeSurface, pieceSymbolResolver, () => internalState.config.animation);
|
|
32
|
+
const internalState = {
|
|
30
33
|
...extensionCreateInternalBase(options),
|
|
31
34
|
board,
|
|
32
35
|
coordinates,
|
|
@@ -34,7 +37,34 @@ function createMainRendererInternal(options, config) {
|
|
|
34
37
|
drag,
|
|
35
38
|
animation,
|
|
36
39
|
runtimeSurface: options.runtimeSurface,
|
|
37
|
-
pieceSymbolResolver
|
|
40
|
+
pieceSymbolResolver,
|
|
41
|
+
config
|
|
42
|
+
};
|
|
43
|
+
return internalState;
|
|
44
|
+
}
|
|
45
|
+
function createMainRendererInstancePublic(state) {
|
|
46
|
+
return {
|
|
47
|
+
getConfig() {
|
|
48
|
+
return denormalizeMainRendererConfig(state.config);
|
|
49
|
+
},
|
|
50
|
+
setConfig(options) {
|
|
51
|
+
// Defensively strip pieceUrls so a non-TS caller cannot bypass the lifecycle.
|
|
52
|
+
const { pieceUrls: _ignored, ...safeInput } = (options ?? {});
|
|
53
|
+
void _ignored;
|
|
54
|
+
const previous = state.config;
|
|
55
|
+
state.config = normalizeMainRendererConfig(safeInput, state.config);
|
|
56
|
+
let dirty = 0;
|
|
57
|
+
if (state.config.colors.board !== previous.colors.board) {
|
|
58
|
+
dirty |= DirtyLayer.Board;
|
|
59
|
+
}
|
|
60
|
+
if (state.config.colors.coordinates !== previous.colors.coordinates) {
|
|
61
|
+
dirty |= DirtyLayer.Coordinates;
|
|
62
|
+
}
|
|
63
|
+
if (dirty !== 0) {
|
|
64
|
+
state.runtimeSurface.invalidation.markDirty(dirty);
|
|
65
|
+
state.runtimeSurface.commands.requestRender({ state: true });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
38
68
|
};
|
|
39
69
|
}
|
|
40
70
|
function extensionUnmountLocal(state) {
|
|
@@ -51,11 +81,12 @@ function validateIsMounted(state) {
|
|
|
51
81
|
}
|
|
52
82
|
function createMainRendererInstance(options, config) {
|
|
53
83
|
const internalState = createMainRendererInternal(options, config);
|
|
84
|
+
const publicInterface = createMainRendererInstancePublic(internalState);
|
|
54
85
|
return {
|
|
55
86
|
id: EXTENSION_ID,
|
|
56
87
|
mount(env) {
|
|
57
88
|
extensionMountBase(internalState, env.slotRoots);
|
|
58
|
-
ensurePieceSymbolsDefined(env.slotRoots.defs, config.pieceUrls, internalState.pieceSymbolResolver);
|
|
89
|
+
ensurePieceSymbolsDefined(env.slotRoots.defs, internalState.config.pieceUrls, internalState.pieceSymbolResolver);
|
|
59
90
|
},
|
|
60
91
|
onUpdate(context) {
|
|
61
92
|
internalState.board.onUpdate(context);
|
|
@@ -94,6 +125,9 @@ function createMainRendererInstance(options, config) {
|
|
|
94
125
|
destroy() {
|
|
95
126
|
extensionUnmountLocal(internalState);
|
|
96
127
|
extensionDestroyBase(internalState);
|
|
128
|
+
},
|
|
129
|
+
getPublic() {
|
|
130
|
+
return publicInterface;
|
|
97
131
|
}
|
|
98
132
|
};
|
|
99
133
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
export declare function normalizeMainRendererConfig(input:
|
|
1
|
+
import type { MainRendererConfig } from './types/internal.js';
|
|
2
|
+
import type { MainRendererInitOptions } from './types/public.js';
|
|
3
|
+
export declare function normalizeMainRendererConfig(input: MainRendererInitOptions | undefined, base: MainRendererConfig): MainRendererConfig;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from '@ktarmyshov/assert';
|
|
2
|
+
import { toMerged } from 'es-toolkit/object';
|
|
2
3
|
import { normalizePiece } from '../../../state/board/normalize.js';
|
|
3
4
|
import { ALL_NON_EMPTY_PIECE_CODES } from '../../../state/board/types/internal.js';
|
|
4
|
-
import { DEFAULT_MAIN_RENDERER_CONFIG } from './types/internal.js';
|
|
5
5
|
function assertPieceUrlsComplete(pieceUrls) {
|
|
6
6
|
for (const pieceCode of ALL_NON_EMPTY_PIECE_CODES) {
|
|
7
7
|
assert(pieceCode in pieceUrls, `Missing URL for piece code: ${pieceCode}`);
|
|
@@ -17,11 +17,23 @@ function normalizePieceUrls(input) {
|
|
|
17
17
|
assertPieceUrlsComplete(normalized);
|
|
18
18
|
return normalized;
|
|
19
19
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
function validateMainRendererConfig(config) {
|
|
21
|
+
const { pieceScale, pieceAnchor, pieceAnchorOffsetY } = config.drag;
|
|
22
|
+
assert(Number.isFinite(pieceScale), 'drag.pieceScale must be a finite number');
|
|
23
|
+
assert(pieceScale > 0, 'drag.pieceScale must be > 0');
|
|
24
|
+
assert(pieceAnchor === 'center' || pieceAnchor === 'bottom', `drag.pieceAnchor must be 'center' or 'bottom', received: ${String(pieceAnchor)}`);
|
|
25
|
+
assert(Number.isFinite(pieceAnchorOffsetY), 'drag.pieceAnchorOffsetY must be a finite number');
|
|
26
|
+
const { durationMs } = config.animation;
|
|
27
|
+
assert(Number.isFinite(durationMs), 'animation.durationMs must be a finite number');
|
|
28
|
+
assert(durationMs >= 0, 'animation.durationMs must be >= 0');
|
|
29
|
+
}
|
|
30
|
+
export function normalizeMainRendererConfig(input, base) {
|
|
31
|
+
const { pieceUrls: inputPieceUrls, ...rest } = input ?? {};
|
|
32
|
+
const merged = toMerged(base, rest);
|
|
33
|
+
const result = {
|
|
34
|
+
...merged,
|
|
35
|
+
pieceUrls: inputPieceUrls !== undefined ? normalizePieceUrls(inputPieceUrls) : merged.pieceUrls
|
|
26
36
|
};
|
|
37
|
+
validateMainRendererConfig(result);
|
|
38
|
+
return result;
|
|
27
39
|
}
|