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.