PageSourceSearch

https://retroapplejs.github.io/res/EMU_CARD_dithertizer2.js

js retroapplejs.github.io collected 2026-10-03 09:50:57 UTC 48,971 bytes, 963 lines download raw bytes

1//
2// EMU_CARD_dithertizer2.js
3//
4// Color HGR camera card. ConvertHGR supplies complete Apple II HGR bytes;
5// the capture copies them to the selected page, including color phase bit 7.
6//
7// The original DSCAN 4.2 driver uses only two slot-I/O addresses:
8//   $C0n0 write : retain the threshold latch and disable capture
9//   $C0n0 read  : read sync in D7 and disable capture
10//   $C0n8 read  : enable capture
11//
12// DSCAN's $1D03 entry selects PAGE2 and performs the video-sync capture.
13// Its $1C00 four-pass merge clears bit 7 and cannot preserve color HGR.
14//
15
16function DithertizerII_2()
17{
18    var card=this;
19    var cameraSource=null;
20    var hostCameraEpoch=0;
21    var cameraFrameBusy=false;
22    var cameraFrameToken=0;
23    var wasmEnabled=true;
24    var hostCameraPage=null;
25    var hostCameraPhaseReference=null;
26    var defaultHGRPage=null;
27    var hostCameraAdapter=null;
28    var hostCameraControlID=null;
29    var convertHGRAdapterLoadPromise=null;
30    var dscanPatchedHardware=null;
31    var syncTraceEnabled=false;
32    var syncTraceEntries=[];
33    var syncTraceLastD7=null;
34    var syncTraceLastTick=null;
35
36    const HGR_WIDTH=280;
37    const HGR_HEIGHT=192;
38    const HGR_BYTES_PER_LINE=40;
39    const HGR_PAGE_BYTES=8192;
40    const PHASE_CHROMA_TOLERANCE=12;
41    const DSCAN_COLOR_ENTRY=0x1C00;
42    const DSCAN_COLOR_JUMP=[0x4C,0x03,0x1D]; // JMP $1D03
43    const DSCAN_ORIGINAL_ENTRY=[0x48,0x8A,0x48,0x98,0x48,0x20,0x3A,0x1C];
44    const DSCAN_MERGE_SIGNATURE=[0x29,0x7F,0x91,0x26]; // AND #$7F; STA ($26),Y
45    const DSCAN_SYNC_SIGNATURE=[0x38,0x08,0x48,0x8A,0x48,0x98,0x48,0x78];
46    // User-supplied Apple II logo HGR page, including color phase and page holes.
47    const DEFAULT_HGR_BASE64=[
48        "gICAgACAgACAgICAAACAgICAgAAAgAAAAIAAAIAAAIAAgACAAACAgAAAgACAUCoBgAAAAAAAgACAAICAgICAAIAAAICAAICAAICA",
49        "gIAAAIAAgNWq1arVqoWAAIAAAAAAAACAAIAAAIAAAIAAgACAgICAAICAgICAAAAAAAAAAACAAACAAICAgIAAAACAAIAAAAAAAICA",
50        "gICAgICAAACAgIAAAICAAACAgAAAAIBVAgCAgACAgAAAAICAgICAgIAAAACAgAAAgAAAgICAgAAAAAAAgAAAgIAAgAAAgACAAIAA",
51        "gICAAIAAAIAAAACAAIAAgAAAgACAgAAAAAAAAAAAAAAAgAAAgICAAIAAAICAAAAAAACAgIAAgACAAAAAgAAAgICAAACAgIAAVCpV",
52        "KlUqVSoBAIAAgAAAAIAAgACAgICAALCWAIAAgACAAHPz4PaegACAAICAgAAAgICAgAAAAIAAgIAAgICAAACAgAAAAAAAAICAAACA",
53        "AAAAAAAAAAAAgIAAgAAAAACAgIAAgIAAgACAAIAAgIAAgICAgACAAAAAgIAAAACAgChVKlUqVSpVKsD///t+gfbu7fyGQP3u+eaA",
54        "cI0AfO/+7ocAwH/hn4CAgICAgAAAgICAgACAAAAAgAAAgACAgACAAACAAICAgACAgIAAAIAAAAAAAAAAAAAAAACAgACAgICAAAAA",
55        "AICAgACAAAAAAAAAAAAAAICAgACAAICAgAAAqtWq1arVqtWCnJMAgO+A9oOA8P/g/4CAfJ/wn/idAOD7gIAAGeCxAICAAACAAICA",
56        "gICAAAAAgAAAAACAgAAAgICAAACAAAAAgICAgIAAgAAAAAAAAAAAAAAAAAAAAICAAIAAAICAAAAAAAAAAAAAgACAAACAgACAAAAA",
57        "AAAAgICq1arVqtWq1YpwvA5AmQHmgICOhsCZgGCkg3CZgAuMAAAAgAD74J4AgAAAgICAAAAAAIAAgACAgICAgACAAIAAAICAgAAA",
58        "gAAAgICAAAAAgAAAAAAAAAAAgIAAgIAAAAAAgACAgICAgACAgICAgICAAIAAgICAAIAAAIAAAACAgKjVqtWq1arVqpUAAAAAAH+D",
59        "AAAAYP8AgICAAAAAgIAAgIAAAACAgAAAgIAAAIAAgIAAgAAAgACAAICAAIAAgICAgACAgAAAgIAAAIAAAIAAAAAAAAAAAAAAgAAA",
60        "gICAAICAAAAAgICAgIAAgICAgACAgIAAgICAAIAAgACAAAAAACpVKlUqVSpVAoCAgICAgICAgIAAgIAAgICAAAAAgAAAgAAAAIAA",
61        "gAAAAACAAICAgACAgAAAAAAAAACAgACAAAAAgACAAAAAgIAAgACAAAAAAAAAAAAAAICAgAAAAAAAgIAAAICAAICAgAAAAACAAAAA",
62        "AAAAgACAgIAAAACAAACAgAAAAFAqAQCAgACAAACAgICAAIAAAAAAgACAAAAAgAAAAIAAAAAAAIDVqtWq1aqFgICAgICAAACAAIAA",
63        "AICAgICAgICAAICAAAAAAIAAgAAAAAAAAAAAgACAAIAAAIAAAIAAgIAAAACAgICAgIAAAAAAAICAgAAAgAAAgIAAgIAAgACAVYAA",
64        "gACAAICAAICAAAAAgIAAAACAgIAAAACAAIAAAIAAAAAAgIAAAACAgIAAgIAAgAAAgAAAAIAAgAAAAAAAgAAAAIAAgICAgACAAAAA",
65        "AAAAAAAAgIAAAIAAAIAAAIAAAAAAgICAgACAAAAAAAAAgIAAgACAgIAAgICAAFQqVSpVKlUqBYCAAACAgAAAgICAAACAAADIiQAA",
66        "gICAAIDE/WC7gICAAICAgIAAgACAAICAgAAAAIAAgICAgACAgIAAAACAAICAgIAAAAAAAAAAAAAAAAAAAACAAIAAgIAAgICAAAAA",
67        "AAAAgAAAAACAgIAAAAAAgICAAACAgICq1arVqtWq1argf///9wHu+/bvjeDzu2+7A7iegPb+/X+NAAB/8L+AgIAAgAAAAIAAgICA",
68        "gACAAACAAAAAAIAAAICAAICAAACAgAAAAIAAgAAAAAAAAAAAgACAgACAAAAAgIAAAICAAACAgAAAAACAgIAAgICAAAAAAIAAgICA",
69        "gKrVqtWq1arVgmKMgIBzge6DAGD94P0AgHydeJ3wtoBwjACAAGMAjIAAgAAAAIAAgACAAACAAIAAgAAAgACAAICAgAAAAACAAIAA",
70        "AICAAACAAAAAAAAAAACAAAAAAACAgACAAAAAAIAAgACAAAAAgACAgICAAACAAAAAAICAAICAAAAAAAAAAAAA4O/3g+eBmYPmcZlA",
71        "TMCdzoGIhkD58/fuh4CA/2C/AAAAAAAAAICAgICAAICAgAAAAIAAAAAAAIAAgACAgIAAgAAAAIAAAIAAAAAAAAAAAICAgICAgAAA",
72        "AACAAIAAAAAAgIAAAAAAAACAAICAAACAAAAAAAAAgAAAAAAAAAAAAAAAAACAAIB/h4CAAGB/gACAAICAAIAAgACAAIAAgIAAAICA",
73        "gAAAAACAAICAgICAAACAAACAgACAgAAAgAAAgICAAIAAAACAgAAAAAAAAAAAAACAAAAAgACAgAAAgAAAgICAgIAAAAAAAIAAAACA",
74        "AIAAgACAgICAAAAqVSpVKlUqVQKAgACAAACAgIAAgACAgIAAgACAAAAAAAAAAICAgIAAgICAAAAAgIAAAACAgACAAACAgACAAIAA",
75        "AAAAAACAgIAAAIAAAICAAAAAAAAAAACAAICAAAAAAICAgACAgAAAAIAAAICAAAAAAACAAICAgACAgACAgICAAIAAgABUKgGAAICA",
76        "gAAAgACAgAAAgIAAgAAAgICAgICAAICAAAAAgICA1KqFANWqgYAAAACAgIAAAACAgAAAAAAAAAAAAAAAAACAAIAAgIAAAAAAAAAA",
77        "AIAAgICAgICAgACAAIAAgICAgICAAIAAAACAgIAAgIAAAIAAgACAAAAAAICAgFUAAIAAAACAAACAAICAAIAAAIAAgICAAAAAAAAA",
78        "gACAgAAAAAAAAACAAIAAgIAAAIAAgICAgACAgIAAAICAAACAAAAAgAAAAAAAAAAAAAAAAAAAAACAgAAAAACAAAAAgIAAgIAAgICA",
79        "AIAAAIAAgIAAgACAgAAAAACAAIBVKlUqVSpVKgWAgIAAAICAAICAAIAAAIAAsJYAgICAgAAAAPtgngCAAAAAgICAAICAgACAgAAA",
80        "AACAAAAAgAAAAAAAgACAgACAAACAAAAAAAAAAAAAAAAAAICAAACAAAAAAACAAIAAgAAAAIAAAACAgACAgACAgIAAAIAAgACAKlUq",
81        "VSpVKlUKcH9/7H8B9rfttptAf+b87oPwD4B/7+b+h4DAf+GbgAAAgIAAgIAAAIAAgICAAICAgICAgIAAgICAgIAAgACAgIAAgIAA",
82        "AIAAAAAAAAAAAICAAACAgAAAAACAAIAAgAAAAACAgAAAgAAAAACAAAAAAIAAgACAgICq1arVqtWq1YIMgwCAnQHmgYDg90D3gIDs",
83        "t/ibuJsAmJsAAACM4LOAgICAgICAgAAAgAAAgICAAIAAgIAAAIAAgIAAAIAAAACAgIAAAICAgAAAAAAAAAAAgIAAgAAAAIAAgAAA",
84        "AIAAAAAAgIAAAACAAAAAgICAAAAAgIAAAACAgKrVqtWq1arViuD//UH7AeYD9swGoPOg47MDcJmA5rzu+wYA5v9g/wwAgIAAgICA",
85        "gACAAACAgICAAIAAgIAAgAAAgIAAgAAAAAAAAICAAAAAAAAAAAAAAAAAgACAgICAAAAAgICAAICAgACAgACAgACAAAAAgACAAICA",
86        "AIAAAAAAUCpVKlUqVSpVKgCAAAAAfoMAAADg/4AAAACAgAAAAIAAAAAAAIAAAAAAgACAgIAAAAAAgAAAAICAgAAAAAAAgAAAgIAA",
87        "gACAAICAAIAAgAAAAAAAAAAAAAAAgACAAIABgICAAIAAAACAgIAAAICAAIAAAIAAAICAgAAAgACAgACAKlUqVSpVKlUCAAAAAIAA",
88        "AAAAgAAAgAAAgICAgACAAAAAgICAgIAAgAAAAIAAgACAgICAAACAgACAAIAAAICAAACAgAAAAICAAACAAICAAAAAAAAAAAAAAIAA",
89        "AACAAIAAgAAAgIAAAACAgACAgIAAAICAgACAgACAgACAAAAAgIAAAACAVCoAgIAAAAAAgAAAAIAAAACAAACAgACAAAAAAICAgIAA",
90        "gIAAgNCqgYDUqoAAgIAAAIAAAAAAAAAAgICAgICAAAAAgACAgIAAAICAAAAAAAAAAAAAgAAAgIAAAICAAAAAAICAAIAAAAAAgIAA",
91        "AIAAAACAAIAAAIAAAIAAAIAAgIAVAAAAAICAAICAgICAAACAAAAAgIAAAIAAAACAgICAgIAAAICAAACAgIAAAAAAgICAgAAAAAAA",
92        "AICAAACAAICAgIAAAAAAAACAAAAAAAAAAAAAAACAgACAgAAAAIAAgICAgAAAgACAAIAAgAAAgAAAgAAAgACAAICAgIAgVSpVKlUq",
93        "VSoVgACAgIAAAICAAACAAICAgPCZgAAAAICAAMDv4B8AAICAgIAAgACAAACAAICAAACAgAAAAAAAgIAAgICAAIAAAIAAgIAAgAAA",
94        "AAAAAAAAgACAgIAAgACAgACAgIAAgAAAAAAAAACAgACAAIAAAACAgICAgIAAgKrVqtWq1arVivB/AwB/gf6DwO2d4P0A8P2HcBtg",
95        "94YA/g+AgO/gvAAAgICAAIAAgIAAAAAAgACAAAAAAAAAgAAAAAAAAACAAAAAAICAAAAAAAAAAAAAAAAAAACAgACAgACAgAAAgIAA",
96        "gACAAIAAgICAAAAAgACAgIAAAIAAgIAAqtWq1arVqtWC5BiAAOMAnoMA4Dxg7YCAuJuwDrDmgOSEgAAA42CMgIAAAIAAgIAAgACA",
97        "gICAgACAAAAAAIAAgIAAgICAAAAAgIAAgAAAgIAAAAAAAAAAAAAAgAAAAICAAAAAgACAgAAAAAAAAIAAAAAAgAAAAACAgIAAAAAA",
98        "gIAAAAAAAAAAAAAAf3/D94H7AWZ2A8DNwJnOALAOADzv9+6GgP//4H+fgIAAAICAgACAAAAAAACAAAAAgAAAAAAAgACAgAAAAIAA",
99        "gICAAAAAgAAAAAAAAAAAgACAAIAAgACAAICAAICAgAAAAIAAgIAAAICAAICAAICAgICAgIAAAFAqVSpVKlUqVSoAgICAAPeGgACA",
100        "oP+AgAAAgIAAgIAAAAAAgACAAIAAAICAgIAAgICAAICAgACAAAAAAICAgAAAAAAAgIAAAACAAIAAAIAAAAAAAAAAAACAgACAAIAg",
101        "AYAAAIAAgICAgACAAICAgIAAgACAAIAAgIAAgAAAgICAACpVKlUqVSpVAoCAgICAAACAAAAAgAAAAAAAgIAAAICAAICAgAAAAACA",
102        "gAAAAIAAAICAgIAAgACAgAAAAACAgICAAIAAAIAAAAAAAIAAAAAAAAAAAAAAAAAAgICAgACAAIAAAAAAAACAgAAAAAAAAACAAIAA",
103        "AICAgICAgACAgICAgACAAFQqAICAAAAAAICAAACAgACAgICAgAAAAICAgICAgAAAgACAgIDAqoCAwIqAgACAgICAAICAAIAAAAAA",
104        "gAAAgIAAAAAAgAAAgICAgAAAAAAAAAAAAACAgICAAICAAACAAIAAgAAAgACAAAAAgIAAgIAAAICAgACAgIAAgAAAKAWAASBVgACA",
105        "AICAgAAAgIAAgAAAgICAsJaAAAAAgIAAxskAswyAgICAAAAAAIAAAICAAACAAIAAgAAAgICAgIAAAIAAAIAAgIAAAAAAAAAAAAAA",
106        "AACAgIAAAIAAgACAAIAAAIAAgACAAACAAICAgACAgICAAIAAgIAAAACAIFUqVSpVKlUqBQAAgICAAICAAAAAgAAAgACwjoAAgJGm",
107        "hgDA/2CfgIAAgICAAIAAgICAgAAAgAAAAICAAIAAgICAAAAAAIAAgACAgACAgAAAAAAAAAAAAACAgIAAAAAAgICAAACAAACAgAAA",
108        "AAAAAAAAAACAAACAAACAAAAAAIAqVSpVKlUqVQr47gCA/wF+g4B89+D3gMD7jXif4P+DgP6HgMC74bMAgACAgAAAgICAAICAAAAA",
109        "AICAgAAAAACAAACAAIAAAAAAAAAAAIAAAAAAAAAAAAAAgACAgIAAgAAAAACAgIAAgAAAgICAgIAAAACAAACAAACAgIAAgAAAAKrV",
110        "qtWq1arVgsyMAICZAWKAALDmQJOAgOyZeJnImYAAgoAAALzAswAAAACAgIAAAACAgACAgICAgIAAAICAAIAAAICAAACAgIAAgIAA",
111        "gAAAAAAAAAAAAACAAICAgAAAgIAAgICAAAAAAICAgICAgACAgIAAAICAAACAgIAAAIAAqtWq1arVqtWqAP5/Qf8B9oM+u4Gg90Bv",
112        "uwDwGwDw+/1/jQB//2D7n4AAAIAAgAAAgICAgACAAACAgAAAAIAAAIAAgIAAgACAgACAgACAgAAAAAAAAAAAAIAAgACAgICAAICA",
113        "AAAAgICAgIAAAACAgAAAgICAgAAAgACAAICAAIBAKlUqVSpVKlUKgACAAAB+gwAAAGD3AICAgIAAAACAAIAAAAAAgACAgIAAgICA",
114        "AICAAIAAAIAAgAAAgICAgIAAAAAAgAAAgACAAIAAAACAAAAAAAAAAAAAAICAgICAKAEAAAAAAICAgAAAgAAAgAAAAAAAAACAgICA",
115        "gAAAAIAAgICo1arVqtWq1YAAgAAAgACAAACAAAAAgACAgACAgACAAICAgAAAgACAgACAgIAAAACAgIAAgAAAAACAAACAgAAAAAAA",
116        "gAAAAAAAAICAgACAAAAAAAAAAACAgACAAIAAgIAAgACAgACAgACAgIAAAICAgAAAAIAAAICAAIAAgICAgICAgIBVKgCAAAAAgACA",
117        "AICAAIAAgAAAAAAAAICAgACAgAAAAICAgACAAICAgICAAIAAAICAgICAAAAAAACAAACAAAAAgICAgACAgAAAgIAAAAAAAAAAAICA",
118        "AIAAgAAAgAAAgACAAAAAAAAAgACAgACAgICAgIAAgIAAgICAAAAAgCpVgIAoVQqAAIAAgICAAICAgAAAgICAAMiJgACAgAAAAPn8",
119        "YH2fAACAAICAgICAAAAAgAAAAACAAACAgAAAgIAAAICAAIAAAICAgAAAAAAAAAAAAAAAAAAAgACAAAAAgACAAIAAAIAAgICAgICA",
120        "gACAgIAAAIAAAAAAgICAAKDVqtWq1arVqoHg/3//AX5//4+AYP//94MAcJkAgH5/+w+AwP/gvwCAgAAAgACAAACAgICAAIAAgAAA",
121        "gICAgACAgAAAAACAAIAAAIAAAICAAAAAAAAAAAAAAACAgACAgAAAgICAAICAgICAAICAAICAAAAAAICAgIAAAAAAAIAAqtWq1arV",
122        "qtWKbLmAQP+BfgMA+H+gf4AAfw/wn/D9gID/AwAA5+DMAICAAIAAAIAAgIAAgACAAACAgIAAgIAAgIAAgAAAAAAAAACAAACAAIAA",
123        "AAAAAAAAAIAAAACAgACAAIAAAIAAAIAAAACAAACAgIAAAAAAgICAAICAAACAAICq1arVqtWq1Ypss4CAYwCeg4DgmWD0gABMjIiG",
124        "sOaAAACAAADj4BwAAACAAAAAgICAgIAAgIAAAAAAgACAgAAAgICAAACAgICAgIAAAICAAAAAAAAAAAAAAACAAAAAgICAAAAAAAAA",
125        "AICAgAAAAIAAAIAAgIAAAIAAgACAgACAgAAAAAAAAAAAAAB4/0P/g/+D9vaAwP3g+Y6A+J4AYH9/f4cA/v9g7o2AAICAgACAAACA",
126        "gIAAgACAgACAAIAAAACAAICAgACAgIAAAAAAgIAAAAAAAAAAAACAAAAAAAAAgAAAAAAAAAAAAAAAAAAAgACAgACAAAAAgACAAIAA",
127        "gIAAQCpVKlUqVSpVCoAAAICA9oYAAIBg/YAAAACAAIAAAAAAAAAAAAAAgICAgICAAAAAgICAAICAgIAAgIAAAICAAICAgIAAAACA",
128        "gAAAAICAAAAAAAAAAAAAAIAAAAAAgCoBgAAAAIAAAAAAAICAgACAgACAAICAAAAAgAAAgAAAgACAqNWq1arVqtWAgIAAgACAAICA",
129        "gIAAAAAAAAAAgACAgAAAgICAgACAgAAAAACAAAAAgAAAAACAgIAAAACAgICAgAAAAICAAIAAgAAAgAAAgAAAAAAAAAAAAACAgICA",
130        "AAAAgACAgIAAgIAAAICAgIAAAAAAAACAgIAAAIAAgAAAAICAgICAVQqAAACAgAAAgIAAAICAAICAAICAgICAAICAgACAgAAAgICA",
131        "AAAAAICAgIAAgACAgAAAAAAAAAAAgAAAAICAAICAgAAAAIAAgACAAAAAAAAAAAAAAACAAACAAACAAAAAgACAAIAAgIAAAIAAgICA",
132        "AACAAAAAAICAAIAAAFAqVQpAKlUqgIAAgACAAAAAgACAAAAAAICwlgCAgICAAIDnZ2DujwAAgACAAICAgIAAAAAAAACAgACAAICA",
133        "gAAAgAAAAAAAgICAAACAAAAAAAAAAAAAAIAAgACAgACAAIAAAAAAgIAAAIAAAICAAAAAgICAgICAAAAAAIAAAIAoVSpVKlUqVSoB",
134        "fH9//YH27mz8AEDZs5mOALCOAODz/e6GAMD/4Z+AAIAAAIAAAAAAgICAgICAAACAAAAAAAAAAACAAICAgIAAAAAAgICAAAAAAAAA",
135        "AAAAgAAAAAAAAAAAgIAAgACAgACAAIAAgICAAACAgIAAgAAAgACAAICAgCpVKlUqVSpVAhyPAAD/AX6DAPj/4P8AAP6feJ9w/wDA",
136        "/4EAALvgswCAAIAAgICAgICAAIAAgACAgAAAgACAgACAAIAAAACAAIAAgICAgIAAAAAAAAAAAAAAAIAAgIAAgICAAICAAICAAICA",
137        "gACAgAAAAAAAAACAgACAgACAAAAAqtWq1arVqtWKmM4AgJkB4ICAGOaAzQAAZpPwmcCZAACAAICAu+CegACAAACAAIAAAIAAAACA",
138        "AAAAgIAAgAAAgACAgACAgIAAgAAAgIAAgAAAAAAAAAAAAIAAgICAAIAAAICAAACAAICAgACAgACAAAAAgICAAIAAAACAAICAAICo",
139        "1arVqtWq1aqBYP1B/4F+A36NAGB/wP+BAPCNAAB8f/4HgPf+4LubAIAAAIAAgICAAIAAAICAAAAAAAAAAACAAACAgIAAAACAAICA",
140        "gIAAAAAAAAAAAAAAAAAAAIAAgAAAgAAAgACAgIAAAAAAgICAgAAAAICAgAAAgIAAgIAAgEAqVSpVKlUqVQoAAAAAAO6DgIAAoPeA",
141        "gACAgAAAgICAAICAgIAAAACAAAAAAAAAgIAAAAAAgICAgAAAAICAgACAAAAAgAAAgIAAAACAgAAAAAAAAAAAAACAAICAgEAqAQCA",
142        "AICAAACAgAAAAIAAAACAgIAAgIAAgICAgICAAACAAKDVqtWq1aqVgAAAAIAAgACAgICAAACAgAAAAICAgACAgACAgAAAAICAgICA",
143        "AAAAgACAAAAAAACAgICAAAAAgACAAIAAAIAAAICAgAAAAIAAAAAAAAAAAICAAAAAgICAAAAAgAAAAAAAAACAAICAgACAAAAAAICA",
144        "AICAgIAAAIAAgICAAFUKAAAAgACAAAAAgACAAICAgACAgIAAgICAAACAAICAAICAAAAAAAAAAICAgIAAAAAAgACAAICAAIAAgACA",
145        "gICAAACAAACAAAAAgAAAAAAAAAAAgICAgIAAgIAAAACAgIAAAICAAICAAICAAACAgACAgICAgACAgIAAAIBQKlUqVSpVKgCAgAAA",
146        "gAAAAICAgACAgAAAyImAAIAAAIAAPr7gu58AAICAAICAAAAAgACAgIAAgAAAgIAAAIAAAACAgIAAAICAgICAgACAAAAAAAAAAAAA",
147        "gAAAAICAAICAgIAAAAAAgACAgIAAgIAAgIAAAIAAAAAAgIAAAIAAqNWq1arVqtWqAP/97veA/vn3Z4Fg5nzu+wBwmwAwvvf3jYDA",
148        "f2C/AACAAIAAAIAAgIAAAIAAgACAgACAgIAAAAAAgAAAgACAAAAAgAAAgIAAAAAAAAAAAACAAIAAgACAAACAAACAgIAAgICAgIAA",
149        "gICAAACAAIAAgIAAgACAAICq1arVqtWq1YLkjICA9wF+g4Bwf2D/gAB8H3gf8P+AYO8AAIDjwIyAAICAgACAAACAgAAAgAAAgACA",
150        "gACAgIAAgACAAAAAAACAAACAgACAgAAAAAAAAAAAgIAAgACAgICAgICAAAAAgICAgACAgACAgICAAICAgIAAgIAAAAAAgKrVqtWq",
151        "1arVivjzgQDnAA6DgGCZQGGAwBmEiIZgZIMAgAAAgO9gHwAAgACAAAAAgACAAIAAgICAgIAAgACAgACAgACAgAAAAACAAACAgAAA",
152        "AAAAAAAAAACAgACAgIAAAICAgIAAgIAAAACAAIAAgIAAgACAAIAAAAAAgICAAAAAAAAAAAAAAAAAAAAAgJGC/weBgABA/4CAAIAA",
153        "AIAAAICAgICAgACAgAAAgIAAAACAAAAAgICAgAAAAICAAICAgIAAgAAAAACAgIAAAIAAAAAAAAAAAAAAAACAgACAgIAAAIAAgIAA",
154        "AIAAgACAgAAAAACAAICAgACAAACAgACAgIAAKlUqVSpVKlUKgAAAAICRggCAAMDJAACAgICAAIAAgICAgICAAACAAACAgIAAgACA",
155        "AIAAAIAAgAAAAICAAACAAACAgAAAgIAAgACAgACAAAAAAAAAAAAAAACAAIBAKgGAgACAgACAgAAAgAAAgIAAAAAAgACAgIAAAICA",
156        "gICAgACg1arVqtWqlQCAAIAAAAAAAACAgAAAgICAAACAAACAgIAAAICAAIAAgIAAAIAAAACAgACAAAAAAICAgAAAAICAAICAgACA",
157        "AICAgICAgICAAAAAAAAAAAA="
158    ].join("");
159    const APPLE_FRAME_CYCLES=17030;
160    const DSCAN_TRACE_PC_MIN=0x1D23;
161    const DSCAN_TRACE_PC_MAX=0x1D35;
162    const SYNC_TRACE_LIMIT=512;
163    const HOST_CAMERA_SEED=0x12345678;
164
165    this.id={"PCODE":"DITHER2","icon":"fa fa-camera"};
166    this.deviceConfig=[{DCODE:"A2CAMERA",coID:"DithertizerCameraDevice",icon:"fa fa-camera",description:"Host camera",autoAttach:true}];
167    this.cameraDevice=null;
168    this.state={
169         "active":true
170        ,"threshold":0
171        ,"captureEnabled":false
172        ,"page2":false
173    };
174
175    var uiState={
176         "mode":"diffusion"
177        ,"preset":"atkinson"
178        ,"error":"accumulate"
179        ,"offset":0
180        ,"perceptual":false
181        ,"luma":80
182        ,"shift":1
183        ,"greyscale":false
184        ,"histogram":false
185        ,"gamma":130
186        ,"filter":"bilinear"
187        ,"errorCoeffs":{"A":1,"B":2,"C":2,"D":2,"E":1,"F":1}
188    };
189
190    function hgrLineAddress(pageBase,y)
191    {
192        return pageBase
193            + ((y & 0x07) << 10)
194            + ((y & 0x38) << 4)
195            + ((y & 0xC0) >> 1)
196            + ((y & 0xC0) >> 3);
197    }
198
199    function writableHardware(hw)
200    {
201        if(!hw) return null;
202
203        if(Array.isArray(hw.WR) && typeof(hw.lineDecode)==="function")
204        {
205            return {
206                write:function(addr,d8)
207                {
208                    var line=hw.lineDecode(addr);
209                    var fn=hw.WR[line];
210                    if(typeof(fn)!=="function") return false;
211                    fn(addr,d8);
212                    return true;
213                }
214            };
215        }
216
217        if(typeof(hw.write)==="function") return hw;
218        return null;
219    }
220
221    function driverByte(hw,address)
222    {
223        return hw.RD[1](address)&0xFF;
224    }
225
226    function driverMatches(hw,address,bytes)
227    {
228        for(var i=0;i<bytes.length;i++)
229            if(driverByte(hw,address+i)!==bytes[i]) return false;
230        return true;
231    }
232
233    function driverWrite(hw,address,bytes)
234    {
235        for(var i=0;i<bytes.length;i++) hw.WR[1](address+i,bytes[i]);
236    }
237
238    function dscanDriverMatches(hw)
239    {
240        return driverMatches(hw,0x1CCC,DSCAN_MERGE_SIGNATURE) &&
241               driverMatches(hw,0x1D03,DSCAN_SYNC_SIGNATURE);
242    }
243
244    function restoreDSCANEntry()
245    {
246        var hw=dscanPatchedHardware;
247        if(hw && driverMatches(hw,DSCAN_COLOR_ENTRY,DSCAN_COLOR_JUMP) && dscanDriverMatches(hw))
248            driverWrite(hw,DSCAN_COLOR_ENTRY,DSCAN_ORIGINAL_ENTRY.slice(0,3));
249        dscanPatchedHardware=null;
250    }
251
252    // The original disk's CALL 7168 uses four monochrome passes and clears D7
253    // in its PAGE2 merge. DITHER2 reroutes only that exact driver to its own
254    // synchronized, one-pass PAGE2 entry. The historical DITHER card is intact.
255    this.cycle=function()
256    {
257        if(card.cameraDevice && card.cameraDevice.isActive() && hostCameraAdapter && !cameraFrameBusy)
258        {
259            cameraFrameBusy=true;
260            var frameEpoch=hostCameraEpoch;
261            var frameToken=++cameraFrameToken;
262            Promise.resolve(captureHostCameraFrame(frameEpoch)).catch(function(error)
263            {
264                if(typeof(console)!=="undefined" && console && typeof(console.warn)==="function")
265                    console.warn("Dithertizer ConvertHGR camera frame failed",error);
266            }).then(function()
267            {
268                if(frameToken===cameraFrameToken) cameraFrameBusy=false;
269            });
270        }
271        if(typeof(apple2plus)==="undefined" || !apple2plus || typeof(apple2plus.hwObj)!=="function") return;
272        var hw=apple2plus.hwObj();
273        if(!hw || !hw.RD || !hw.WR || typeof(hw.RD[1])!=="function" || typeof(hw.WR[1])!=="function") return;
274        if(dscanPatchedHardware && dscanPatchedHardware!==hw) restoreDSCANEntry();
275        if(dscanPatchedHardware===hw && driverMatches(hw,DSCAN_COLOR_ENTRY,DSCAN_COLOR_JUMP)) return;
276        dscanPatchedHardware=null;
277        if(!driverMatches(hw,DSCAN_COLOR_ENTRY,DSCAN_ORIGINAL_ENTRY) || !dscanDriverMatches(hw)) return;
278        driverWrite(hw,DSCAN_COLOR_ENTRY,DSCAN_COLOR_JUMP);
279        dscanPatchedHardware=hw;
280    };
281
282    function resolveHardware(ctx)
283    {
284        var hw=writableHardware(ctx && ctx.hw);
285        if(hw) return hw;
286
287        hw=writableHardware(ctx && ctx.vid && ctx.vid.hw);
288        if(hw) return hw;
289
290        if(typeof(apple2plus)!=="undefined" && apple2plus && typeof(apple2plus.hwObj)==="function")
291            return writableHardware(apple2plus.hwObj());
292
293        return null;
294    }
295
296    function currentPage2(ctx)
297    {
298        if(ctx && ctx.vid && ctx.vid.state && typeof(ctx.vid.state.page2)==="boolean")
299            return ctx.vid.state.page2;
300
301        return false;
302    }
303
304    function clockTicks(ctx)
305    {
306        if(ctx && ctx.io && typeof(ctx.io.getClockTicks)==="function")
307            return Number(ctx.io.getClockTicks()) || 0;
308
309        if(typeof(oEMU)!=="undefined" && oEMU && oEMU.component && oEMU.component.IO &&
310           oEMU.component.IO.self && typeof(oEMU.component.IO.self.getClockTicks)==="function")
311            return Number(oEMU.component.IO.self.getClockTicks()) || 0;
312
313        return 0;
314    }
315
316    function syncPhase(ticks)
317    {
318        return ((Number(ticks)||0)%APPLE_FRAME_CYCLES+APPLE_FRAME_CYCLES)%APPLE_FRAME_CYCLES;
319    }
320
321    function currentPC()
322    {
323        try
324        {
325            if(typeof(apple2plus)!=="undefined" && apple2plus && typeof(apple2plus.cpuObj)==="function")
326            {
327                var cpu=apple2plus.cpuObj();
328                if(cpu && typeof(cpu.watch)==="function")
329                {
330                    var state=cpu.watch();
331                    if(state && Number.isFinite(Number(state.pc))) return Number(state.pc)&0xFFFF;
332                }
333            }
334        }
335        catch(e){}
336        return null;
337    }
338
339    function traceSyncD7(ticks,d8)
340    {
341        if(!syncTraceEnabled || syncTraceEntries.length>=SYNC_TRACE_LIMIT) return;
342
343        var pc=currentPC();
344        if(pc===null || pc<DSCAN_TRACE_PC_MIN || pc>DSCAN_TRACE_PC_MAX) return;
345
346        var d7=(Number(d8)&0x80) ? 1 : 0;
347        if(syncTraceLastD7!==null && d7===syncTraceLastD7) return;
348
349        var now=Math.trunc(Number(ticks)||0);
350        var entry={
351             "index":syncTraceEntries.length
352            ,"pc":pc
353            ,"clockTicks":now
354            ,"phase":syncPhase(now)
355            ,"d7":d7
356            ,"deltaTicks":syncTraceLastTick===null ? null : now-syncTraceLastTick
357        };
358        syncTraceEntries.push(entry);
359        syncTraceLastD7=d7;
360        syncTraceLastTick=now;
361
362        if(typeof(console)!=="undefined" && console && typeof(console.log)==="function")
363        {
364            var pcHex=pc.toString(16).toUpperCase().padStart(4,"0");
365            var width=entry.deltaTicks===null ? "start" : String(entry.deltaTicks)+" cycles";
366            console.log("Dithertizer DSCAN sync transition #"+entry.index+
367                " PC=$"+pcHex+" ticks="+entry.clockTicks+" phase="+entry.phase+
368                " D7="+entry.d7+" width="+width);
369        }
370    }
371
372    function syncD7(ticks)
373    {
374        var phase=syncPhase(ticks);
375        if(phase < 64) return 0x00;
376        if(phase < 96) return 0x80;
377        if(phase < 103) return 0x00;
378        return 0x80;
379    }
380
381    function normalizeSourceFrame(frame)
382    {
383        if(frame instanceof ArrayBuffer) frame=new Uint8Array(frame);
384        else if(typeof(ArrayBuffer)!=="undefined" && typeof(ArrayBuffer.isView)==="function" && ArrayBuffer.isView(frame))
385            frame=new Uint8Array(frame.buffer,frame.byteOffset,frame.byteLength);
386
387        if(!frame || typeof(frame.length)!=="number" || frame.length < HGR_WIDTH*HGR_HEIGHT)
388            throw new Error("Dithertizer camera source must provide at least 280x192 luminance bytes");
389
390        return frame;
391    }
392
393    function normalizeHGRPage(page)
394    {
395        if(page instanceof ArrayBuffer) page=new Uint8Array(page);
396        else if(typeof(ArrayBuffer)!=="undefined" && typeof(ArrayBuffer.isView)==="function" && ArrayBuffer.isView(page))
397            page=new Uint8Array(page.buffer,page.byteOffset,page.byteLength);
398
399        if(!page || typeof(page.length)!=="number" || page.length!==HGR_PAGE_BYTES)
400            throw new Error("Dithertizer color source must provide an 8192-byte HGR page");
401        return page;
402    }
403
404    function offlineHGRPage()
405    {
406        if(defaultHGRPage) return defaultHGRPage;
407        var alphabet="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
408        var page=new Uint8Array(HGR_PAGE_BYTES);
409        var value=0,bits=0,offset=0;
410        for(var i=0;i<DEFAULT_HGR_BASE64.length;i++)
411        {
412            var digit=alphabet.indexOf(DEFAULT_HGR_BASE64.charAt(i));
413            if(digit<0) continue; // Base64 padding.
414            value=(value<<6)|digit;
415            bits+=6;
416            if(bits>=8)
417            {
418                bits-=8;
419                page[offset++]=(value>>bits)&0xFF;
420            }
421        }
422        if(offset!==HGR_PAGE_BYTES) throw new Error("Invalid embedded Dithertizer HGR page");
423        defaultHGRPage=page;
424        return page;
425    }
426
427    this.setCameraSource=function(source)
428    {
429        if(source!==null && source!==undefined && typeof(source.getHGRPage)!=="function" &&
430           typeof(source.getLumaFrame)!=="function")
431            throw new TypeError("Dithertizer color source must expose getHGRPage() or getLumaFrame(width,height)");
432
433        cameraSource=source || null;
434        return cameraSource;
435    };
436
437    this.getCameraSource=function(){return cameraSource;};
438
439    this.setSyncTrace=function(enabled)
440    {
441        syncTraceEnabled=!!enabled;
442        syncTraceEntries=[];
443        syncTraceLastD7=null;
444        syncTraceLastTick=null;
445        if(syncTraceEnabled && typeof(console)!=="undefined" && console && typeof(console.info)==="function")
446            console.info("Dithertizer DSCAN sync trace enabled for PC $1D23-$1D35");
447        return syncTraceEnabled;
448    };
449
450    this.clearSyncTrace=function()
451    {
452        syncTraceEntries=[];
453        syncTraceLastD7=null;
454        syncTraceLastTick=null;
455    };
456
457    this.getSyncTrace=function(){return syncTraceEntries.slice();};
458    this.dumpSyncTrace=function()
459    {
460        var out=card.getSyncTrace();
461        if(typeof(console)!=="undefined" && console)
462        {
463            if(typeof(console.table)==="function") console.table(out);
464            else if(typeof(console.log)==="function") console.log(out);
465        }
466        return out;
467    };
468
469    this.readVideoSync=function(ctx)
470    {
471        var ticks=clockTicks(ctx);
472        var d8=syncD7(ticks);
473        traceSyncD7(ticks,d8);
474        return d8;
475    };
476    this.stopCapture=function(){card.state.captureEnabled=false;};
477
478    this.startCapture=function(ctx)
479    {
480        card.state.captureEnabled=true;
481        card.state.page2=currentPage2(ctx);
482
483        var hw=resolveHardware(ctx);
484        if(!hw) return false;
485
486        var pageBase=card.state.page2 ? 0x4000 : 0x2000;
487
488        var page=null;
489        if(cameraSource && typeof(cameraSource.getHGRPage)==="function")
490            page=normalizeHGRPage(cameraSource.getHGRPage());
491        else if(!cameraSource)
492            page=hostCameraPage || offlineHGRPage();
493
494        if(page)
495        {
496            for(var py=0;py<HGR_HEIGHT;py++)
497            {
498                var offset=hgrLineAddress(0,py);
499                var destination=pageBase+offset;
500                for(var px=0;px<HGR_BYTES_PER_LINE;px++)
501                    hw.write(destination+px,page[offset+px]&0xFF);
502            }
503            return true;
504        }
505
506        // Keep luminance-only injected sources usable for diagnostics.
507        var frame=normalizeSourceFrame(cameraSource.getLumaFrame(HGR_WIDTH,HGR_HEIGHT));
508        var threshold=card.state.threshold & 0xFF;
509
510        for(var y=0;y<HGR_HEIGHT;y++)
511        {
512            var line=hgrLineAddress(pageBase,y);
513            var row=y*HGR_WIDTH;
514            for(var xb=0;xb<HGR_BYTES_PER_LINE;xb++)
515            {
516                var d8=0;
517                var x0=xb*7;
518                for(var bit=0;bit<7;bit++)
519                    if((frame[row+x0+bit]&0xFF) >= threshold)
520                        d8 |= (1 << bit);
521                hw.write(line+xb,d8&0x7F);
522            }
523        }
524        return true;
525    };
526
527    this.readSlotIO=function(addr,ctx)
528    {
529        var reg=Number(addr)&0x0F;
530        var safe=ctx && ctx.bRO===true;
531        switch(reg)
532        {
533            case 0x00:
534            {
535                var d8=card.readVideoSync(ctx);
536                if(!safe) card.stopCapture();
537                return d8;
538            }
539            case 0x08:
540                if(!safe) card.startCapture(ctx);
541                return 0x00;
542        }
543        return 0x00;
544    };
545
546    this.writeSlotIO=function(addr,d8,ctx)
547    {
548        var reg=Number(addr)&0x0F;
549        if(reg===0x00)
550        {
551            card.stopCapture();
552            card.state.threshold=Number(d8)&0xFF;
553        }
554        return 0x00;
555    };
556
557    function uiOption(value,label,current)
558    {
559        return "<option value=\""+value+"\""+(String(value)===String(current) ? " selected" : "")+">"+label+"</option>";
560    }
561
562    function uiNumber(value,min,max)
563    {
564        value=Number(value);
565        if(!Number.isFinite(value)) return null;
566        value=Math.round(value);
567        if(value<min) value=min;
568        if(value>max) value=max;
569        return value;
570    }
571
572    function uiReadout(controlID,setting,value)
573    {
574        if(typeof(document)==="undefined" || !document || typeof(document.getElementById)!=="function") return;
575        var el=document.getElementById(controlID+"_"+setting+"_value");
576        if(!el) return;
577        if(setting==="rate") el.textContent=(Number(value)/1000).toFixed(2)+"s";
578        else
579        {
580            var suffix=(setting==="luma" || setting==="shift" || setting==="gamma") ? "%" : "";
581            el.textContent=String(value)+suffix;
582        }
583    }
584
585    function presetError(name)
586    {
587        var presets={
588             "atkinson":{"A":1,"B":2,"C":2,"D":2,"E":1,"F":1}
589            ,"floyd":{"A":3,"B":5,"C":1,"D":7,"E":0,"F":0}
590            ,"pattern":{"A":0,"B":8,"C":0,"D":8,"E":0,"F":0}
591            ,"diag":{"A":1,"B":3,"C":2,"D":3,"E":1,"F":1}
592            ,"none":{"A":0,"B":0,"C":0,"D":0,"E":0,"F":0}
593        };
594        return presets[name] || presets.atkinson;
595    }
596
597    function buildConvertHGRSettings()
598    {
599        var orderedMode=0,mapSize=2;
600        if(uiState.mode==="order1"){orderedMode=1;mapSize=2;}
601        else if(uiState.mode==="order2"){orderedMode=2;mapSize=2;}
602        else if(uiState.mode==="order3"){orderedMode=1;mapSize=4;}
603        else if(uiState.mode==="order4"){orderedMode=2;mapSize=4;}
604
605        var e=uiState.mode==="none" ? {A:0,B:0,C:0,D:0,E:0,F:0} : uiState.errorCoeffs;
606        return {
607            image:{
608                 greyscale:!!uiState.greyscale
609                ,stretchHistogram:!!uiState.histogram
610                ,gamma:Number(uiState.gamma)/100
611                ,maxColorShift:Number(uiState.shift)
612            },
613            scaling:{
614                 filter:uiState.filter
615                ,fillMode:"default"
616                ,horizontalNudge:0
617                ,verticalNudge:0
618                ,applePixelAspect:256/280
619            },
620            matching:{
621                 perceptual:!!uiState.perceptual
622                ,lumaEmphasis:Number(uiState.luma)/100
623                ,perceptualR:0.30
624                ,perceptualG:0.52
625                ,perceptualB:0.18
626            },
627            dither:{
628                 orderedMode:orderedMode
629                ,mapSize:mapSize
630                ,orderedOffset:Number(uiState.offset)
631                ,accumulateErrors:uiState.mode!=="none" && uiState.error==="accumulate"
632                ,error:{A:e.A,B:e.B,C:e.C,D:e.D,E:e.E,F:e.F}
633            }
634        };
635    }
636
637    function ensureConvertHGRAdapterCtor()
638    {
639        if(typeof(DithertizerConvertHGRAdapter)==="function")
640            return Promise.resolve(DithertizerConvertHGRAdapter);
641        if(convertHGRAdapterLoadPromise) return convertHGRAdapterLoadPromise;
642        if(typeof(document)==="undefined" || !document || typeof(document.createElement)!=="function" ||
643           !document.head || typeof(document.head.appendChild)!=="function")
644            return Promise.resolve(null);
645
646        convertHGRAdapterLoadPromise=new Promise(function(resolve,reject)
647        {
648            var script=document.createElement("script");
649            script.type="text/javascript";
650            script.src="res/EMU_DITHERTIZER_converthgr.js";
651            script.onload=function()
652            {
653                if(typeof(DithertizerConvertHGRAdapter)==="function") resolve(DithertizerConvertHGRAdapter);
654                else reject(new Error("ConvertHGR adapter did not register"));
655            };
656            script.onerror=function(){reject(new Error("Unable to load ConvertHGR adapter"));};
657            document.head.appendChild(script);
658        }).catch(function(error)
659        {
660            convertHGRAdapterLoadPromise=null;
661            throw error;
662        });
663        return convertHGRAdapterLoadPromise;
664    }
665
666    function stabilizeCameraPhase(page,processedRGB)
667    {
668        if(!processedRGB || processedRGB.length!==HGR_WIDTH*HGR_HEIGHT*3)
669        {
670            hostCameraPhaseReference=null;
671            return;
672        }
673
674        // ConvertHGR may choose a different D7 phase for virtually identical
675        // camera pixels. Retain the phase while the byte's source chroma stays
676        // close to its last accepted value; always use the new seven pixel bits.
677        var reference=new Int16Array(HGR_HEIGHT*HGR_BYTES_PER_LINE*2);
678        for(var y=0;y<HGR_HEIGHT;y++)
679        {
680            var row=hgrLineAddress(0,y);
681            for(var xb=0;xb<HGR_BYTES_PER_LINE;xb++)
682            {
683                var index=y*HGR_BYTES_PER_LINE+xb;
684                var rgbIndex=(y*HGR_WIDTH+xb*7)*3;
685                var rg=0,bg=0;
686                for(var pixel=0;pixel<7;pixel++,rgbIndex+=3)
687                {
688                    rg+=processedRGB[rgbIndex]-processedRGB[rgbIndex+1];
689                    bg+=processedRGB[rgbIndex+2]-processedRGB[rgbIndex+1];
690                }
691                var refIndex=index*2;
692                if(hostCameraPage && hostCameraPhaseReference &&
693                   Math.abs(rg-hostCameraPhaseReference[refIndex])+
694                   Math.abs(bg-hostCameraPhaseReference[refIndex+1])<=7*PHASE_CHROMA_TOLERANCE)
695                {
696                    page[row+xb]=(page[row+xb]&0x7F)|(hostCameraPage[row+xb]&0x80);
697                    reference[refIndex]=hostCameraPhaseReference[refIndex];
698                    reference[refIndex+1]=hostCameraPhaseReference[refIndex+1];
699                }
700                else
701                {
702                    reference[refIndex]=rg;
703                    reference[refIndex+1]=bg;
704                }
705            }
706        }
707        hostCameraPhaseReference=reference;
708    }
709
710    async function captureHostCameraFrame(epoch)
711    {
712        var device=card.cameraDevice;
713        if(!device || !device.isActive() || !hostCameraAdapter || epoch!==hostCameraEpoch) return false;
714        var frame=device.ports.video.read();
715        if(!frame || !frame.rgb || frame.rgb.length!==frame.width*frame.height*3) return false;
716
717        hostCameraAdapter.configure(buildConvertHGRSettings());
718        var result=await hostCameraAdapter.convert(frame.rgb,frame.width,frame.height,HOST_CAMERA_SEED);
719        if(device!==card.cameraDevice || !device.isActive() || epoch!==hostCameraEpoch) return false;
720
721        var nextPage=new Uint8Array(normalizeHGRPage(result && result.hgrPage));
722        stabilizeCameraPhase(nextPage,result && result.processedRGB);
723        hostCameraPage=nextPage;
724        return true;
725    }
726
727    async function startHostCameraBridge(epoch)
728    {
729        var AdapterCtor=await ensureConvertHGRAdapterCtor();
730        if(!AdapterCtor || !card.cameraDevice || !card.cameraDevice.isActive() || epoch!==hostCameraEpoch)
731            return false;
732        var adapter=new AdapterCtor({backend:wasmEnabled ? "wasm" : "javascript"});
733        try { await adapter.init(); }
734        catch(error) { if(typeof(adapter.close)==="function") adapter.close(); throw error; }
735        if(typeof(adapter.setBackend)==="function")
736            adapter.setBackend(wasmEnabled ? "wasm" : "javascript");
737        if(!card.cameraDevice || !card.cameraDevice.isActive() || epoch!==hostCameraEpoch)
738        {
739            if(typeof(adapter.close)==="function") adapter.close();
740            return false;
741        }
742        hostCameraAdapter=adapter;
743        await captureHostCameraFrame(epoch);
744        return true;
745    }
746
747    function stopHostCamera()
748    {
749        hostCameraEpoch++;
750        cameraFrameToken++;
751        cameraFrameBusy=false;
752        if(hostCameraAdapter && typeof(hostCameraAdapter.close)==="function") hostCameraAdapter.close();
753        hostCameraAdapter=null;
754        hostCameraPage=null;
755        hostCameraPhaseReference=null;
756        if(card.cameraDevice) card.cameraDevice.stop();
757    }
758
759    function updateCameraButton(controlID)
760    {
761        if(typeof(document)==="undefined" || !document || typeof(document.getElementById)!=="function") return;
762        var button=document.getElementById(controlID+"_camera");
763        if(!button) return;
764        var active=!!(card.cameraDevice && card.cameraDevice.isActive());
765        button.setAttribute("aria-pressed",active ? "true" : "false");
766        button.title=active ? "Stop host camera" : "Start host camera";
767        var icon=button.querySelector ? button.querySelector("i") : null;
768        if(icon && icon.style) icon.style.color=active ? "#0a0" : "";
769        var status=document.getElementById(controlID+"_camera_status");
770        if(status) status.textContent=active ? "ON" : "OFF";
771    }
772
773    function updateWasmButton(controlID)
774    {
775        if(typeof(document)==="undefined" || !document || typeof(document.getElementById)!=="function") return;
776        var button=document.getElementById(controlID+"_wasm");
777        if(!button) return;
778        button.setAttribute("aria-pressed",wasmEnabled ? "true" : "false");
779        button.title=wasmEnabled ? "Use JavaScript ConvertHGR" : "Use WASM ConvertHGR";
780        var icon=button.querySelector ? button.querySelector("i") : null;
781        if(icon && icon.style) icon.style.color=wasmEnabled ? "#0a0" : "";
782        var status=document.getElementById(controlID+"_wasm_status");
783        if(status) status.textContent=wasmEnabled ? "ON" : "OFF";
784    }
785
786    this.deviceToolWasmToggle=function(controlID)
787    {
788        hostCameraControlID=String(controlID || "");
789        wasmEnabled=!wasmEnabled;
790        // Discard the result of a conversion started with the previous backend.
791        if(hostCameraAdapter)
792        {
793            hostCameraEpoch++;
794            if(typeof(hostCameraAdapter.setBackend)==="function")
795                hostCameraAdapter.setBackend(wasmEnabled ? "wasm" : "javascript");
796        }
797        updateWasmButton(hostCameraControlID);
798        return wasmEnabled;
799    };
800
801    this.deviceToolCameraToggle=async function(controlID)
802    {
803        hostCameraControlID=String(controlID || "");
804        var device=card.cameraDevice;
805        if(!device) return false;
806        if(device.isActive() || device.isPending())
807        {
808            stopHostCamera();
809            updateCameraButton(hostCameraControlID);
810            return false;
811        }
812        var epoch=++hostCameraEpoch;
813        var started=await device.start();
814        if(epoch!==hostCameraEpoch || device!==card.cameraDevice || !started) return false;
815        try
816        {
817            started=await startHostCameraBridge(epoch);
818            if(!started || epoch!==hostCameraEpoch) return false;
819            updateCameraButton(hostCameraControlID);
820            return true;
821        }
822        catch(error)
823        {
824            if(epoch===hostCameraEpoch) stopHostCamera();
825            updateCameraButton(hostCameraControlID);
826            return false;
827        }
828    };
829
830    function updateModeControls(controlID)
831    {
832        if(typeof(document)==="undefined" || !document || typeof(document.getElementById)!=="function") return;
833        var error=document.getElementById(controlID+"_error");
834        var offset=document.getElementById(controlID+"_offset");
835        if(error) error.disabled=uiState.mode==="none";
836        if(offset) offset.disabled=uiState.mode==="none" || uiState.mode==="diffusion";
837    }
838
839    this.deviceToolSetting=function(controlID,setting,value)
840    {
841        controlID=String(controlID || "");
842        setting=String(setting || "");
843        var allowed;
844        switch(setting)
845        {
846            case "mode":
847                allowed=["none","diffusion","order1","order2","order3","order4"];
848                if(allowed.indexOf(String(value))<0) return false;
849                uiState.mode=String(value);
850                updateModeControls(controlID);
851                if(uiState.mode==="order2" || uiState.mode==="order4")
852                    uiState.errorCoeffs={"A":1,"B":2,"C":2,"D":2,"E":0,"F":0};
853                return true;
854            case "preset":
855                allowed=["atkinson","floyd","pattern","diag","none"];
856                if(allowed.indexOf(String(value))<0) return false;
857                uiState.preset=String(value);uiState.errorCoeffs=presetError(uiState.preset);return true;
858            case "error":
859                allowed=["accumulate","average"];
860                if(allowed.indexOf(String(value))<0) return false;
861                if(uiState.mode==="none") return false;
862                uiState.error=String(value);return true;
863            case "filter":
864                allowed=["box","gaussian","hamming","blackman","bilinear"];
865                if(allowed.indexOf(String(value))<0) return false;
866                uiState.filter=String(value);return true;
867            case "offset":
868                if(uiState.mode==="none" || uiState.mode==="diffusion") return false;
869                value=uiNumber(value,0,16);if(value===null)return false;uiState.offset=value;uiReadout(controlID,"offset",value);return true;
870            case "luma":
871                value=uiNumber(value,0,500);if(value===null)return false;uiState.luma=value;uiReadout(controlID,"luma",value);return true;
872            case "shift":
873                value=uiNumber(value,0,100);if(value===null)return false;uiState.shift=value;uiReadout(controlID,"shift",value);return true;
874            case "gamma":
875                value=uiNumber(value,0,500);if(value===null)return false;uiState.gamma=value;uiReadout(controlID,"gamma",value);return true;
876        }
877        return false;
878    };
879
880    this.deviceToolFlag=function(controlID,setting,enabled)
881    {
882        setting=String(setting || "");
883        if(setting!=="perceptual" && setting!=="greyscale" && setting!=="histogram") return false;
884        uiState[setting]=!!enabled;
885        return true;
886    };
887
888    this.deviceToolSlotHTML=function(ctx)
889    {
890        ctx=ctx || {};
891        var toolboxID=ctx.toolboxID || ("device_tool_"+ctx.slotID);
892        var slotID=ctx.slotID==null ? "?" : String(ctx.slotID);
893        var slotN=Number(ctx.slotN);
894        var controlID="dither_ctrl_"+slotID;
895        var call="apple2plus.hwObj().io.SLOT2obj("+slotN+")";
896
897        var modeOptions=""+uiOption("none","None",uiState.mode)+uiOption("diffusion","Error diffusion",uiState.mode)+uiOption("order1","Order1",uiState.mode)+uiOption("order2","Order2",uiState.mode)+uiOption("order3","Order3",uiState.mode)+uiOption("order4","Order4",uiState.mode);
898        var presetOptions=""+uiOption("atkinson","Atkinson",uiState.preset)+uiOption("floyd","Floyd-Stein",uiState.preset)+uiOption("pattern","Pattern",uiState.preset)+uiOption("diag","Diag",uiState.preset)+uiOption("none","None",uiState.preset);
899        var errorOptions=""+uiOption("accumulate","Accumulate",uiState.error)+uiOption("average","Average",uiState.error);
900        var filterOptions=""+uiOption("box","Box",uiState.filter)+uiOption("gaussian","Gaussian",uiState.filter)+uiOption("hamming","Hamming",uiState.filter)+uiOption("blackman","Blackman",uiState.filter)+uiOption("bilinear","Bilinear",uiState.filter);
901
902        var perceptualChecked=uiState.perceptual ? " checked" : "";
903        var greyscaleChecked=uiState.greyscale ? " checked" : "";
904        var histogramChecked=uiState.histogram ? " checked" : "";
905        var cameraActive=!!(card.cameraDevice && card.cameraDevice.isActive());
906        var rowStyle="height:21px;display:flex;align-items:center;gap:4px;";
907        var headStyle="display:inline-block;width:28px;";
908        var selectStyle="height:19px;font:11px monospace;padding:0px 1px;";
909        var sliderStyle="width:68px;height:15px;padding:0;margin:0;";
910        var compactOffsetStyle="width:54px;height:15px;padding:0;margin:0;";
911        var valueStyle="display:inline-block;width:30px;font:10px monospace;text-align:right;";
912
913        return ""
914            +"<div class=toolbox id=\""+toolboxID+"\" hidden>"
915            +" <div class=appbox style=\"box-sizing:border-box;text-align:left;height:76px;padding:3px 6px;display:flex;flex-direction:column;gap:2px;font-size:11px;white-space:nowrap;\">"
916            +"  <div data-dither-row=\"DTH\" style=\""+rowStyle+"\">"
917            +"   <b style=\""+headStyle+"\">DTH</b><span>MODE</span>"
918            +"   <select id=\""+controlID+"_mode\" title=\"Dithering mode\" onchange=\""+call+".deviceToolSetting('"+controlID+"','mode',this.value)\" style=\""+selectStyle+"\">"+modeOptions+"</select>"
919            +"   <span>OFFSET</span><input id=\""+controlID+"_offset\" type=\"range\" min=\"0\" max=\"16\" step=\"1\" value=\""+uiState.offset+"\" title=\"Ordered offset\""+(uiState.mode==="diffusion" || uiState.mode==="none" ? " disabled" : "")+" oninput=\""+call+".deviceToolSetting('"+controlID+"','offset',this.value)\" style=\""+compactOffsetStyle+"\">"
920            +"   <span id=\""+controlID+"_offset_value\" style=\"width:17px;font:10px monospace;text-align:right;\">"+uiState.offset+"</span>"
921            +"   <select id=\""+controlID+"_preset\" title=\"Error-diffusion preset\" onchange=\""+call+".deviceToolSetting('"+controlID+"','preset',this.value)\" style=\""+selectStyle+"\">"+presetOptions+"</select>"
922            +"   <span>ERR</span><select id=\""+controlID+"_error\" title=\"Incoming error handling\""+(uiState.mode==="none" ? " disabled" : "")+" onchange=\""+call+".deviceToolSetting('"+controlID+"','error',this.value)\" style=\""+selectStyle+"\">"+errorOptions+"</select>"
923            +"  </div>"
924            +"  <div data-dither-row=\"COL\" style=\""+rowStyle+"\">"
925            +"   <b style=\""+headStyle+"\">COL</b><label title=\"Use perceptual RGB colour matching\" style=\"display:flex;align-items:center;gap:2px;\"><input id=\""+controlID+"_perceptual\" type=\"checkbox\""+perceptualChecked+" onchange=\""+call+".deviceToolFlag('"+controlID+"','perceptual',this.checked)\">Perceptual RGB</label>"
926            +"   <span>LUMA</span><input id=\""+controlID+"_luma\" type=\"range\" min=\"0\" max=\"500\" step=\"1\" value=\""+uiState.luma+"\" title=\"Luma emphasis\" oninput=\""+call+".deviceToolSetting('"+controlID+"','luma',this.value)\" style=\""+sliderStyle+"\"><span id=\""+controlID+"_luma_value\" style=\""+valueStyle+"\">"+uiState.luma+"%</span>"
927            +"   <span>
927SHIFT</span><input id=\""+controlID+"_shift\" type=\"range\" min=\"0\" max=\"100\" step=\"1\" value=\""+uiState.shift+"\" title=\"Maximum colour shift\" oninput=\""+call+".deviceToolSetting('"+controlID+"','shift',this.value)\" style=\""+sliderStyle+"\"><span id=\""+controlID+"_shift_value\" style=\""+valueStyle+"\">"+uiState.shift+"%</span></div>"
928            +"  <div data-dither-row=\"IMG\" style=\""+rowStyle+"\">"
929            +"   <b style=\""+headStyle+"\">IMG</b><label style=\"display:flex;align-items:center;gap:2px;\"><input id=\""+controlID+"_greyscale\" type=\"checkbox\""+greyscaleChecked+" onchange=\""+call+".deviceToolFlag('"+controlID+"','greyscale',this.checked)\">Greyscale</label>"
930            +"   <label style=\"display:flex;align-items:center;gap:2px;\"><input id=\""+controlID+"_histogram\" type=\"checkbox\""+histogramChecked+" onchange=\""+call+".deviceToolFlag('"+controlID+"','histogram',this.checked)\">Stretch histo</label>"
931            +"   <span>GAMMA</span><input id=\""+controlID+"_gamma\" type=\"range\" min=\"0\" max=\"500\" step=\"1\" value=\""+uiState.gamma+"\" title=\"Gamma\" oninput=\""+call+".deviceToolSetting('"+controlID+"','gamma',this.value)\" style=\""+sliderStyle+"\"><span id=\""+controlID+"_gamma_value\" style=\""+valueStyle+"\">"+uiState.gamma+"%</span>"
932            +"   <span>FILTER</span><select id=\""+controlID+"_filter\" title=\"Scaling filter\" onchange=\""+call+".deviceToolSetting('"+controlID+"','filter',this.value)\" style=\""+selectStyle+"\">"+filterOptions+"</select>"
933            +"   <button id=\""+controlID+"_camera\" class=\"appbut skinny\" type=\"button\" aria-pressed=\""+(cameraActive ? "true" : "false")+"\" title=\""+(cameraActive ? "Stop host camera" : "Start host camera")+"\" onclick=\""+call+".deviceToolCameraToggle('"+controlID+"')\" style=\"margin-left:auto;height:19px;padding:1px 5px;\"><i class=\"fa fa-camera\" style=\""+(cameraActive ? "color:#0a0;" : "")+"\"></i></button>"
934            +"   <span id=\""+controlID+"_camera_status\" style=\"display:inline-block;width:20px;font-size:10px;text-align:center;
934color:#777;\">"+(cameraActive ? "ON" : "OFF")+"</span>"
935            +"   <button id=\""+controlID+"_wasm\" class=\"appbut skinny\" type=\"button\" aria-pressed=\""+(wasmEnabled ? "true" : "false")+"\" title=\""+(wasmEnabled ? "Use JavaScript ConvertHGR" : "Use WASM ConvertHGR")+"\" onclick=\""+call+".deviceToolWasmToggle('"+controlID+"')\" style=\"height:19px;padding:1px 5px;\"><i class=\"fa fa-microchip\" style=\""+(wasmEnabled ? "color:#0a0;" : "")+"\"></i></button>"
936            +"   <span id=\""+controlID+"_wasm_status\" style=\"display:inline-block;width:20px;font-size:10px;text-align:center;color:#777;\">"+(wasmEnabled ? "ON" : "OFF")+"</span></div>"
937            +" </div></div>";
938    };
939
940    this.action={"SlotIO":{"RD":{"callback":function(addr,ctx){return card.readSlotIO(addr,ctx);}},"WR":{"callback":function(addr,d8,ctx){return card.writeSlotIO(addr,d8,ctx);}}}};
941
942    this.reset=function()
943    {
944        restoreDSCANEntry();
945        stopHostCamera();
946        if(hostCameraControlID) updateCameraButton(hostCameraControlID);
947        if(hostCameraControlID) updateWasmButton(hostCameraControlID);
948        card.state.threshold=0;
949        card.state.captureEnabled=false;
950        card.state.page2=false;
951    };
952    this.restart=this.reset;
953    this.onUnmount=this.reset;
954}
955
956if(typeof(oEMU)==="undefined")
957    var oEMU={"component":{"IO":{}}};
958else
959{
960    if(!oEMU.component) oEMU.component={};
961    if(!oEMU.component.IO) oEMU.component.IO={};
962}
963oEMU.component.IO.DithertizerII_2=new DithertizerII_2();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.