@wodzik/cubecore 0.1.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.
Files changed (279) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +118 -0
  3. package/analyze/client.d.ts +18 -0
  4. package/analyze/client.js +42 -0
  5. package/analyze/index.d.ts +114 -0
  6. package/analyze/index.js +151 -0
  7. package/analyze/rotations.d.ts +3 -0
  8. package/analyze/rotations.js +15 -0
  9. package/analyze/roux.d.ts +67 -0
  10. package/analyze/roux.js +95 -0
  11. package/analyze/worker.d.ts +6 -0
  12. package/analyze/worker.js +31 -0
  13. package/analyze/zz.d.ts +47 -0
  14. package/analyze/zz.js +102 -0
  15. package/bld/index.d.ts +14 -0
  16. package/bld/index.js +14 -0
  17. package/bld/memo.d.ts +66 -0
  18. package/bld/memo.js +139 -0
  19. package/bld/scheme.d.ts +30 -0
  20. package/bld/scheme.js +35 -0
  21. package/bld/skin.d.ts +26 -0
  22. package/bld/skin.js +46 -0
  23. package/bld/tracker.d.ts +68 -0
  24. package/bld/tracker.js +145 -0
  25. package/bluetooth/clock.d.ts +22 -0
  26. package/bluetooth/clock.js +37 -0
  27. package/bluetooth/cubeSkins.d.ts +31 -0
  28. package/bluetooth/cubeSkins.js +32 -0
  29. package/bluetooth/gyro.d.ts +31 -0
  30. package/bluetooth/gyro.js +46 -0
  31. package/bluetooth/index.d.ts +5 -0
  32. package/bluetooth/index.js +5 -0
  33. package/bluetooth/session.d.ts +140 -0
  34. package/bluetooth/session.js +201 -0
  35. package/bluetooth/simulated.d.ts +52 -0
  36. package/bluetooth/simulated.js +80 -0
  37. package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-definitions.d.ts +38 -0
  38. package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-definitions.js +51 -0
  39. package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-encrypter.d.ts +35 -0
  40. package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-encrypter.js +70 -0
  41. package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-protocol.d.ts +273 -0
  42. package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-protocol.js +846 -0
  43. package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen1.d.ts +38 -0
  44. package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen1.js +340 -0
  45. package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen234-packet-validate.d.ts +11 -0
  46. package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen234-packet-validate.js +118 -0
  47. package/bluetooth/vendor/smartcube-web-bluetooth/gan-mac-salt.d.ts +2 -0
  48. package/bluetooth/vendor/smartcube-web-bluetooth/gan-mac-salt.js +15 -0
  49. package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-cube.d.ts +18 -0
  50. package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-cube.js +152 -0
  51. package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-timer.d.ts +74 -0
  52. package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-timer.js +161 -0
  53. package/bluetooth/vendor/smartcube-web-bluetooth/gan-write-queue.d.ts +7 -0
  54. package/bluetooth/vendor/smartcube-web-bluetooth/gan-write-queue.js +11 -0
  55. package/bluetooth/vendor/smartcube-web-bluetooth/gatt-characteristic-write.d.ts +2 -0
  56. package/bluetooth/vendor/smartcube-web-bluetooth/gatt-characteristic-write.js +36 -0
  57. package/bluetooth/vendor/smartcube-web-bluetooth/index.d.ts +4 -0
  58. package/bluetooth/vendor/smartcube-web-bluetooth/index.js +4 -0
  59. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/address-hints.d.ts +12 -0
  60. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/address-hints.js +122 -0
  61. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/build-picker-options.d.ts +11 -0
  62. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/build-picker-options.js +55 -0
  63. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/default-optional-services.d.ts +21 -0
  64. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/default-optional-services.js +47 -0
  65. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/device-name-scan-filters.d.ts +6 -0
  66. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/device-name-scan-filters.js +24 -0
  67. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/gatt-snapshot.d.ts +6 -0
  68. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/gatt-snapshot.js +57 -0
  69. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-candidates.d.ts +11 -0
  70. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-candidates.js +67 -0
  71. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-moyu32.d.ts +8 -0
  72. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-moyu32.js +98 -0
  73. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-qiyi.d.ts +8 -0
  74. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-qiyi.js +128 -0
  75. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/moyu32-session-crypto.d.ts +5 -0
  76. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/moyu32-session-crypto.js +60 -0
  77. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/normalize-uuid.d.ts +2 -0
  78. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/normalize-uuid.js +8 -0
  79. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/packet-sanity.d.ts +7 -0
  80. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/packet-sanity.js +142 -0
  81. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/profile-rank.d.ts +6 -0
  82. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/profile-rank.js +21 -0
  83. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/types.d.ts +32 -0
  84. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/types.js +1 -0
  85. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/ble-utils.d.ts +6 -0
  86. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/ble-utils.js +68 -0
  87. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/connect.d.ts +3 -0
  88. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/connect.js +178 -0
  89. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/cubie-cube.d.ts +16 -0
  90. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/cubie-cube.js +141 -0
  91. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/index.d.ts +11 -0
  92. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/index.js +10 -0
  93. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocol.d.ts +26 -0
  94. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocol.js +8 -0
  95. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gan.d.ts +3 -0
  96. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gan.js +265 -0
  97. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/giiker.d.ts +3 -0
  98. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/giiker.js +345 -0
  99. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gocube.d.ts +15 -0
  100. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gocube.js +382 -0
  101. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-mhc.d.ts +3 -0
  102. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-mhc.js +352 -0
  103. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-v1.d.ts +57 -0
  104. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-v1.js +243 -0
  105. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu32.d.ts +3 -0
  106. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu32.js +454 -0
  107. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/qiyi.d.ts +3 -0
  108. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/qiyi.js +424 -0
  109. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/types.d.ts +78 -0
  110. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/types.js +1 -0
  111. package/bluetooth/vendor/smartcube-web-bluetooth/utils.d.ts +45 -0
  112. package/bluetooth/vendor/smartcube-web-bluetooth/utils.js +150 -0
  113. package/cfop/caseData.d.ts +16 -0
  114. package/cfop/caseData.js +88 -0
  115. package/cfop/cases.d.ts +42 -0
  116. package/cfop/cases.js +116 -0
  117. package/cfop/f2l.d.ts +28 -0
  118. package/cfop/f2l.js +88 -0
  119. package/cfop/f2lData.d.ts +15 -0
  120. package/cfop/f2lData.js +49 -0
  121. package/cfop/index.d.ts +41 -0
  122. package/cfop/index.js +110 -0
  123. package/cfop/slots.d.ts +2 -0
  124. package/cfop/slots.js +1 -0
  125. package/core/arrows.d.ts +24 -0
  126. package/core/arrows.js +22 -0
  127. package/core/bytes.d.ts +27 -0
  128. package/core/bytes.js +145 -0
  129. package/core/centers.d.ts +32 -0
  130. package/core/centers.js +83 -0
  131. package/core/checks.d.ts +53 -0
  132. package/core/checks.js +85 -0
  133. package/core/cubies.d.ts +45 -0
  134. package/core/cubies.js +169 -0
  135. package/core/frames.d.ts +61 -0
  136. package/core/frames.js +126 -0
  137. package/core/geometry.d.ts +58 -0
  138. package/core/geometry.js +132 -0
  139. package/core/index.d.ts +19 -0
  140. package/core/index.js +19 -0
  141. package/core/masks.d.ts +32 -0
  142. package/core/masks.js +39 -0
  143. package/core/method.d.ts +35 -0
  144. package/core/method.js +30 -0
  145. package/core/moves.d.ts +48 -0
  146. package/core/moves.js +88 -0
  147. package/core/notation.d.ts +48 -0
  148. package/core/notation.js +226 -0
  149. package/core/physical.d.ts +53 -0
  150. package/core/physical.js +99 -0
  151. package/core/practice.d.ts +62 -0
  152. package/core/practice.js +87 -0
  153. package/core/sequence.d.ts +90 -0
  154. package/core/sequence.js +157 -0
  155. package/core/stages.d.ts +90 -0
  156. package/core/stages.js +21 -0
  157. package/core/state.d.ts +55 -0
  158. package/core/state.js +140 -0
  159. package/core/stateCodec.d.ts +12 -0
  160. package/core/stateCodec.js +51 -0
  161. package/core/stream.d.ts +45 -0
  162. package/core/stream.js +75 -0
  163. package/core/tracker.d.ts +62 -0
  164. package/core/tracker.js +121 -0
  165. package/element/alg.d.ts +45 -0
  166. package/element/alg.js +150 -0
  167. package/element/base.d.ts +2 -0
  168. package/element/base.js +3 -0
  169. package/element/bld.d.ts +82 -0
  170. package/element/bld.js +222 -0
  171. package/element/index.d.ts +8 -0
  172. package/element/index.js +21 -0
  173. package/element/model.d.ts +36 -0
  174. package/element/model.js +64 -0
  175. package/element/player.d.ts +196 -0
  176. package/element/player.js +680 -0
  177. package/element/practice.d.ts +75 -0
  178. package/element/practice.js +150 -0
  179. package/element/scramble.d.ts +50 -0
  180. package/element/scramble.js +107 -0
  181. package/element/sequence.d.ts +126 -0
  182. package/element/sequence.js +254 -0
  183. package/element/styles.d.ts +32 -0
  184. package/element/styles.js +196 -0
  185. package/image/cache.d.ts +21 -0
  186. package/image/cache.js +66 -0
  187. package/image/index.d.ts +2 -0
  188. package/image/index.js +2 -0
  189. package/image/png-node.d.ts +6 -0
  190. package/image/png-node.js +10 -0
  191. package/image/svg.d.ts +36 -0
  192. package/image/svg.js +215 -0
  193. package/lbl/index.d.ts +10 -0
  194. package/lbl/index.js +31 -0
  195. package/methods/index.d.ts +20 -0
  196. package/methods/index.js +33 -0
  197. package/package.json +133 -0
  198. package/petrus/index.d.ts +11 -0
  199. package/petrus/index.js +27 -0
  200. package/react/index.d.ts +176 -0
  201. package/react/index.js +228 -0
  202. package/render/arrows.d.ts +68 -0
  203. package/render/arrows.js +190 -0
  204. package/render/build/attachments.d.ts +35 -0
  205. package/render/build/attachments.js +94 -0
  206. package/render/build/models.d.ts +9 -0
  207. package/render/build/models.js +27 -0
  208. package/render/build/tiles.d.ts +48 -0
  209. package/render/build/tiles.js +194 -0
  210. package/render/gltf.d.ts +10 -0
  211. package/render/gltf.js +106 -0
  212. package/render/index.d.ts +12 -0
  213. package/render/index.js +12 -0
  214. package/render/layers.d.ts +21 -0
  215. package/render/layers.js +24 -0
  216. package/render/pictures.d.ts +46 -0
  217. package/render/pictures.js +80 -0
  218. package/render/pieceModels.d.ts +38 -0
  219. package/render/pieceModels.js +43 -0
  220. package/render/renderer.d.ts +165 -0
  221. package/render/renderer.js +647 -0
  222. package/render/tile.d.ts +114 -0
  223. package/render/tile.js +390 -0
  224. package/render/viewports.d.ts +25 -0
  225. package/render/viewports.js +26 -0
  226. package/roux/cmll.d.ts +35 -0
  227. package/roux/cmll.js +121 -0
  228. package/roux/cmllData.d.ts +12 -0
  229. package/roux/cmllData.js +49 -0
  230. package/roux/index.d.ts +52 -0
  231. package/roux/index.js +175 -0
  232. package/skin/attachments.d.ts +82 -0
  233. package/skin/attachments.js +110 -0
  234. package/skin/index.d.ts +3 -0
  235. package/skin/index.js +3 -0
  236. package/skin/shapes.d.ts +112 -0
  237. package/skin/shapes.js +147 -0
  238. package/skin/skin.d.ts +911 -0
  239. package/skin/skin.js +412 -0
  240. package/solve/client.d.ts +58 -0
  241. package/solve/client.js +56 -0
  242. package/solve/handle.d.ts +6 -0
  243. package/solve/handle.js +38 -0
  244. package/solve/index.d.ts +8 -0
  245. package/solve/index.js +7 -0
  246. package/solve/lse.d.ts +42 -0
  247. package/solve/lse.js +151 -0
  248. package/solve/pieces.d.ts +18 -0
  249. package/solve/pieces.js +24 -0
  250. package/solve/protocol.d.ts +70 -0
  251. package/solve/protocol.js +16 -0
  252. package/solve/scramble.d.ts +76 -0
  253. package/solve/scramble.js +187 -0
  254. package/solve/stage.d.ts +108 -0
  255. package/solve/stage.js +532 -0
  256. package/solve/twophase.d.ts +36 -0
  257. package/solve/twophase.js +229 -0
  258. package/solve/worker.d.ts +6 -0
  259. package/solve/worker.js +15 -0
  260. package/timeline/codec.d.ts +23 -0
  261. package/timeline/codec.js +84 -0
  262. package/timeline/index.d.ts +7 -0
  263. package/timeline/index.js +7 -0
  264. package/timeline/recording.d.ts +19 -0
  265. package/timeline/recording.js +20 -0
  266. package/timeline/replay.d.ts +68 -0
  267. package/timeline/replay.js +145 -0
  268. package/timeline/segments.d.ts +31 -0
  269. package/timeline/segments.js +39 -0
  270. package/timeline/share.d.ts +30 -0
  271. package/timeline/share.js +62 -0
  272. package/timeline/stats.d.ts +34 -0
  273. package/timeline/stats.js +56 -0
  274. package/timeline/timings.d.ts +30 -0
  275. package/timeline/timings.js +54 -0
  276. package/zz/index.d.ts +29 -0
  277. package/zz/index.js +68 -0
  278. package/zz/seeds.d.ts +9 -0
  279. package/zz/seeds.js +29 -0
