@wodzik/cubecore 0.1.1 → 0.1.2
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/element/player.d.ts +11 -0
- package/element/player.js +65 -4
- package/element/styles.d.ts +4 -4
- package/element/styles.js +5 -4
- package/package.json +1 -1
package/element/player.d.ts
CHANGED
|
@@ -74,6 +74,8 @@ export declare class CubePlayer extends ElementBase {
|
|
|
74
74
|
private liveSource;
|
|
75
75
|
private turnArrows;
|
|
76
76
|
private clock;
|
|
77
|
+
/** How long a move animates (recording ms) — the clock's, and a step's. */
|
|
78
|
+
private maxAnimMs;
|
|
77
79
|
private unsubscribe;
|
|
78
80
|
private rec;
|
|
79
81
|
private start;
|
|
@@ -144,8 +146,17 @@ export declare class CubePlayer extends ElementBase {
|
|
|
144
146
|
get applied(): number;
|
|
145
147
|
/** The moves being played (the algorithm, or the recording's moves). */
|
|
146
148
|
get moves(): Move[];
|
|
149
|
+
/** The next move, animated (as it turns when playing, at the current speed). */
|
|
147
150
|
stepForward(): void;
|
|
151
|
+
/** The last move undone, animated backwards. */
|
|
148
152
|
stepBack(): void;
|
|
153
|
+
private stepFrame;
|
|
154
|
+
private stepEnd;
|
|
155
|
+
/** Run the clock from `from` to `to` (recording ms) in real time, then settle on `end`. */
|
|
156
|
+
private animateStep;
|
|
157
|
+
/** A step still animating: jump to where it ends (another step, or anything else, comes next). */
|
|
158
|
+
private finishStep;
|
|
159
|
+
private stopStep;
|
|
149
160
|
toStart(): void;
|
|
150
161
|
toEnd(): void;
|
|
151
162
|
/** Follow a smart cube: its moves animate, resyncs jump, its gyro turns the cube. Returns detach. */
|
package/element/player.js
CHANGED
|
@@ -42,7 +42,7 @@ import { applyMoves, parseAlg, solvedState, spinsAfter } from "../core/index.js"
|
|
|
42
42
|
import { renderSvg } from "../image/index.js";
|
|
43
43
|
import { CubeRenderer, showPosition } from "../render/index.js";
|
|
44
44
|
import { SKINS } from "../skin/index.js";
|
|
45
|
-
import { ReplayClock, compressPauses, segmentAt, segmentPlayed, stageSegments } from "../timeline/index.js";
|
|
45
|
+
import { ReplayClock, compressPauses, moveWindows, segmentAt, segmentPlayed, stageSegments } from "../timeline/index.js";
|
|
46
46
|
import { formatTime, fraction, segmentLines, segmentText, startMoves, stepTime, tempoRecording } from "./model.js";
|
|
47
47
|
import { ICONS, STYLES } from "./styles.js";
|
|
48
48
|
import { ElementBase } from "./base.js";
|
|
@@ -83,6 +83,8 @@ export class CubePlayer extends ElementBase {
|
|
|
83
83
|
liveSource = null;
|
|
84
84
|
turnArrows = null;
|
|
85
85
|
clock = null;
|
|
86
|
+
/** How long a move animates (recording ms) — the clock's, and a step's. */
|
|
87
|
+
maxAnimMs = 150;
|
|
86
88
|
unsubscribe = null;
|
|
87
89
|
rec = { scramble: [], moves: [], totalMs: 0 };
|
|
88
90
|
start = solvedState();
|
|
@@ -122,6 +124,7 @@ export class CubePlayer extends ElementBase {
|
|
|
122
124
|
}
|
|
123
125
|
disconnectedCallback() {
|
|
124
126
|
this.connected = false;
|
|
127
|
+
this.stopStep();
|
|
125
128
|
this.darkQuery?.removeEventListener("change", this.onScheme);
|
|
126
129
|
this.clock?.pause();
|
|
127
130
|
this.unsubscribe?.();
|
|
@@ -282,6 +285,7 @@ export class CubePlayer extends ElementBase {
|
|
|
282
285
|
play() {
|
|
283
286
|
if (!this.clock || this.playing)
|
|
284
287
|
return;
|
|
288
|
+
this.finishStep();
|
|
285
289
|
this.clock.play();
|
|
286
290
|
this.dispatchEvent(new Event("play"));
|
|
287
291
|
this.updateUi(this.currentTime);
|
|
@@ -300,6 +304,7 @@ export class CubePlayer extends ElementBase {
|
|
|
300
304
|
this.play();
|
|
301
305
|
}
|
|
302
306
|
seek(ms) {
|
|
307
|
+
this.stopStep();
|
|
303
308
|
this.clock?.seek(ms);
|
|
304
309
|
}
|
|
305
310
|
/** Jump to the moment `k` moves are done (0 = the start). */
|
|
@@ -315,13 +320,67 @@ export class CubePlayer extends ElementBase {
|
|
|
315
320
|
get moves() {
|
|
316
321
|
return this.rec.moves.map((m) => m.move);
|
|
317
322
|
}
|
|
323
|
+
/** The next move, animated (as it turns when playing, at the current speed). */
|
|
318
324
|
stepForward() {
|
|
319
325
|
this.pause();
|
|
320
|
-
this.
|
|
321
|
-
|
|
326
|
+
this.finishStep();
|
|
327
|
+
const time = this.currentTime;
|
|
328
|
+
const target = stepTime(this.rec, time, 1);
|
|
329
|
+
const k = this.rec.moves.findIndex((m) => m.t === target);
|
|
330
|
+
if (k < 0)
|
|
331
|
+
return this.seek(target);
|
|
332
|
+
const [from, to] = moveWindows(this.rec, { maxAnimMs: this.maxAnimMs })[k];
|
|
333
|
+
this.animateStep(Math.max(time, from), to, to);
|
|
334
|
+
}
|
|
335
|
+
/** The last move undone, animated backwards. */
|
|
322
336
|
stepBack() {
|
|
323
337
|
this.pause();
|
|
324
|
-
this.
|
|
338
|
+
this.finishStep();
|
|
339
|
+
const time = this.currentTime;
|
|
340
|
+
const target = stepTime(this.rec, time, -1);
|
|
341
|
+
// The move being undone: the last one finished by now.
|
|
342
|
+
let k = -1;
|
|
343
|
+
this.rec.moves.forEach((m, i) => {
|
|
344
|
+
if (m.t <= time + 0.5)
|
|
345
|
+
k = i;
|
|
346
|
+
});
|
|
347
|
+
if (k < 0)
|
|
348
|
+
return this.seek(target);
|
|
349
|
+
const [from, to] = moveWindows(this.rec, { maxAnimMs: this.maxAnimMs })[k];
|
|
350
|
+
this.animateStep(Math.min(time, to), from, target);
|
|
351
|
+
}
|
|
352
|
+
stepFrame = 0;
|
|
353
|
+
stepEnd = null;
|
|
354
|
+
/** Run the clock from `from` to `to` (recording ms) in real time, then settle on `end`. */
|
|
355
|
+
animateStep(from, to, end) {
|
|
356
|
+
const clock = this.clock;
|
|
357
|
+
if (!clock)
|
|
358
|
+
return;
|
|
359
|
+
clock.seek(from);
|
|
360
|
+
// At least a quarter second: a step is for looking at the move.
|
|
361
|
+
const duration = Math.max(250, Math.abs(to - from) / (this._rate || 1));
|
|
362
|
+
const started = performance.now();
|
|
363
|
+
this.stepEnd = end;
|
|
364
|
+
const tick = () => {
|
|
365
|
+
const k = Math.min(1, (performance.now() - started) / duration);
|
|
366
|
+
if (k >= 1)
|
|
367
|
+
return this.finishStep();
|
|
368
|
+
clock.seek(from + (to - from) * k);
|
|
369
|
+
this.stepFrame = requestAnimationFrame(tick);
|
|
370
|
+
};
|
|
371
|
+
this.stepFrame = requestAnimationFrame(tick);
|
|
372
|
+
}
|
|
373
|
+
/** A step still animating: jump to where it ends (another step, or anything else, comes next). */
|
|
374
|
+
finishStep() {
|
|
375
|
+
if (this.stepEnd === null)
|
|
376
|
+
return;
|
|
377
|
+
const end = this.stepEnd;
|
|
378
|
+
this.stopStep();
|
|
379
|
+
this.clock?.seek(end);
|
|
380
|
+
}
|
|
381
|
+
stopStep() {
|
|
382
|
+
cancelAnimationFrame(this.stepFrame);
|
|
383
|
+
this.stepEnd = null;
|
|
325
384
|
}
|
|
326
385
|
toStart() {
|
|
327
386
|
this.pause();
|
|
@@ -400,6 +459,7 @@ export class CubePlayer extends ElementBase {
|
|
|
400
459
|
this.clock?.pause();
|
|
401
460
|
this.unsubscribe?.();
|
|
402
461
|
let maxAnimMs = 150;
|
|
462
|
+
this.stopStep();
|
|
403
463
|
let solves = [];
|
|
404
464
|
try {
|
|
405
465
|
if (this._recording) {
|
|
@@ -426,6 +486,7 @@ export class CubePlayer extends ElementBase {
|
|
|
426
486
|
this.start = this._setupState ?? solvedState();
|
|
427
487
|
this.dispatchEvent(new CustomEvent("error", { detail: { message: err instanceof Error ? err.message : String(err) } }));
|
|
428
488
|
}
|
|
489
|
+
this.maxAnimMs = maxAnimMs;
|
|
429
490
|
this.clock = new ReplayClock(this.rec, { maxAnimMs });
|
|
430
491
|
this.clock.rate = this._rate;
|
|
431
492
|
this.unsubscribe = this.clock.onChange((time, pos) => this.onTime(time, pos));
|
package/element/styles.d.ts
CHANGED
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
export declare const STYLES = "\n:host {\n --cc-accent: #4f8cff;\n --cc-control-bg: color-mix(in srgb, currentColor 8%, transparent);\n --cc-control-bg-hover: color-mix(in srgb, currentColor 15%, transparent);\n --cc-control-fg: currentColor;\n --cc-control-radius: 10px;\n --cc-button-size: 34px;\n --cc-controls-gap: 5px;\n --cc-progress-height: 6px;\n --cc-progress-track: color-mix(in srgb, currentColor 14%, transparent);\n --cc-progress-fill: var(--cc-accent);\n --cc-marker: color-mix(in srgb, currentColor 55%, transparent);\n --cc-thumb-size: 14px;\n --cc-font: ui-monospace, \"SF Mono\", Menlo, monospace;\n /* sections of the progress bar */\n --cc-segment-1: #4f8cff;\n --cc-segment-2: #22c55e;\n --cc-segment-3: #f59e0b;\n --cc-segment-4: #ef4444;\n --cc-segment-5: #a855f7;\n --cc-segment-6: #14b8a6;\n --cc-segment-7: #ec4899;\n --cc-segment-8: #eab308;\n --cc-segment-height: calc(var(--cc-progress-height) + 2px);\n --cc-segment-gap: 2px;\n --cc-segment-unplayed: 30%;\n /* popup: dark and solid by default \u2014 readable on light and dark pages and over the cube */\n --cc-tooltip-bg: rgb(17 20 28 / 0.97);\n --cc-tooltip-fg: #f4f5f7;\n --cc-tooltip-muted: #a9afbb;\n --cc-tooltip-border: rgb(255 255 255 / 0.12);\n display: flex;\n flex-direction: column;\n gap: 10px;\n min-width: 200px;\n min-height: 200px;\n outline: none;\n container-type: inline-size;\n}\n:host([hidden]) { display: none; }\n.stage { position: relative; flex: 1; min-height: 0; }\n.flat { position: absolute; inset: 0; display: grid; place-items: center; }\n.flat[hidden] { display: none; }\n.flat svg { width: 100%; height: 100%; }\n\n/* \u2500\u2500\u2500 progress bar \u2500\u2500\u2500 */\n.progress { display: none; padding: 6px 2px; cursor: pointer; touch-action: none; }\n:host([progress]) .progress { display: block; }\n.track {\n position: relative;\n height: var(--cc-progress-height);\n border-radius: 999px;\n background: var(--cc-progress-track);\n}\n.fill {\n position: absolute; inset: 0 auto 0 0;\n width: 0;\n border-radius: inherit;\n background: var(--cc-progress-fill);\n}\n.thumb {\n position: absolute; top: 50%; left: 0;\n width: var(--cc-thumb-size); height: var(--cc-thumb-size);\n border-radius: 50%;\n background: var(--cc-progress-fill);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--cc-progress-fill) 25%, transparent);\n transform: translate(-50%, -50%) scale(0.85);\n transition: transform 0.12s ease;\n}\n.progress:hover .thumb, .progress.dragging .thumb { transform: translate(-50%, -50%) scale(1); }\n/* Sections: coloured parts of the bar, dim until played, recognition hatched. */\n.progress { position: relative; }\n.progress.has-segments .fill { display: none; }\n.segments { position: absolute; inset: 0; }\n.segment {\n position: absolute; top: 50%;\n height: var(--cc-segment-height);\n transform: translateY(-50%);\n overflow: hidden;\n background: color-mix(in srgb, var(--seg) var(--cc-segment-unplayed), transparent);\n clip-path: inset(0 calc(var(--cc-segment-gap) / 2) 0 calc(var(--cc-segment-gap) / 2) round 999px);\n}\n.segment .played { position: absolute; inset: 0 auto 0 0; width: 0; background: var(--seg); }\n.segment .recognition {\n position: absolute; inset: 0 auto 0 0;\n background: repeating-linear-gradient(135deg, color-mix(in srgb, Canvas 50%, transparent) 0 2px, transparent 2px 5px);\n}\n.progress.has-segments .thumb { background: currentColor; box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 20%, transparent); }\n.tooltip {\n position: absolute; bottom: calc(100% + 4px); left: 0;\n transform: translateX(-50%);\n width: max-content; max-width: min(320px, 100%);\n padding: 6px 9px;\n border-radius: 8px;\n background: var(--cc-tooltip-bg);\n border: 1px solid var(--cc-tooltip-border);\n font: 500 12px/1.4 system-ui, sans-serif;\n pointer-events: none;\n z-index: 2;\n box-shadow: 0 8px 24px -8px rgb(0 0 0 / 0.6);\n display: flex; flex-direction: column; gap: 1px;\n}\n.tooltip .tip { color: var(--cc-tooltip-fg); }\n.tooltip .tip-title { font-weight: 650; font-variant-numeric: tabular-nums; }\n.tooltip .tip-details { color: var(--cc-tooltip-muted); font-size: 11px; font-variant-numeric: tabular-nums; }\n.tooltip[hidden] { display: none; }\n.labels { display: none; position: relative; height: 18px; margin-top: -4px; }\n:host([segment-labels]) .labels { display: block; }\n.label {\n position: absolute; top: 0;\n min-width: 0; height: 18px; padding: 0 2px;\n background: none; border-radius: 4px;\n color: inherit; opacity: 0.6;\n font: 500 11px/18px system-ui, sans-serif;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n}\n.label:hover { background: var(--cc-control-bg); opacity: 1; transform: none; }\n.label:active { transform: none; }\n.label.current { opacity: 1; font-weight: 700; }\n.marker {\n position: absolute; top: 50%;\n width: 2px; height: calc(var(--cc-progress-height) + 8px);\n border-radius: 1px;\n background: var(--cc-marker);\n transform: translate(-50%, -50%);\n pointer-events: none;\n}\n:host(:not([markers])) .marker { display: none; }\n.progress:focus-visible .track { outline: 2px solid var(--cc-accent); outline-offset: 4px; }\n\n/* \u2500\u2500\u2500 controls \u2500\u2500\u2500 */\n/* time | buttons | speed \u2014 the buttons stay centred whatever the time text's width */\n.controls { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: var(--cc-controls-gap); }\n.buttons { display: flex; gap: var(--cc-controls-gap); }\n.time { justify-self: start; }\n.speed { justify-self: end; }\n/* Narrow players: buttons on top, time and speed below. */\n@container (max-width: 300px) {\n .controls { grid-template-columns: 1fr 1fr; grid-template-areas: \"buttons buttons\" \"time speed\"; }\n .buttons { grid-area: buttons; justify-content: center; }\n .time { grid-area: time; }\n .speed { grid-area: speed; }\n}\n:host([controls=\"none\"]) .controls { display: none; }\n/* Live (following a smart cube): nothing to replay. */\n:host([live]) .controls, :host([live]) .progress { display: none; }\nbutton {\n display: inline-grid; place-items: center;\n min-width: var(--cc-button-size); height: var(--cc-button-size);\n padding: 0 8px;\n border: 0;\n border-radius: var(--cc-control-radius);\n background: var(--cc-control-bg);\n color: var(--cc-control-fg);\n font: 600 12px/1 var(--cc-font);\n cursor: pointer;\n transition: background 0.12s ease, transform 0.08s ease;\n}\nbutton:hover { background: var(--cc-control-bg-hover); }\nbutton:active { transform: scale(0.94); }\nbutton:focus-visible { outline: 2px solid var(--cc-accent); outline-offset: 2px; }\nbutton:disabled { opacity: 0.4; cursor: default; transform: none; }\n/* Icons never take the click: it always lands on the button itself, even if the icon changes mid-click. */\nbutton svg { width: 18px; height: 18px; display: block; fill: currentColor; pointer-events: none; }\n.play { min-width: calc(var(--cc-button-size) * 1.25); background: var(--cc-accent); color: #fff; }\n.play:hover { background: color-mix(in srgb, var(--cc-accent) 85%, #000); }\n.time { font: 500 12px/1 var(--cc-font); opacity: 0.75; white-space: nowrap; font-variant-numeric: tabular-nums; }\n";
|
|
24
24
|
/** Inline icons (no icon font, no network). */
|
|
25
25
|
export declare const ICONS: {
|
|
26
|
-
readonly start: "<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"
|
|
27
|
-
readonly back: "<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"
|
|
26
|
+
readonly start: "<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M4 5h2v14H4zM13 6.3v11.4a.5.5 0 0 1-.8.4L6.9 12.4a.5.5 0 0 1 0-.8l5.3-5.7a.5.5 0 0 1 .8.4zM20.5 6.3v11.4a.5.5 0 0 1-.8.4l-5.3-5.7a.5.5 0 0 1 0-.8l5.3-5.7a.5.5 0 0 1 .8.4z\"/></svg>";
|
|
27
|
+
readonly back: "<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M16 6.2v11.6a.5.5 0 0 1-.8.4l-7.7-5.8a.5.5 0 0 1 0-.8l7.7-5.8a.5.5 0 0 1 .8.4z\"/></svg>";
|
|
28
28
|
readonly play: "<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M8 5.6v12.8a.6.6 0 0 0 .92.5l10.1-6.4a.6.6 0 0 0 0-1L8.92 5.1a.6.6 0 0 0-.92.5z\"/></svg>";
|
|
29
29
|
readonly pause: "<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><rect x=\"6.5\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\"/><rect x=\"13.5\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\"/></svg>";
|
|
30
|
-
readonly forward: "<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"
|
|
31
|
-
readonly end: "<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"
|
|
30
|
+
readonly forward: "<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M8 6.2v11.6a.5.5 0 0 0 .8.4l7.7-5.8a.5.5 0 0 0 0-.8L8.8 5.8a.5.5 0 0 0-.8.4z\"/></svg>";
|
|
31
|
+
readonly end: "<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M18 5h2v14h-2zM3.5 6.3v11.4a.5.5 0 0 0 .8.4l5.3-5.7a.5.5 0 0 0 0-.8L4.3 5.9a.5.5 0 0 0-.8.4zM11 6.3v11.4a.5.5 0 0 0 .8.4l5.3-5.7a.5.5 0 0 0 0-.8l-5.3-5.7a.5.5 0 0 0-.8.4z\"/></svg>";
|
|
32
32
|
};
|
package/element/styles.js
CHANGED
|
@@ -191,10 +191,11 @@ button svg { width: 18px; height: 18px; display: block; fill: currentColor; poin
|
|
|
191
191
|
`;
|
|
192
192
|
/** Inline icons (no icon font, no network). */
|
|
193
193
|
export const ICONS = {
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
// To the start / end: two triangles against a bar; one move back / on: a single triangle (as cubing.js).
|
|
195
|
+
start: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 5h2v14H4zM13 6.3v11.4a.5.5 0 0 1-.8.4L6.9 12.4a.5.5 0 0 1 0-.8l5.3-5.7a.5.5 0 0 1 .8.4zM20.5 6.3v11.4a.5.5 0 0 1-.8.4l-5.3-5.7a.5.5 0 0 1 0-.8l5.3-5.7a.5.5 0 0 1 .8.4z"/></svg>',
|
|
196
|
+
back: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M16 6.2v11.6a.5.5 0 0 1-.8.4l-7.7-5.8a.5.5 0 0 1 0-.8l7.7-5.8a.5.5 0 0 1 .8.4z"/></svg>',
|
|
196
197
|
play: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5.6v12.8a.6.6 0 0 0 .92.5l10.1-6.4a.6.6 0 0 0 0-1L8.92 5.1a.6.6 0 0 0-.92.5z"/></svg>',
|
|
197
198
|
pause: '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="6.5" y="5" width="4" height="14" rx="1"/><rect x="13.5" y="5" width="4" height="14" rx="1"/></svg>',
|
|
198
|
-
forward: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="
|
|
199
|
-
end: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="
|
|
199
|
+
forward: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 6.2v11.6a.5.5 0 0 0 .8.4l7.7-5.8a.5.5 0 0 0 0-.8L8.8 5.8a.5.5 0 0 0-.8.4z"/></svg>',
|
|
200
|
+
end: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M18 5h2v14h-2zM3.5 6.3v11.4a.5.5 0 0 0 .8.4l5.3-5.7a.5.5 0 0 0 0-.8L4.3 5.9a.5.5 0 0 0-.8.4zM11 6.3v11.4a.5.5 0 0 0 .8.4l5.3-5.7a.5.5 0 0 0 0-.8l-5.3-5.7a.5.5 0 0 0-.8.4z"/></svg>',
|
|
200
201
|
};
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.2",
|
|
7
7
|
"description": "A 3×3×3 cube library for the web, built around smart (Bluetooth) cubes: state, notation, methods, solvers and scrambles, a three.js renderer with skins, pictures, web components, React bindings.",
|
|
8
8
|
"license": "MPL-2.0",
|
|
9
9
|
"author": "Tomasz Wodzikowski <wodziszczakow@gmail.com>",
|