ice-web-components 1.1.0 → 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.
@@ -8,6 +8,14 @@ export declare class ICELabel extends ICEWidget {
8
8
  constructor(props?: any);
9
9
  setText(text: string): this;
10
10
  getText(): string;
11
+ /**
12
+ * 改文字颜色(动态强调 / 置灰用,比如 BIOS 自检行的灰→黄→绿、菜单选中态)。
13
+ *
14
+ * 必须走内层 `ICEText`:文字是它画的,构造期 `style` 已经下沉到那一层,
15
+ * 之后再 `setState({ style })` 只会改到外壳容器,屏幕上的字纹丝不动。
16
+ * 只覆盖 `fillStyle`,字号 / 字体 / 对齐这些原样保留。
17
+ */
18
+ setTextColor(color: string): this;
11
19
  /**
12
20
  * 让标签自身占据文字的实测尺寸。
13
21
  *
@@ -90,6 +90,9 @@ export declare class ICETileMap extends ICEWidget {
90
90
  private highlightColor;
91
91
  private labelFontSize;
92
92
  private labelColor;
93
+ /** 构造期是否显式给过 width/height(`props` 里已被默认值填过,只能在这里记一笔) */
94
+ private explicitWidth;
95
+ private explicitHeight;
93
96
  /** 自绘流程执行次数(浏览器里由 render() 驱动;单测可直接调 paintBoard()) */
94
97
  private paintCount;
95
98
  constructor(props: ICETileMapOptions);
@@ -97,6 +100,15 @@ export declare class ICETileMap extends ICEWidget {
97
100
  getCols(): number;
98
101
  getCellSize(): number;
99
102
  getGap(): number;
103
+ /**
104
+ * 换网格尺寸(行列 / 格子大小一起换),并清空格子数据与高亮。
105
+ *
106
+ * 为什么必须有这个方法:`rows/cols/cellSize` 在构造期就被缓存进实例字段,
107
+ * 只 `setState({ rows, cols })` 的话**内部字段还是旧的** —— 下一次 `setTiles` 会按旧尺寸
108
+ * 抛错(像素编辑器把 32×32 切成 16×16 时就是这么炸的)。
109
+ * 显式给过 `width/height` 的(想自己留白)保持不动,没给过的跟着格子尺寸重算。
110
+ */
111
+ setSize(rows: number, cols: number, cellSize?: number): this;
100
112
  /** 格子在组件内的矩形(gap 均分在两边)。 */
101
113
  getCellRect(row: number, col: number): {
102
114
  left: number;
@@ -110,3 +110,6 @@ export * from './model/ICESnakeModel';
110
110
  export * from './model/ICEHighScoreModel';
111
111
  export * from './model/ICEChip8Model';
112
112
  export * from './model/ICE2048Model';
113
+ export * from './model/ICEHistoryModel';
114
+ export * from './model/ICEPixelModel';
115
+ export * from './model/ICEBiosModel';
@@ -0,0 +1,124 @@
1
+ /**
2
+ * 掌机 BIOS 的纯逻辑(不碰 canvas、不碰 DOM)。
3
+ *
4
+ * 掌机开机先跑一段「自检 → 菜单」的引导,然后才把控制权交给卡带:
5
+ * 自检**按时序推进**(页面每帧调 `tick(dt)`),菜单是**光标 + 确认**的老式控制台交互。
6
+ * 页面只负责把 `getSteps()` / `getMenuEntries()` 画成文字,并执行 `confirm()` 返回的动作 ——
7
+ * 状态机与渲染分开,所以「自检要跑多久、菜单怎么绕、设置存到哪」都能在 node 里单测。
8
+ *
9
+ * 约定:
10
+ * - 自检项由 `steps` 注入(默认 5 项:CPU / RAM / VRAM / SOUND / CART),每项有自己的耗时;
11
+ * - 相位机:`post`(自检中)→ `menu`(启动菜单)或 `boot`(直接启动,快速启动开着时);
12
+ * `settings`(设置页)从菜单进、`back()` 回菜单;
13
+ * - 菜单光标上下**循环**(到头绕回去,老式 BIOS 都这样),进设置前记下位置、回来还停在那儿;
14
+ * - 设置(快速启动 / 默认卡带)落到注入的存储里;坏数据、写盘失败一律降级,绝不抛。
15
+ */
16
+ export declare type ICEBiosPhase = 'post' | 'menu' | 'settings' | 'boot';
17
+ export interface ICEBiosStep {
18
+ /** 稳定 key(页面拿它做节点 id / 维护增量更新) */
19
+ key: string;
20
+ label: string;
21
+ detail: string;
22
+ /** 这一项「检测」要花多久(毫秒) */
23
+ duration: number;
24
+ }
25
+ export interface ICEBiosStepState extends ICEBiosStep {
26
+ state: 'pending' | 'running' | 'ok';
27
+ /** 当前项的进度 0..1;非当前项是 0 或 1 */
28
+ progress: number;
29
+ }
30
+ export interface ICEBiosCartridge {
31
+ key: string;
32
+ label: string;
33
+ }
34
+ export interface ICEBiosMenuEntry {
35
+ key: string;
36
+ label: string;
37
+ type: 'cartridge' | 'settings' | 'boot';
38
+ }
39
+ export interface ICEBiosSettings {
40
+ /** 开着就跳过菜单直接启动默认卡带(开机自检还是要跑) */
41
+ quickBoot: boolean;
42
+ defaultCartridge: string;
43
+ }
44
+ export declare type ICEBiosAction = {
45
+ type: 'boot';
46
+ cartridge: string;
47
+ } | {
48
+ type: 'settings';
49
+ } | {
50
+ type: 'menu';
51
+ } | {
52
+ type: 'none';
53
+ };
54
+ export interface ICEBiosStorage {
55
+ getItem(key: string): string | null;
56
+ setItem(key: string, value: string): void;
57
+ }
58
+ export interface ICEBiosOptions {
59
+ cartridges: ICEBiosCartridge[];
60
+ /** 自检项,默认 `ICE_BIOS_DEFAULT_STEPS` */
61
+ steps?: ICEBiosStep[];
62
+ /** 初始设置(优先级低于存储里的值) */
63
+ settings?: Partial<ICEBiosSettings>;
64
+ /** 存储实现;传 null 表示只放内存;不传则用 globalThis.localStorage(没有就退化成内存) */
65
+ storage?: ICEBiosStorage | null;
66
+ /** 存储键前缀,默认 `ice-arcade-bios-` */
67
+ prefix?: string;
68
+ }
69
+ export declare type ICEBiosListener = (model: ICEBiosModel) => void;
70
+ /** 默认自检项:每一项都是一句「老式 BIOS 会印在屏幕上的话」。 */
71
+ export declare const ICE_BIOS_DEFAULT_STEPS: ICEBiosStep[];
72
+ export declare class ICEBiosModel {
73
+ private steps;
74
+ private cartridges;
75
+ private storage;
76
+ private storageKey;
77
+ private listeners;
78
+ private phase;
79
+ private elapsed;
80
+ private cursor;
81
+ private cursorBeforeSettings;
82
+ private settings;
83
+ constructor(options: ICEBiosOptions);
84
+ getPhase(): ICEBiosPhase;
85
+ getCartridges(): ICEBiosCartridge[];
86
+ getPostElapsed(): number;
87
+ /** 自检总时长(毫秒)。 */
88
+ getPostDuration(): number;
89
+ /** 自检进度 0..1(按时间算,页面拿它画进度条)。 */
90
+ getPostProgress(): number;
91
+ /** 自检项与它们此刻的状态(当前项 running、前面的 ok)。 */
92
+ getSteps(): ICEBiosStepState[];
93
+ /** 菜单项:卡带列表 + 设置 + 退出并启动。 */
94
+ getMenuEntries(): ICEBiosMenuEntry[];
95
+ getCursor(): number;
96
+ getSelectedEntry(): ICEBiosMenuEntry | null;
97
+ getSettings(): ICEBiosSettings;
98
+ isQuickBoot(): boolean;
99
+ getDefaultCartridge(): string;
100
+ /** 推进自检(页面每帧调用),返回是否发生了变化。 */
101
+ tick(delta: number): boolean;
102
+ moveCursor(delta: number): void;
103
+ setCursor(index: number): void;
104
+ /** 确认当前项:只返回动作,执行动作是页面的事(好测)。 */
105
+ confirm(): ICEBiosAction;
106
+ /** 返回上一层(设置 → 菜单)。 */
107
+ back(): void;
108
+ /** 从游戏里回到 BIOS 菜单(掌机的「复位」)。 */
109
+ openMenu(): void;
110
+ /** 重新跑一遍自检(再开一次机)。 */
111
+ restart(): void;
112
+ toggleQuickBoot(): boolean;
113
+ setDefaultCartridge(key: string): void;
114
+ addChangeListener(listener: ICEBiosListener): () => void;
115
+ private __alignCursorToDefault;
116
+ /** 卡带 key 必须真的存在,否则回退到第一个卡带。 */
117
+ private __normalizeCartridge;
118
+ private __load;
119
+ private __readStorage;
120
+ private __save;
121
+ private __defaultStorage;
122
+ private notify;
123
+ }
124
+ export default ICEBiosModel;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * 撤销 / 重做栈(纯逻辑,泛型)。
3
+ *
4
+ * 不是给某个画板专用的:任何「编辑 → 提交 → 后悔」的界面都能用(像素画板、看板、
5
+ * 表格编辑、表单草稿…),所以它只认泛型快照,不认识 canvas、颜色和数据结构。
6
+ *
7
+ * 约定:
8
+ * - `push(state)` 入栈并**清空 redo 栈** —— 从历史中间改一下,原来那条「未来」就作废了;
9
+ * - `undo()` / `redo()` 返回目标快照,到边界返回 `null`(调用方据此把按钮置灰);
10
+ * - 超过 `limit` 丢**最旧**的一条(默认 50:够用,且内存不会无限涨);
11
+ * - 变更通知带 `reason`(push / undo / redo / clear),页面按需刷新按钮态;
12
+ * - 快照的存取由调用方负责 `slice()` / 深拷贝 —— 栈只存引用,不做深拷贝(性能)。
13
+ */
14
+ export declare type ICEHistoryReason = 'push' | 'undo' | 'redo' | 'clear';
15
+ export declare type ICEHistoryListener<T> = (model: ICEHistoryModel<T>, reason: ICEHistoryReason) => void;
16
+ export interface ICEHistoryOptions<T> {
17
+ /** 最多保留多少个**可撤销**的快照,默认 50 */
18
+ limit?: number;
19
+ /** 初始当前值(不占历史深度) */
20
+ initial?: T;
21
+ }
22
+ export declare class ICEHistoryModel<T> {
23
+ private limit;
24
+ /** 已提交的快照,最后一个就是当前值 */
25
+ private past;
26
+ private future;
27
+ private listeners;
28
+ constructor(options?: ICEHistoryOptions<T>);
29
+ getLimit(): number;
30
+ getCurrent(): T | null;
31
+ canUndo(): boolean;
32
+ canRedo(): boolean;
33
+ /** 栈深:undo = 还能撤销几步,redo = 还能重做几步。 */
34
+ getDepth(): {
35
+ undo: number;
36
+ redo: number;
37
+ };
38
+ push(state: T): void;
39
+ undo(): T | null;
40
+ redo(): T | null;
41
+ clear(initial?: T): void;
42
+ addChangeListener(listener: ICEHistoryListener<T>): () => void;
43
+ private notify;
44
+ }
45
+ export default ICEHistoryModel;
@@ -0,0 +1,89 @@
1
+ export interface ICEPixelModelOptions {
2
+ rows?: number;
3
+ cols?: number;
4
+ /** 调色板(`#rgb` / `#rrggbb`),下标就是画布上存的值;不传给一组默认色 */
5
+ palette?: string[];
6
+ /** 背景色下标,默认 0 */
7
+ background?: number;
8
+ /** 最多能撤销几步,默认 50 */
9
+ limit?: number;
10
+ }
11
+ export interface ICEPixelSVGOptions {
12
+ /** 每个像素在 SVG 里占多少单位,默认 16 */
13
+ cellSize?: number;
14
+ /** 是否画一层背景矩形,默认 true */
15
+ background?: boolean;
16
+ }
17
+ export declare type ICEPixelListener = (model: ICEPixelModel) => void;
18
+ /** 默认调色板:Bootstrap 语义色 + 黑白灰(够画像素画,也不刺眼)。 */
19
+ export declare const ICE_PIXEL_DEFAULT_PALETTE: string[];
20
+ /** `#rgb` / `#rrggbb` → [r, g, b];认不出来当黑色(画布上永远有个确定的结果)。 */
21
+ export declare function icePixelParseColor(color: string): [number, number, number];
22
+ export declare class ICEPixelModel {
23
+ private rows;
24
+ private cols;
25
+ private palette;
26
+ private background;
27
+ private cells;
28
+ private history;
29
+ private listeners;
30
+ private dirty;
31
+ constructor(options?: ICEPixelModelOptions);
32
+ getRows(): number;
33
+ getCols(): number;
34
+ getPalette(): string[];
35
+ getBackground(): number;
36
+ /** 画布副本(行优先)。 */
37
+ getCells(): number[];
38
+ getPixel(row: number, col: number): number;
39
+ /** 有没提交的改动(页面据此把「保存 / 导出」按钮点亮)。 */
40
+ isDirty(): boolean;
41
+ canUndo(): boolean;
42
+ canRedo(): boolean;
43
+ getHistoryDepth(): {
44
+ undo: number;
45
+ redo: number;
46
+ };
47
+ /**
48
+ * 画一个像素。返回「真的改了」—— 越界、颜色非法、值没变都返回 false,
49
+ * 这样页面可以放心地在拖动回调里调用它(不会刷出一堆无意义的重绘)。
50
+ */
51
+ setPixel(row: number, col: number, color: number): boolean;
52
+ /** Bresenham 直线(铅笔拖动、直线工具都走它)。 */
53
+ drawLine(row0: number, col0: number, row1: number, col1: number, color: number): boolean;
54
+ /**
55
+ * 直线经过的格子坐标(Bresenham)。
56
+ * 单独暴露出来是给**预览**用的:拖直线 / 拖矩形时先拿坐标点亮高亮层,松手才真正落笔。
57
+ */
58
+ getLineCells(row0: number, col0: number, row1: number, col1: number): Array<[number, number]>;
59
+ /** 矩形**描边**经过的格子坐标(和 drawRect 同一套口径,供预览用)。 */
60
+ getRectCells(row0: number, col0: number, row1: number, col1: number): Array<[number, number]>;
61
+ /** 矩形描边(只画框,不填内部)。 */
62
+ drawRect(row0: number, col0: number, row1: number, col1: number, color: number): boolean;
63
+ /**
64
+ * 四邻域油漆桶(迭代版,不用递归 —— 大画布上递归会爆栈)。
65
+ * 目标和填充色相同、或起点越界时返回 false。
66
+ */
67
+ fill(row: number, col: number, color: number): boolean;
68
+ /** 整块刷成某个颜色(清空 / 填充背景)。 */
69
+ clear(color?: number): boolean;
70
+ /** 换尺寸:重新铺一块空画布(旧内容不留,历史也重开)。 */
71
+ resize(rows: number, cols: number): void;
72
+ /** 把当前画布落一次历史(一次笔画 / 一次图形操作调用一次)。返回是否真的入了栈。 */
73
+ commit(): boolean;
74
+ undo(): boolean;
75
+ redo(): boolean;
76
+ toSVG(options?: ICEPixelSVGOptions): string;
77
+ /** 给 `new ImageData(rgba, cols * scale, rows * scale)` 用的 RGBA 数组。 */
78
+ toRGBA(scale?: number): Uint8ClampedArray;
79
+ toJSON(): string;
80
+ /** 反序列化;坏数据抛错而不是静默给一块白板(导入失败得让人看见)。 */
81
+ static fromJSON(json: string): ICEPixelModel;
82
+ addChangeListener(listener: ICEPixelListener): () => void;
83
+ private __inside;
84
+ /** 非法下标一律当背景:画布上永远只有调色板里真实存在的颜色。 */
85
+ private __normalizeColor;
86
+ private __restore;
87
+ private notify;
88
+ }
89
+ export default ICEPixelModel;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ice-web-components",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Swing-style Canvas UI components built on ice-render.",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.mjs",
@@ -22,6 +22,7 @@
22
22
  "qa:workbench": "node scripts/qa-workbench.mjs",
23
23
  "qa:xp": "node scripts/qa-xp.mjs",
24
24
  "qa:arcade": "node scripts/qa-arcade.mjs",
25
+ "qa:pixel": "node scripts/qa-pixel.mjs",
25
26
  "docs:api": "node scripts/gen-docs.mjs",
26
27
  "docs:check": "node scripts/check-docs.mjs",
27
28
  "docs": "npm run docs:api && npm run docs:check",