@@ -0,0 +1,254 @@
1
+ /**
2
+ * CubeSequenceElement — what <cube-scramble> and <cube-alg-practice> share:
3
+ * a list of moves followed on a smart cube (core SequenceTracker under the
4
+ * hood), with the slip / undo line, a status message, controls, events and
5
+ * one styling vocabulary. Not registered on its own.
6
+ *
7
+ * Three levels of customising, like <cube-player>:
8
+ * 1. CSS variables (on the element or any ancestor):
9
+ * --cc-seq-font, --cc-seq-size, --cc-seq-gap text
10
+ * --cc-seq-done, --cc-seq-current, --cc-seq-current-bg, --cc-seq-partial,
11
+ * --cc-seq-todo, --cc-seq-hidden (dots), --cc-seq-undo,
12
+ * --cc-seq-wrong-way (the move due, turned the wrong way) move colours
13
+ * --cc-accent, --cc-control-bg, --cc-control-radius controls
14
+ * 2. ::part(): container, moves, move, move-done / -current / -partial / -todo /
15
+ * -hidden, undo, undo-label, undo-move, message, controls, button (+ button-<name>),
16
+ * and each element's own (scramble: input, error).
17
+ * 3. Replace pieces: children with slot="undo", slot="message" or
18
+ * slot="controls" take those places (controls="none" hides the default
19
+ * controls); `headless` hides the whole built-in view — build yours
20
+ * from the `progress` event (tokens with text / status / visibility,
21
+ * undo, needsReset, complete, …) and call the element's methods.
22
+ *
23
+ * Arrows: with `arrows` set (the element following a cube — attached, or
24
+ * fed with push()) and a player (`player="id"` of a <cube-player>,
25
+ * or the `player` property), the 3D cube shows the next turn — one arrow per
26
+ * turning layer (two for a wide r), as many heads as quarter turns, in
27
+ * --cc-arrow; after a slip the undo move in --cc-arrow-undo, or in
28
+ * --cc-arrow-wrong-way when the right face went the wrong way.
29
+ * `arrow-shape="circle"` for arcs instead of ribbons along the faces.
30
+ *
31
+ * After a slip the moves give way to the way back: the undo moves in their
32
+ * place (--cc-seq-undo), until the cube is back on track —
33
+ * `undo-display="below"` keeps the moves and shows the undo line under them.
34
+ *
35
+ * Events: progress (detail: the progress), complete.
36
+ */
37
+ import { IDENTITY_FRAME, formatMove, solvedState } from "../core/index.js";
38
+ import { ElementBase } from "./base.js";
39
+ export const SEQUENCE_STYLES = /* css */ `
40
+ :host {
41
+ --cc-seq-font: ui-monospace, "SF Mono", Menlo, monospace;
42
+ --cc-seq-size: 22px;
43
+ --cc-seq-gap: 0.35em;
44
+ --cc-seq-done: color-mix(in srgb, currentColor 35%, transparent);
45
+ --cc-seq-current: currentColor;
46
+ --cc-seq-current-bg: color-mix(in srgb, var(--cc-accent) 24%, transparent);
47
+ --cc-seq-partial: var(--cc-accent);
48
+ --cc-seq-todo: color-mix(in srgb, currentColor 82%, transparent);
49
+ --cc-seq-hidden: color-mix(in srgb, currentColor 45%, transparent);
50
+ --cc-seq-undo: #ff5a5a;
51
+ --cc-seq-wrong-way: #ff9a1f;
52
+ --cc-seq-decoration: color-mix(in srgb, currentColor 70%, transparent);
53
+ --cc-arrow: #2f8bff;
54
+ --cc-arrow-undo: #ff4545;
55
+ --cc-arrow-wrong-way: #ff9a1f;
56
+ --cc-accent: #4f8cff;
57
+ --cc-control-bg: color-mix(in srgb, currentColor 8%, transparent);
58
+ --cc-control-bg-hover: color-mix(in srgb, currentColor 15%, transparent);
59
+ --cc-control-radius: 8px;
60
+ display: block;
61
+ }
62
+ :host([headless]) .container { display: none; }
63
+ .group { display: inline-flex; align-items: baseline; }
64
+ .decoration { color: var(--cc-seq-decoration); }
65
+ .container { display: flex; flex-direction: column; gap: 0.5em; }
66
+ .container.slip .moves { display: none; }
67
+ .container.slip .undo { font-size: var(--cc-seq-size); line-height: 1.5; }
68
+ .moves { display: flex; flex-wrap: wrap; gap: 0.1em var(--cc-seq-gap); font: 600 var(--cc-seq-size) / 1.5 var(--cc-seq-font); }
69
+ .move { padding: 0 0.18em; border-radius: 0.25em; transition: color 0.15s ease, background 0.15s ease; }
70
+ .done { color: var(--cc-seq-done); }
71
+ .current { color: var(--cc-seq-current); background: var(--cc-seq-current-bg); }
72
+ .partial { color: var(--cc-seq-partial); background: var(--cc-seq-current-bg); }
73
+ .wrong-way { color: var(--cc-seq-wrong-way); background: var(--cc-seq-current-bg); }
74
+ .todo { color: var(--cc-seq-todo); }
75
+ .hidden { color: var(--cc-seq-hidden); }
76
+ .undo { display: none; color: var(--cc-seq-undo); gap: var(--cc-seq-gap); flex-wrap: wrap; align-items: baseline; font: 600 calc(var(--cc-seq-size) * 0.85) / 1.4 var(--cc-seq-font); }
77
+ .undo.on { display: flex; }
78
+ .undo-label { font: 600 0.65em/1 system-ui, sans-serif; text-transform: uppercase; letter-spacing: 0.06em; margin-right: 0.3em; }
79
+ .message { display: none; font: 500 13px/1.4 system-ui, sans-serif; opacity: 0.85; }
80
+ .message.on { display: block; }
81
+ .controls { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
82
+ .controls:empty { display: none; }
83
+ :host([controls="none"]) .controls { display: none; }
84
+ button {
85
+ border: 0; border-radius: var(--cc-control-radius);
86
+ background: var(--cc-control-bg); color: inherit;
87
+ padding: 6px 11px; font: 600 12px/1 system-ui, sans-serif; cursor: pointer;
88
+ }
89
+ button:hover { background: var(--cc-control-bg-hover); }
90
+ button:focus-visible, input:focus-visible { outline: 2px solid var(--cc-accent); outline-offset: 2px; }
91
+ button[aria-pressed="true"] { background: color-mix(in srgb, var(--cc-accent) 25%, transparent); }
92
+ `;
93
+ export class CubeSequenceElement extends ElementBase {
94
+ root;
95
+ source = null;
96
+ off = null;
97
+ messagesBase = { undo: "Undo", reset: "Too far off — solve the cube and start again", complete: "" };
98
+ completed = false;
99
+ _player = null;
100
+ /** How the cube is held when the sequence starts (see `frame`). */
101
+ startFrame = IDENTITY_FRAME;
102
+ static observedAttributes = ["arrows", "player", "arrow-shape", "undo-display"];
103
+ attributeChangedCallback() {
104
+ this.update();
105
+ }
106
+ constructor(extraStyles = "", controlsHtml = "") {
107
+ super();
108
+ this.root = this.attachShadow({ mode: "open" });
109
+ this.root.innerHTML = `<style>${SEQUENCE_STYLES}${extraStyles}</style>
110
+ <div class="container" part="container">
111
+ <div class="moves" part="moves"></div>
112
+ <slot name="undo"><div class="undo" part="undo"><span class="undo-label" part="undo-label"></span><span class="undo-moves"></span></div></slot>
113
+ <slot name="message"><div class="message" part="message" role="status"></div></slot>
114
+ <slot name="controls"><div class="controls" part="controls">${controlsHtml}</div></slot>
115
+ </div>`;
116
+ this.root.addEventListener("click", (e) => {
117
+ const action = e.target.closest("[data-action]")?.dataset.action;
118
+ if (action)
119
+ this.onAction(action);
120
+ });
121
+ }
122
+ /** Extra message (e.g. stats) when nothing else is said. */
123
+ extraMessage() {
124
+ return "";
125
+ }
126
+ onAction(_action) { }
127
+ // ─── shared API ───
128
+ /** Follow a smart cube from its current state. Returns detach. */
129
+ attach(source) {
130
+ this.detach();
131
+ this.source = source;
132
+ this.off = source.on("move", (e) => this.push(e.move, e.time));
133
+ this.reset();
134
+ return () => this.detach();
135
+ }
136
+ detach() {
137
+ this.arrowTarget()?.showTurnArrows(null, {}, this);
138
+ this.off?.();
139
+ this.off = null;
140
+ this.source = null;
141
+ }
142
+ /**
143
+ * How the cube is held when the sequence starts (default: its own U on top,
144
+ * F in front) — for an algorithm right after one with a net rotation.
145
+ * Setting it starts again.
146
+ */
147
+ get frame() {
148
+ return this.startFrame;
149
+ }
150
+ set frame(f) {
151
+ this.startFrame = f ?? IDENTITY_FRAME;
152
+ this.reset();
153
+ }
154
+ /** Start again from `start` (default: the attached cube's state, else solved). */
155
+ reset(start) {
156
+ this.completed = false;
157
+ this.startTracking(start ?? this.source?.state ?? solvedState());
158
+ this.update();
159
+ }
160
+ /** A face turn from the cube (called for you when attached). Once complete, further turns are
161
+ * ignored (the solve / next attempt has started) until a new sequence or `reset()`. */
162
+ push(move, time = performance.now()) {
163
+ if (this.completed)
164
+ return;
165
+ const p = this.track(move, time);
166
+ this.update();
167
+ if (p.complete && !this.completed) {
168
+ this.completed = true;
169
+ this.dispatchEvent(new CustomEvent("complete", { detail: p }));
170
+ }
171
+ }
172
+ /** Player for the arrows (else the `player` attribute's id). */
173
+ get player() {
174
+ return this.arrowTarget();
175
+ }
176
+ set player(p) {
177
+ this.arrowTarget()?.showTurnArrows(null, {}, this);
178
+ this._player = p;
179
+ this.update();
180
+ }
181
+ arrowTarget() {
182
+ if (this._player)
183
+ return this._player;
184
+ const id = this.getAttribute("player");
185
+ const el = id ? this.getRootNode().getElementById?.(id) : null;
186
+ return el && "showTurnArrows" in el ? el : null;
187
+ }
188
+ /** Draw the arrows again — e.g. after changing --cc-arrow* (CSS changes don't notify the element). */
189
+ refreshArrows() {
190
+ this.syncArrows();
191
+ }
192
+ syncArrows() {
193
+ const target = this.arrowTarget();
194
+ if (!target)
195
+ return;
196
+ // The page opts in with `arrows` (a cube followed by attach() or push()).
197
+ const turn = this.hasAttribute("arrows") ? this.nextTurn() : null;
198
+ if (!turn)
199
+ return target.showTurnArrows(null, {}, this);
200
+ const css = getComputedStyle(this);
201
+ const color = css.getPropertyValue(turn.kind === "next" ? "--cc-arrow" : `--cc-arrow-${turn.kind}`).trim();
202
+ const shape = this.getAttribute("arrow-shape") === "circle" ? "circle" : "box";
203
+ target.showTurnArrows(turn.arrows, { color: color || undefined, shape }, this);
204
+ }
205
+ get messages() {
206
+ return { ...this.messagesBase };
207
+ }
208
+ set messages(m) {
209
+ this.setMessages(m);
210
+ }
211
+ setMessages(m) {
212
+ this.messagesBase = { ...this.messagesBase, ...m };
213
+ this.update();
214
+ }
215
+ /** Text around written move `index` (none here; <cube-alg-practice> has trigger groups). */
216
+ decorationOf(_index) {
217
+ return undefined;
218
+ }
219
+ // ─── rendering ───
220
+ update() {
221
+ const p = this.progress;
222
+ const deco = (text) => (text ? `<span class="decoration" part="decoration">${escapeHtml(text)}</span>` : "");
223
+ this.root.querySelector(".moves").innerHTML = this.tokens()
224
+ .map((t, i) => {
225
+ const cls = t.visible ? t.status : `${t.status} hidden`;
226
+ const parts = t.visible ? `move move-${t.status}` : `move move-${t.status} move-hidden`;
227
+ const d = this.decorationOf(i);
228
+ const move = `<span class="move ${cls}" part="${parts}">${t.visible ? escapeHtml(t.text) : "•"}</span>`;
229
+ return d ? `<span class="group">${deco(d.prefix)}${move}${deco(d.suffix)}</span>` : move;
230
+ })
231
+ .join("");
232
+ const showUndo = !!p && p.undo.length > 0 && !p.needsReset;
233
+ // A real slip: the moves give way to the way back. The move due just turned the wrong way: it turns orange instead.
234
+ const slip = showUndo && !p.wrongWay;
235
+ this.root.querySelector(".container")?.classList.toggle("slip", slip && this.getAttribute("undo-display") !== "below");
236
+ this.root.querySelector(".undo")?.classList.toggle("on", slip);
237
+ const label = this.root.querySelector(".undo-label");
238
+ if (label)
239
+ label.textContent = this.messagesBase.undo;
240
+ const undoMoves = this.root.querySelector(".undo-moves");
241
+ if (undoMoves)
242
+ undoMoves.innerHTML = showUndo ? p.undo.map((m) => `<span class="move" part="undo-move">${formatMove(m)}</span>`).join(" ") : "";
243
+ const message = this.root.querySelector(".message");
244
+ if (message) {
245
+ const text = p?.needsReset ? this.messagesBase.reset : p?.complete && this.messagesBase.complete ? this.messagesBase.complete : this.extraMessage();
246
+ message.textContent = text;
247
+ message.classList.toggle("on", !!text);
248
+ }
249
+ this.syncArrows();
250
+ if (p)
251
+ this.dispatchEvent(new CustomEvent("progress", { detail: { ...p, shown: this.tokens() } }));
252
+ }
253
+ }
254
+ export const escapeHtml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Default look of <cube-player>. Everything is themable from outside:
3
+ *
4
+ * - CSS custom properties on the element (or any ancestor) — quick theming:
5
+ * --cc-accent play button, progress fill, focus ring
6
+ * --cc-control-bg button background --cc-control-bg-hover
7
+ * --cc-control-fg button icon colour --cc-control-radius
8
+ * --cc-button-size button height/width --cc-controls-gap
9
+ * --cc-progress-height bar thickness --cc-progress-track / --cc-progress-fill
10
+ * --cc-marker stage marker colour --cc-thumb-size
11
+ * --cc-font time / speed text
12
+ * - ::part() for anything else: stage, flat (the 2D picture), progress, progress-track,
13
+ * progress-fill, progress-thumb, progress-marker, segment (+ segment-<id>),
14
+ * segment-played, segment-recognition, segment-labels, segment-label
15
+ * (+ segment-label-<id>), tooltip, tooltip-text, controls, buttons, button,
16
+ * button-start, button-back, button-play, button-forward, button-end,
17
+ * button-speed, time.
18
+ * - Replace the controls entirely: put your own element in slot="controls"
19
+ * (or set controls="none") and drive the player's API.
20
+ *
21
+ * Colours derive from `currentColor`, so the defaults fit light and dark pages.
22
+ */
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: inverted page colours by default (text colour as background) */\n --cc-tooltip-bg: color-mix(in srgb, currentColor 92%, transparent);\n --cc-tooltip-fg: Canvas;\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 /* colour stays the page's text colour here, so the background can be derived from it\u2026 */\n background: var(--cc-tooltip-bg);\n font: 500 12px/1.35 system-ui, sans-serif;\n pointer-events: none;\n z-index: 2;\n box-shadow: 0 6px 18px -8px rgb(0 0 0 / 0.5);\n}\n/* \u2026and the text inverts it. */\n.tooltip .tip { color: var(--cc-tooltip-fg); }\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
+ /** Inline icons (no icon font, no network). */
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>";
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
+ 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>";
32
+ };
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Default look of <cube-player>. Everything is themable from outside:
3
+ *
4
+ * - CSS custom properties on the element (or any ancestor) — quick theming:
5
+ * --cc-accent play button, progress fill, focus ring
6
+ * --cc-control-bg button background --cc-control-bg-hover
7
+ * --cc-control-fg button icon colour --cc-control-radius
8
+ * --cc-button-size button height/width --cc-controls-gap
9
+ * --cc-progress-height bar thickness --cc-progress-track / --cc-progress-fill
10
+ * --cc-marker stage marker colour --cc-thumb-size
11
+ * --cc-font time / speed text
12
+ * - ::part() for anything else: stage, flat (the 2D picture), progress, progress-track,
13
+ * progress-fill, progress-thumb, progress-marker, segment (+ segment-<id>),
14
+ * segment-played, segment-recognition, segment-labels, segment-label
15
+ * (+ segment-label-<id>), tooltip, tooltip-text, controls, buttons, button,
16
+ * button-start, button-back, button-play, button-forward, button-end,
17
+ * button-speed, time.
18
+ * - Replace the controls entirely: put your own element in slot="controls"
19
+ * (or set controls="none") and drive the player's API.
20
+ *
21
+ * Colours derive from `currentColor`, so the defaults fit light and dark pages.
22
+ */
23
+ export const STYLES = /* css */ `
24
+ :host {
25
+ --cc-accent: #4f8cff;
26
+ --cc-control-bg: color-mix(in srgb, currentColor 8%, transparent);
27
+ --cc-control-bg-hover: color-mix(in srgb, currentColor 15%, transparent);
28
+ --cc-control-fg: currentColor;
29
+ --cc-control-radius: 10px;
30
+ --cc-button-size: 34px;
31
+ --cc-controls-gap: 5px;
32
+ --cc-progress-height: 6px;
33
+ --cc-progress-track: color-mix(in srgb, currentColor 14%, transparent);
34
+ --cc-progress-fill: var(--cc-accent);
35
+ --cc-marker: color-mix(in srgb, currentColor 55%, transparent);
36
+ --cc-thumb-size: 14px;
37
+ --cc-font: ui-monospace, "SF Mono", Menlo, monospace;
38
+ /* sections of the progress bar */
39
+ --cc-segment-1: #4f8cff;
40
+ --cc-segment-2: #22c55e;
41
+ --cc-segment-3: #f59e0b;
42
+ --cc-segment-4: #ef4444;
43
+ --cc-segment-5: #a855f7;
44
+ --cc-segment-6: #14b8a6;
45
+ --cc-segment-7: #ec4899;
46
+ --cc-segment-8: #eab308;
47
+ --cc-segment-height: calc(var(--cc-progress-height) + 2px);
48
+ --cc-segment-gap: 2px;
49
+ --cc-segment-unplayed: 30%;
50
+ /* popup: inverted page colours by default (text colour as background) */
51
+ --cc-tooltip-bg: color-mix(in srgb, currentColor 92%, transparent);
52
+ --cc-tooltip-fg: Canvas;
53
+ display: flex;
54
+ flex-direction: column;
55
+ gap: 10px;
56
+ min-width: 200px;
57
+ min-height: 200px;
58
+ outline: none;
59
+ container-type: inline-size;
60
+ }
61
+ :host([hidden]) { display: none; }
62
+ .stage { position: relative; flex: 1; min-height: 0; }
63
+ .flat { position: absolute; inset: 0; display: grid; place-items: center; }
64
+ .flat[hidden] { display: none; }
65
+ .flat svg { width: 100%; height: 100%; }
66
+
67
+ /* ─── progress bar ─── */
68
+ .progress { display: none; padding: 6px 2px; cursor: pointer; touch-action: none; }
69
+ :host([progress]) .progress { display: block; }
70
+ .track {
71
+ position: relative;
72
+ height: var(--cc-progress-height);
73
+ border-radius: 999px;
74
+ background: var(--cc-progress-track);
75
+ }
76
+ .fill {
77
+ position: absolute; inset: 0 auto 0 0;
78
+ width: 0;
79
+ border-radius: inherit;
80
+ background: var(--cc-progress-fill);
81
+ }
82
+ .thumb {
83
+ position: absolute; top: 50%; left: 0;
84
+ width: var(--cc-thumb-size); height: var(--cc-thumb-size);
85
+ border-radius: 50%;
86
+ background: var(--cc-progress-fill);
87
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--cc-progress-fill) 25%, transparent);
88
+ transform: translate(-50%, -50%) scale(0.85);
89
+ transition: transform 0.12s ease;
90
+ }
91
+ .progress:hover .thumb, .progress.dragging .thumb { transform: translate(-50%, -50%) scale(1); }
92
+ /* Sections: coloured parts of the bar, dim until played, recognition hatched. */
93
+ .progress { position: relative; }
94
+ .progress.has-segments .fill { display: none; }
95
+ .segments { position: absolute; inset: 0; }
96
+ .segment {
97
+ position: absolute; top: 50%;
98
+ height: var(--cc-segment-height);
99
+ transform: translateY(-50%);
100
+ overflow: hidden;
101
+ background: color-mix(in srgb, var(--seg) var(--cc-segment-unplayed), transparent);
102
+ clip-path: inset(0 calc(var(--cc-segment-gap) / 2) 0 calc(var(--cc-segment-gap) / 2) round 999px);
103
+ }
104
+ .segment .played { position: absolute; inset: 0 auto 0 0; width: 0; background: var(--seg); }
105
+ .segment .recognition {
106
+ position: absolute; inset: 0 auto 0 0;
107
+ background: repeating-linear-gradient(135deg, color-mix(in srgb, Canvas 50%, transparent) 0 2px, transparent 2px 5px);
108
+ }
109
+ .progress.has-segments .thumb { background: currentColor; box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 20%, transparent); }
110
+ .tooltip {
111
+ position: absolute; bottom: calc(100% + 4px); left: 0;
112
+ transform: translateX(-50%);
113
+ width: max-content; max-width: min(320px, 100%);
114
+ padding: 6px 9px;
115
+ border-radius: 8px;
116
+ /* colour stays the page's text colour here, so the background can be derived from it… */
117
+ background: var(--cc-tooltip-bg);
118
+ font: 500 12px/1.35 system-ui, sans-serif;
119
+ pointer-events: none;
120
+ z-index: 2;
121
+ box-shadow: 0 6px 18px -8px rgb(0 0 0 / 0.5);
122
+ }
123
+ /* …and the text inverts it. */
124
+ .tooltip .tip { color: var(--cc-tooltip-fg); }
125
+ .tooltip[hidden] { display: none; }
126
+ .labels { display: none; position: relative; height: 18px; margin-top: -4px; }
127
+ :host([segment-labels]) .labels { display: block; }
128
+ .label {
129
+ position: absolute; top: 0;
130
+ min-width: 0; height: 18px; padding: 0 2px;
131
+ background: none; border-radius: 4px;
132
+ color: inherit; opacity: 0.6;
133
+ font: 500 11px/18px system-ui, sans-serif;
134
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
135
+ }
136
+ .label:hover { background: var(--cc-control-bg); opacity: 1; transform: none; }
137
+ .label:active { transform: none; }
138
+ .label.current { opacity: 1; font-weight: 700; }
139
+ .marker {
140
+ position: absolute; top: 50%;
141
+ width: 2px; height: calc(var(--cc-progress-height) + 8px);
142
+ border-radius: 1px;
143
+ background: var(--cc-marker);
144
+ transform: translate(-50%, -50%);
145
+ pointer-events: none;
146
+ }
147
+ :host(:not([markers])) .marker { display: none; }
148
+ .progress:focus-visible .track { outline: 2px solid var(--cc-accent); outline-offset: 4px; }
149
+
150
+ /* ─── controls ─── */
151
+ /* time | buttons | speed — the buttons stay centred whatever the time text's width */
152
+ .controls { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: var(--cc-controls-gap); }
153
+ .buttons { display: flex; gap: var(--cc-controls-gap); }
154
+ .time { justify-self: start; }
155
+ .speed { justify-self: end; }
156
+ /* Narrow players: buttons on top, time and speed below. */
157
+ @container (max-width: 300px) {
158
+ .controls { grid-template-columns: 1fr 1fr; grid-template-areas: "buttons buttons" "time speed"; }
159
+ .buttons { grid-area: buttons; justify-content: center; }
160
+ .time { grid-area: time; }
161
+ .speed { grid-area: speed; }
162
+ }
163
+ :host([controls="none"]) .controls { display: none; }
164
+ /* Live (following a smart cube): nothing to replay. */
165
+ :host([live]) .controls, :host([live]) .progress { display: none; }
166
+ button {
167
+ display: inline-grid; place-items: center;
168
+ min-width: var(--cc-button-size); height: var(--cc-button-size);
169
+ padding: 0 8px;
170
+ border: 0;
171
+ border-radius: var(--cc-control-radius);
172
+ background: var(--cc-control-bg);
173
+ color: var(--cc-control-fg);
174
+ font: 600 12px/1 var(--cc-font);
175
+ cursor: pointer;
176
+ transition: background 0.12s ease, transform 0.08s ease;
177
+ }
178
+ button:hover { background: var(--cc-control-bg-hover); }
179
+ button:active { transform: scale(0.94); }
180
+ button:focus-visible { outline: 2px solid var(--cc-accent); outline-offset: 2px; }
181
+ button:disabled { opacity: 0.4; cursor: default; transform: none; }
182
+ /* Icons never take the click: it always lands on the button itself, even if the icon changes mid-click. */
183
+ button svg { width: 18px; height: 18px; display: block; fill: currentColor; pointer-events: none; }
184
+ .play { min-width: calc(var(--cc-button-size) * 1.25); background: var(--cc-accent); color: #fff; }
185
+ .play:hover { background: color-mix(in srgb, var(--cc-accent) 85%, #000); }
186
+ .time { font: 500 12px/1 var(--cc-font); opacity: 0.75; white-space: nowrap; font-variant-numeric: tabular-nums; }
187
+ `;
188
+ /** Inline icons (no icon font, no network). */
189
+ export const ICONS = {
190
+ 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>',
191
+ 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>',
192
+ 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>',
193
+ 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>',
194
+ 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>',
195
+ 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>',
196
+ };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Caching SVG output. `svgKey` is a stable string for (state, options) — use it
3
+ * as a key for an in-memory LRU, IndexedDB, localStorage, a CDN path or a
4
+ * server-side file name. Rendering is deterministic, so equal keys mean equal
5
+ * images.
6
+ */
7
+ import type { State } from "../core/index.js";
8
+ import { type SvgOptions } from "./svg.js";
9
+ export declare function svgKey(state: State, options?: SvgOptions): string;
10
+ /** Small LRU of rendered SVG strings. */
11
+ export declare class SvgCache {
12
+ private readonly max;
13
+ private map;
14
+ constructor(max?: number);
15
+ get(state: State, options?: SvgOptions): string;
16
+ get size(): number;
17
+ }
18
+ /** `data:` URL for an <img src>. */
19
+ export declare const svgDataUrl: (svg: string) => string;
20
+ /** Browser only: rasterise to a PNG Blob (uses an offscreen canvas). */
21
+ export declare function svgToPngBlob(svg: string, width: number): Promise<Blob>;
package/image/cache.js ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Caching SVG output. `svgKey` is a stable string for (state, options) — use it
3
+ * as a key for an in-memory LRU, IndexedDB, localStorage, a CDN path or a
4
+ * server-side file name. Rendering is deterministic, so equal keys mean equal
5
+ * images.
6
+ */
7
+ import { renderSvg } from "./svg.js";
8
+ const hex = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
9
+ /** Short FNV-1a hash — keys stay short enough for file names and URLs. */
10
+ function fnv(text) {
11
+ let h = 0x811c9dc5;
12
+ for (let i = 0; i < text.length; i++) {
13
+ h ^= text.charCodeAt(i);
14
+ h = Math.imul(h, 0x01000193) >>> 0;
15
+ }
16
+ return h.toString(36);
17
+ }
18
+ export function svgKey(state, options = {}) {
19
+ const { mask, frame, skin, spins, ...rest } = options;
20
+ const parts = [
21
+ hex(state),
22
+ mask ? hex(mask) : "",
23
+ frame ? String(frame.id) : "0",
24
+ skin ? JSON.stringify(skin) : "",
25
+ spins && skin?.decals?.length ? hex(spins) : "",
26
+ JSON.stringify(rest, Object.keys(rest).sort()),
27
+ ];
28
+ return `${options.view ?? "iso"}-${fnv(parts.join("|"))}-${fnv(parts.reverse().join("|"))}`;
29
+ }
30
+ /** Small LRU of rendered SVG strings. */
31
+ export class SvgCache {
32
+ max;
33
+ map = new Map();
34
+ constructor(max = 500) {
35
+ this.max = max;
36
+ }
37
+ get(state, options = {}) {
38
+ const key = svgKey(state, options);
39
+ const hit = this.map.get(key);
40
+ if (hit !== undefined) {
41
+ this.map.delete(key);
42
+ this.map.set(key, hit);
43
+ return hit;
44
+ }
45
+ const svg = renderSvg(state, options);
46
+ this.map.set(key, svg);
47
+ if (this.map.size > this.max)
48
+ this.map.delete(this.map.keys().next().value);
49
+ return svg;
50
+ }
51
+ get size() {
52
+ return this.map.size;
53
+ }
54
+ }
55
+ /** `data:` URL for an <img src>. */
56
+ export const svgDataUrl = (svg) => `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
57
+ /** Browser only: rasterise to a PNG Blob (uses an offscreen canvas). */
58
+ export async function svgToPngBlob(svg, width) {
59
+ const img = new Image();
60
+ img.src = svgDataUrl(svg);
61
+ await img.decode();
62
+ const height = Math.round((width * img.naturalHeight) / img.naturalWidth);
63
+ const canvas = new OffscreenCanvas(width, height);
64
+ canvas.getContext("2d").drawImage(img, 0, 0, width, height);
65
+ return canvas.convertToBlob({ type: "image/png" });
66
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./svg.js";
2
+ export * from "./cache.js";
package/image/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./svg.js";
2
+ export * from "./cache.js";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Server-side PNG (Node / Bun), via @resvg/resvg-js — an optional dependency:
3
+ * install it only where you render PNGs on a server. Browsers use
4
+ * `svgToPngBlob` instead.
5
+ */
6
+ export declare function svgToPng(svg: string, width?: number): Promise<Uint8Array>;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Server-side PNG (Node / Bun), via @resvg/resvg-js — an optional dependency:
3
+ * install it only where you render PNGs on a server. Browsers use
4
+ * `svgToPngBlob` instead.
5
+ */
6
+ export async function svgToPng(svg, width) {
7
+ const { Resvg } = await import("@resvg/resvg-js");
8
+ const resvg = new Resvg(svg, width ? { fitTo: { mode: "width", value: width } } : undefined);
9
+ return resvg.render().asPng();
10
+ }
package/image/svg.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Cube pictures as SVG strings — no DOM, no canvas: the same call works in a
3
+ * browser, a worker, or on a server (Node/Bun), and the output is a plain
4
+ * string you can cache, inline, serve or turn into PNG.
5
+ *
6
+ * Looks come from the same `Skin` as the 3D renderer (@cubecore/skin):
7
+ * colours, mask colours, tile shapes per piece kind, custom SVG tile outlines,
8
+ * stickerless tiles and the logo — one skin, one look in 3D and in pictures.
9
+ *
10
+ * Views:
11
+ * "iso" — 3D-looking cube showing U, F and R (like a case card)
12
+ * "top" — last-layer diagram: U face plus the side stickers of the top layer
13
+ * "net" — unfolded cube (U on top, L F R B across, D below)
14
+ *
15
+ * Every tile is drawn in its face's own (a, b) coordinates and then mapped to
16
+ * the picture by an affine projection per view, so shapes, paths and logos
17
+ * work the same way in all three views.
18
+ */
19
+ import { type CenterSpins, type Frame, type Mask, type State } from "../core/index.js";
20
+ import { type Skin, type Theme } from "../skin/index.js";
21
+ export type View = "iso" | "top" | "net";
22
+ export interface SvgOptions {
23
+ view?: View;
24
+ /** Output width in px (height follows the view's aspect ratio). */
25
+ size?: number;
26
+ /** Looks — the same skin the 3D renderer uses. Default `SKINS.default`. */
27
+ skin?: Skin;
28
+ mask?: Mask;
29
+ /** Draw the cube as held in this frame (e.g. to show a case with the cross on D whatever face it was solved on). */
30
+ frame?: Frame;
31
+ /** Centre spins (see `spinsAfter`) — only matter for decals on centres. Default: all upright. */
32
+ spins?: CenterSpins;
33
+ /** Page theme the skin is adjusted for (skin.themes). Default "dark". */
34
+ theme?: Theme;
35
+ }
36
+ export declare function renderSvg(state: State, options?: SvgOptions): string;