@wodzik/cubecore 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.seek(stepTime(this.rec, this.currentTime, 1));
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.seek(stepTime(this.rec, this.currentTime, -1));
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));
@@ -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=\"M6 5h2v14H6zM20 5.5v13a.5.5 0 0 1-.78.42L9.5 12.4a.5.5 0 0 1 0-.8l9.72-6.52A.5.5 0 0 1 20 5.5z\"/></svg>";
27
- readonly back: "<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M17 6.2v11.6a.5.5 0 0 1-.8.4L8 12.4a.5.5 0 0 1 0-.8l8.2-5.8a.5.5 0 0 1 .8.4zM6 6h2v12H6z\"/></svg>";
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=\"M7 6.2v11.6a.5.5 0 0 0 .8.4l8.2-5.8a.5.5 0 0 0 0-.8L7.8 5.8a.5.5 0 0 0-.8.4zM16 6h2v12h-2z\"/></svg>";
31
- readonly end: "<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M16 5h2v14h-2zM4 5.5v13a.5.5 0 0 0 .78.42l9.72-6.52a.5.5 0 0 0 0-.8L4.78 5.08A.5.5 0 0 0 4 5.5z\"/></svg>";
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
- start: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 5h2v14H6zM20 5.5v13a.5.5 0 0 1-.78.42L9.5 12.4a.5.5 0 0 1 0-.8l9.72-6.52A.5.5 0 0 1 20 5.5z"/></svg>',
195
- back: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M17 6.2v11.6a.5.5 0 0 1-.8.4L8 12.4a.5.5 0 0 1 0-.8l8.2-5.8a.5.5 0 0 1 .8.4zM6 6h2v12H6z"/></svg>',
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="M7 6.2v11.6a.5.5 0 0 0 .8.4l8.2-5.8a.5.5 0 0 0 0-.8L7.8 5.8a.5.5 0 0 0-.8.4zM16 6h2v12h-2z"/></svg>',
199
- end: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M16 5h2v14h-2zM4 5.5v13a.5.5 0 0 0 .78.42l9.72-6.52a.5.5 0 0 0 0-.8L4.78 5.08A.5.5 0 0 0 4 5.5z"/></svg>',
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.1",
6
+ "version": "0.1.3",
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>",
@@ -106,7 +106,9 @@
106
106
  },
107
107
  "sideEffects": [
108
108
  "./element/index.js",
109
- "./react/index.js"
109
+ "./react/index.js",
110
+ "./skin/attachments.js",
111
+ "./bluetooth/vendor/**"
110
112
  ],
111
113
  "dependencies": {
112
114
  "aes-js": "^3.1.2",