@mirasen/chessboard 1.2.4 → 1.3.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 +36 -0
- package/README.md +9 -0
- 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/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,41 @@
|
|
|
1
1
|
# @mirasen/chessboard
|
|
2
2
|
|
|
3
|
+
## 1.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- eaed694: Add configurable desktop and mobile drag behavior.
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
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:
|
|
12
|
+
- `drag.pieceScale`
|
|
13
|
+
- `drag.pieceAnchor`
|
|
14
|
+
- `drag.pieceAnchorOffsetY`
|
|
15
|
+
- `animation.durationMs`
|
|
16
|
+
|
|
17
|
+
The package also exports public desktop/mobile main-renderer defaults from `@mirasen/chessboard/extensions` as `DefaultMainRendererDesktopConfig` and `DefaultMainRendererMobileConfig`.
|
|
18
|
+
|
|
19
|
+
Set `animation.durationMs` to `0` to disable piece movement animation by skipping animation creation.
|
|
20
|
+
|
|
21
|
+
Custom extension authors may need to update runtime interaction action names and payload fields to the new drag-session naming.
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- dde200f: dependabot: directory '/', update eslint
|
|
26
|
+
- dfe6882: dependabot: directory '/', update typescript from 5.9.3 to 6.0.3
|
|
27
|
+
- eb1fcb5: dependabot: directory '/', update @types/node
|
|
28
|
+
- eb1fcb5: dependabot: directory '/', update @vitest/browser-playwright
|
|
29
|
+
- eb1fcb5: dependabot: directory '/', update @vitest/coverage-istanbul
|
|
30
|
+
- eb1fcb5: dependabot: directory '/', update svelte
|
|
31
|
+
- eb1fcb5: dependabot: directory '/', update typescript-eslint
|
|
32
|
+
- eb1fcb5: dependabot: directory '/', update vitest
|
|
33
|
+
- d1da3c1: Prevent fast mouse dragging from triggering native browser drag-and-drop on SVG piece images.
|
|
34
|
+
|
|
35
|
+
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`.
|
|
36
|
+
|
|
37
|
+
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.
|
|
38
|
+
|
|
3
39
|
## 1.2.4
|
|
4
40
|
|
|
5
41
|
### 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:
|
|
@@ -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
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { isDragSessionActiveLiftedPiece } from '../../../../state/interaction/helpers.js';
|
|
1
2
|
export function calculateSuppressedSquares(_state, context, animationSuppressedSquares) {
|
|
2
3
|
const dragSession = context.currentFrame.state.interaction.dragSession;
|
|
3
|
-
const dragSquare = dragSession
|
|
4
|
+
const dragSquare = dragSession && isDragSessionActiveLiftedPiece(dragSession) ? dragSession.sourceSquare : null;
|
|
4
5
|
const result = new Set(animationSuppressedSquares);
|
|
5
6
|
if (dragSquare !== null) {
|
|
6
7
|
result.add(dragSquare);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { denormalizeMainRendererConfig } from './denormalize.js';
|
|
2
|
+
import { DefaultMainRendererDesktopConfig, DefaultMainRendererMobileConfig } from './types/internal.js';
|
|
3
|
+
export const DefaultMainRendererDesktopConfigPublic = denormalizeMainRendererConfig(DefaultMainRendererDesktopConfig);
|
|
4
|
+
export const DefaultMainRendererMobileConfigPublic = denormalizeMainRendererConfig(DefaultMainRendererMobileConfig);
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { ExtensionDefinition, ExtensionInstance } from '../../../types/extension.js';
|
|
2
|
-
import {
|
|
3
|
-
export type MainRendererInitOptions = Partial<MainRendererConfigInput>;
|
|
1
|
+
import type { ExtensionDefinition, ExtensionInstance } from '../../../types/extension.js';
|
|
2
|
+
import type { MainRendererConfigPublic, MainRendererSetConfigOptions } from './public.js';
|
|
4
3
|
export declare const EXTENSION_SLOTS: readonly ["defs", "board", "coordinates", "pieces", "animation", "drag"];
|
|
5
4
|
export type ExtensionSlotsType = typeof EXTENSION_SLOTS;
|
|
6
5
|
export declare const EXTENSION_ID: "renderer";
|
|
7
|
-
export
|
|
8
|
-
|
|
6
|
+
export interface RendererPublicAPI {
|
|
7
|
+
setConfig(options: MainRendererSetConfigOptions): void;
|
|
8
|
+
getConfig(): MainRendererConfigPublic;
|
|
9
|
+
}
|
|
10
|
+
export type MainRendererDefinition = ExtensionDefinition<typeof EXTENSION_ID, typeof EXTENSION_SLOTS, RendererPublicAPI>;
|
|
11
|
+
export type MainRendererInstance = ExtensionInstance<typeof EXTENSION_ID, typeof EXTENSION_SLOTS, RendererPublicAPI>;
|
|
9
12
|
export declare enum DirtyLayer {
|
|
10
13
|
Board = 1,// 1 << 0,
|
|
11
14
|
Coordinates = 2,// 1 << 1,
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { ExtensionRuntimeSurface } from '../../../types/surface/main.js';
|
|
2
|
-
import { ExtensionInternalBase } from '../../common/types.js';
|
|
3
|
-
import { MainRendererAnimation } from '../animation/types.js';
|
|
4
|
-
import { MainRendererBoard } from '../board/types.js';
|
|
5
|
-
import { MainRendererCoordinates } from '../coordinates/types.js';
|
|
6
|
-
import { MainRendererDrag } from '../drag/types.js';
|
|
1
|
+
import type { ExtensionRuntimeSurface } from '../../../types/surface/main.js';
|
|
2
|
+
import type { ExtensionInternalBase } from '../../common/types.js';
|
|
3
|
+
import type { MainRendererAnimation } from '../animation/types.js';
|
|
4
|
+
import type { MainRendererBoard } from '../board/types.js';
|
|
5
|
+
import type { MainRendererCoordinates } from '../coordinates/types.js';
|
|
6
|
+
import type { MainRendererDrag } from '../drag/types.js';
|
|
7
7
|
import type { PieceSymbolResolver } from '../piece-symbols.js';
|
|
8
|
-
import { MainRendererPieces } from '../pieces/types.js';
|
|
9
|
-
import { ExtensionSlotsType } from './extension.js';
|
|
8
|
+
import type { MainRendererPieces } from '../pieces/types.js';
|
|
9
|
+
import type { ExtensionSlotsType } from './extension.js';
|
|
10
|
+
import type { MainRendererConfig } from './internal.js';
|
|
10
11
|
export interface MainRendererInstanceInternal extends ExtensionInternalBase<ExtensionSlotsType> {
|
|
11
12
|
readonly board: MainRendererBoard;
|
|
12
13
|
readonly coordinates: MainRendererCoordinates;
|
|
@@ -15,4 +16,5 @@ export interface MainRendererInstanceInternal extends ExtensionInternalBase<Exte
|
|
|
15
16
|
readonly animation: MainRendererAnimation;
|
|
16
17
|
readonly runtimeSurface: ExtensionRuntimeSurface;
|
|
17
18
|
readonly pieceSymbolResolver: PieceSymbolResolver;
|
|
19
|
+
config: MainRendererConfig;
|
|
18
20
|
}
|