Skip to content

Commit 0103552

Browse files
committed
Asset editor Apple II artifact color
Use `art:true` to indicate Apple II HGR graphics artifact color. Renders bit 7 separately.
1 parent 4f886b9 commit 0103552

2 files changed

Lines changed: 155 additions & 11 deletions

File tree

src/ide/pixeleditor.ts

Lines changed: 153 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,14 @@ export type PixelEditorImageFormat = {
4848
il?: boolean // interleave images row by row
4949
aspect?: number // aspect ratio
5050
xform?: string // CSS transform
51+
art?: number // artifact color, Apple II
5152
destfmt?: PixelEditorImageFormat
5253
};
5354

55+
function getArtBit(fmt: PixelEditorImageFormat): number | null {
56+
return fmt.art ? (fmt.brev ? 0 : 7) : null;
57+
}
58+
5459
export type PixelEditorPaletteFormat = {
5560
pal?: number | string
5661
n?: number
@@ -836,6 +841,7 @@ export class ImageChooser {
836841
rgbimgs: Uint32Array[];
837842
width: number;
838843
height: number;
844+
gapX: number | null = null;
839845
viewers: Viewer[];
840846

841847
recreate(parentdiv: JQuery, onclick) {
@@ -849,8 +855,9 @@ export class ImageChooser {
849855
var viewer = new Viewer();
850856
viewer.width = this.width;
851857
viewer.height = this.height;
858+
viewer.gapX = this.gapX;
852859
viewer.recreate();
853-
viewer.canvas.style.width = (viewer.width * cscale) + 'px'; // TODO
860+
viewer.canvas.style.width = (viewer.displayWidth * cscale) + 'px'; // TODO
854861
viewer.canvas.title = '$' + hex(i);
855862
viewer.updateImage(imdata);
856863
$(viewer.canvas).addClass('asset_cell');
@@ -909,6 +916,7 @@ export class CharmapEditor extends PixNode {
909916
this.rgbimgs = this.left.rgbimgs;
910917
// if chooser already exists with same number of images, update in place
911918
if (this.chooser && this.chooser.viewers && this.chooser.viewers.length == this.rgbimgs.length) {
919+
this.updateArtInfo();
912920
this.chooser.updateImages(this.rgbimgs);
913921
// keep rgbimgs pointing to viewer buffers so edits propagate back via refreshLeft
914922
for (var i = 0; i < this.chooser.viewers.length; i++) {
@@ -924,17 +932,27 @@ export class CharmapEditor extends PixNode {
924932
chooser.rgbimgs = this.rgbimgs;
925933
chooser.width = this.fmt.w || 1;
926934
chooser.height = this.fmt.h || 1;
935+
var artBit = getArtBit(this.fmt);
936+
if (artBit != null) {
937+
chooser.gapX = this.fmt.brev ? artBit + 1 : artBit;
938+
}
927939
chooser.recreate(agrid, (index, viewer) => {
928940
// TODO: variable scale?
929941
const aspect = this.fmt.aspect || 1.0;
930942
const yscale = Math.min(MAX_SCALE,
931-
MAX_SIZE_X / this.fmt.w * aspect,
943+
MAX_SIZE_X / viewer.displayWidth * aspect,
932944
MAX_SIZE_Y / this.fmt.h);
933945
const xscale = yscale * aspect;
934946
this.createEditor(aeditor, viewer, xscale, yscale);
935947
this.context.setCurrentEditor(aeditor, $(viewer.canvas), this);
936948
this.rgbimgs[index] = viewer.rgbdata;
937949
});
950+
this.updateArtInfo();
951+
if (this.fmt.art) {
952+
for (var i = 0; i < this.chooser.viewers.length; i++) {
953+
this.chooser.viewers[i].updateImage();
954+
}
955+
}
938956
// add palette selector
939957
// TODO: only view when editing?
940958
var palizer = this.left;
@@ -955,11 +973,27 @@ export class CharmapEditor extends PixNode {
955973
return true;
956974
}
957975

976+
updateArtInfo() {
977+
if (!this.fmt.art || !this.chooser || !this.chooser.viewers) return;
978+
var bitsperword = this.fmt.bpw || 8;
979+
var artBit = getArtBit(this.fmt)!;
980+
var indexedImages = this.left.images;
981+
if (!indexedImages) return;
982+
for (var i = 0; i < this.chooser.viewers.length; i++) {
983+
var viewer = this.chooser.viewers[i];
984+
viewer.artInfo = {
985+
artBit: artBit,
986+
bitsperword: bitsperword,
987+
indexedImage: indexedImages[i]
988+
};
989+
}
990+
}
991+
958992
createEditor(aeditor: JQuery, viewer: Viewer, xscale: number, yscale: number): PixEditor {
959993
var im = new PixEditor();
960994
im.createWith(viewer);
961995
im.updateImage();
962-
var w = viewer.width * xscale;
996+
var w = im.displayWidth * xscale;
963997
var h = viewer.height * yscale;
964998
im.canvas.style.width = w + 'px'; // TODO
965999
im.canvas.style.height = h + 'px'; // TODO
@@ -1006,32 +1040,50 @@ export class Viewer {
10061040

10071041
width: number;
10081042
height: number;
1043+
gapX: number | null = null;
10091044
canvas: HTMLCanvasElement;
10101045
ctx: CanvasRenderingContext2D;
10111046
imagedata: ImageData;
10121047
rgbdata: Uint32Array;
1048+
displayImageData: ImageData;
1049+
displayRgbData: Uint32Array;
10131050
peerviewers: Viewer[];
1051+
artInfo: { artBit: number, bitsperword: number, indexedImage: Uint8Array } | null = null;
1052+
1053+
get displayWidth(): number {
1054+
return this.width + (this.gapX != null ? 1 : 0);
1055+
}
10141056

10151057
recreate() {
10161058
this.canvas = this.newCanvas();
10171059
this.imagedata = this.ctx.createImageData(this.width, this.height);
10181060
this.rgbdata = new Uint32Array(this.imagedata.data.buffer);
1061+
if (this.gapX != null) {
1062+
this.displayImageData = this.ctx.createImageData(this.displayWidth, this.height);
1063+
this.displayRgbData = new Uint32Array(this.displayImageData.data.buffer);
1064+
}
10191065
this.peerviewers = [this];
10201066
}
10211067

10221068
createWith(pv: Viewer) {
10231069
this.width = pv.width;
10241070
this.height = pv.height;
1071+
this.gapX = pv.gapX;
10251072
this.imagedata = pv.imagedata;
10261073
this.rgbdata = pv.rgbdata;
10271074
this.canvas = this.newCanvas();
1075+
if (this.gapX != null) {
1076+
this.displayImageData = this.ctx.createImageData(this.displayWidth, this.height);
1077+
this.displayRgbData = new Uint32Array(this.displayImageData.data.buffer);
1078+
}
10281079
pv.peerviewers.push(this);
10291080
this.peerviewers = pv.peerviewers;
1081+
this.artInfo = pv.artInfo;
10301082
}
10311083

10321084
newCanvas(): HTMLCanvasElement {
10331085
var c = document.createElement('canvas');
1034-
c.width = this.width;
1086+
c.width = this.displayWidth;
10351087
c.height = this.height;
10361088
//if (fmt.xform) c.style.transform = fmt.xform;
10371089
c.classList.add("pixels");
@@ -1045,7 +1097,24 @@ export class Viewer {
10451097
this.rgbdata.set(imdata);
10461098
}
10471099
for (let v of this.peerviewers) {
1048-
v.ctx.putImageData(this.imagedata, 0, 0);
1100+
if (v.gapX != null && v.displayRgbData) {
1101+
var gx = v.gapX;
1102+
var dw = v.displayWidth;
1103+
for (var y = 0; y < this.height; y++) {
1104+
var srcOfs = y * this.width;
1105+
var dstOfs = y * dw;
1106+
for (var x = 0; x < gx; x++) {
1107+
v.displayRgbData[dstOfs + x] = this.rgbdata[srcOfs + x];
1108+
}
1109+
v.displayRgbData[dstOfs + gx] = 0x00000000; // Transparent gap column.
1110+
for (var x = gx; x < this.width; x++) {
1111+
v.displayRgbData[dstOfs + x + 1] = this.rgbdata[srcOfs + x];
1112+
}
1113+
}
1114+
v.ctx.putImageData(v.displayImageData, 0, 0);
1115+
} else {
1116+
v.ctx.putImageData(this.imagedata, 0, 0);
1117+
}
10491118
}
10501119
}
10511120
}
@@ -1059,9 +1128,11 @@ class PixEditor extends Viewer {
10591128
palbtns: JQuery[];
10601129
offscreen: Map<string, number> = new Map();
10611130

1062-
getPositionFromEvent(e) {
1063-
var x = Math.floor(e.offsetX * this.width / $(this.canvas).width());
1131+
getPositionFromEvent(e): { x: number, y: number } {
1132+
var dw = this.displayWidth;
1133+
var x = Math.floor(e.offsetX * dw / $(this.canvas).width());
10641134
var y = Math.floor(e.offsetY * this.height / $(this.canvas).height());
1135+
if (this.gapX != null && x > this.gapX) x--;
10651136
return { x: x, y: y };
10661137
}
10671138

@@ -1084,8 +1155,20 @@ class PixEditor extends Viewer {
10841155
var dragging = false;
10851156

10861157
var pxls = $(this.canvas);
1158+
var artDragIdx = -1;
10871159
pxls.mousedown((e) => {
10881160
var pos = this.getPositionFromEvent(e);
1161+
if (this.isArtPixel(pos.x)) {
1162+
artDragIdx = this.toggleArtPixel(pos.x, pos.y);
1163+
dragging = true;
1164+
$(document).mouseup((e) => {
1165+
$(document).off('mouseup');
1166+
dragging = false;
1167+
artDragIdx = -1;
1168+
this.commit();
1169+
});
1170+
return;
1171+
}
10891172
dragcol = this.getPixel(pos.x, pos.y) == this.currgba ? this.palette[0] : this.currgba;
10901173
this.setPixel(pos.x, pos.y, this.currgba);
10911174
dragging = true;
@@ -1099,7 +1182,10 @@ class PixEditor extends Viewer {
10991182
})
11001183
.mousemove((e) => {
11011184
var pos = this.getPositionFromEvent(e);
1102-
if (dragging) {
1185+
if (!dragging) return;
1186+
if (artDragIdx >= 0 && this.isArtPixel(pos.x)) {
1187+
this.setArtPixel(pos.x, pos.y, artDragIdx);
1188+
} else if (artDragIdx < 0 && !this.isArtPixel(pos.x)) {
11031189
this.setPixel(pos.x, pos.y, dragcol);
11041190
}
11051191
});
@@ -1111,6 +1197,31 @@ class PixEditor extends Viewer {
11111197
this.setPaletteColor(1);
11121198
}
11131199

1200+
isArtPixel(x: number): boolean {
1201+
if (!this.artInfo) return false;
1202+
return (x % this.artInfo.bitsperword) == this.artInfo.artBit;
1203+
}
1204+
1205+
toggleArtPixel(x: number, y: number): number {
1206+
var info = this.artInfo;
1207+
if (!info) return -1;
1208+
var ofs = y * this.width + x;
1209+
var newIdx = info.indexedImage[ofs] ? 0 : 1;
1210+
this.setArtPixel(x, y, newIdx);
1211+
return newIdx;
1212+
}
1213+
1214+
setArtPixel(x: number, y: number, idx: number): void {
1215+
var info = this.artInfo;
1216+
if (!info) return;
1217+
if (x < 0 || x >= this.width || y < 0 || y >= this.height) return;
1218+
var ofs = y * this.width + x;
1219+
if (info.indexedImage[ofs] == idx) return;
1220+
info.indexedImage[ofs] = idx;
1221+
this.rgbdata[ofs] = this.palette[idx];
1222+
this.updateImage();
1223+
}
1224+
11141225
getPixel(x: number, y: number): number {
11151226
x = Math.round(x);
11161227
y = Math.round(y);
@@ -1175,16 +1286,47 @@ class PixEditor extends Viewer {
11751286
}
11761287
}
11771288

1289+
copyNonArtPixels(dst: Uint32Array, dstWidth: number, src: Uint32Array, srcWidth: number) {
1290+
for (var y = 0; y < this.height; y++) {
1291+
var si = 0, di = 0;
1292+
for (var x = 0; x < this.width; x++) {
1293+
var isArt = this.isArtPixel(x);
1294+
if (!isArt)
1295+
dst[y * dstWidth + di] = src[y * srcWidth + si];
1296+
if (!isArt || srcWidth === this.width) si++;
1297+
if (!isArt || dstWidth === this.width) di++;
1298+
}
1299+
}
1300+
}
1301+
11781302
remapPixels(mapfn: (x: number, y: number) => number) {
1303+
// Strip art bit columns so transforms ignore them.
1304+
var savedRgb = this.rgbdata;
1305+
var savedWidth = this.width;
1306+
if (this.artInfo) {
1307+
var bpw = this.artInfo.bitsperword;
1308+
var strippedWidth = this.width - Math.floor(this.width / bpw);
1309+
var stripped = new Uint32Array(strippedWidth * this.height);
1310+
this.copyNonArtPixels(stripped, strippedWidth, this.rgbdata, this.width);
1311+
this.rgbdata = stripped;
1312+
this.width = strippedWidth;
1313+
}
1314+
// Apply the transform.
11791315
var i = 0;
11801316
var pixels = new Uint32Array(this.rgbdata.length);
11811317
for (var y = 0; y < this.height; y++) {
11821318
for (var x = 0; x < this.width; x++) {
1183-
pixels[i] = mapfn(x, y);
1184-
i++;
1319+
pixels[i++] = mapfn(x, y);
11851320
}
11861321
}
1187-
this.rgbdata.set(pixels);
1322+
// Restore art bit columns from the original data.
1323+
this.rgbdata = savedRgb;
1324+
this.width = savedWidth;
1325+
if (this.artInfo) {
1326+
this.copyNonArtPixels(this.rgbdata, this.width, pixels, strippedWidth);
1327+
} else {
1328+
this.rgbdata.set(pixels);
1329+
}
11881330
this.commit();
11891331
}
11901332

src/ide/ui.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1602,6 +1602,7 @@ function openAssetEditorHelp() {
16021602
${row('wpimg', 'sl&times;h', 'Words per image')}
16031603
${row('aspect', '1', 'Pixel aspect ratio for display')}
16041604
${row('xform', '&mdash;', 'CSS transform on canvas')}
1605+
${row('art', 'false', 'Artifact color mode: true = Apple II HGR (bit 7 toggles artifact color)')}
16051606
<tr><th colspan="3">Palette Format</th></tr>
16061607
<tr><td><b>Field</b></td><td><b>Default</b></td><td><b>Description</b></td></tr>
16071608
${row('pal', '&mdash;', 'Palette: number (e.g. 332 = 3R,3G,2B) or name (nes, vcs, c64, ap2lores, astrocade)')}
@@ -1611,6 +1612,7 @@ function openAssetEditorHelp() {
16111612
<tr><td colspan="2"><code>/*{w:8,h:8,bpp:1,brev:1}*/</code></td><td>8x8 1bpp, MSB first (NES-style)</td></tr>
16121613
<tr><td colspan="2"><code>;;{w:8,h:5,count:4,il:1};;</code></td><td>4 interleaved 8x5 chars (stored as 32x5 block)</td></tr>
16131614
<tr><td colspan="2"><code>;;{w:7,h:8};;</code></td><td>7x8 1bpp, LSB first (Apple II HGR)</td></tr>
1615+
<tr><td colspan="2"><code>;;{w:8,h:8,art:true};;</code></td><td>8x8 with artifact color (Apple II HGR, bit 7 toggle)</td></tr>
16141616
<tr><td colspan="2"><code>/*{w:16,h:16,bpp:4,np:1}*/</code></td><td>16x16 4bpp</td></tr>
16151617
<tr><td colspan="2"><code>/*{pal:332,n:16}*/</code></td><td>16-entry RGB332 palette</pre></td></tr>
16161618
</table>`,

0 commit comments

Comments
 (0)