started on asset editor

This commit is contained in:
Steven Hugg 2019-03-18 14:39:02 -04:00
parent f190bf2d58
commit 22c2fb3c2f
9 changed files with 327 additions and 12 deletions

View File

@ -449,3 +449,26 @@ span.profiler-local {
span.profiler-cident {
color: #99ffff;
}
div.asset_file {
border: 2px solid black;
border-radius:8px;
color: #ddd;
background-color:#666;
margin:0.5em;
padding:1em;
}
div.asset_file_header {
font-weight: bold;
color: white;
}
div.asset_grid {
line-height:0;
margin: 1em;
}
div.asset_grid span {
white-space: nowrap;
}
div.asset_grid canvas {
padding: 0px;
border: 1px solid black;
}

View File

@ -150,6 +150,10 @@ We'll delve into these circuits as they morph from Pong into programmable person
At the end of this adventure, you should be well-equipped to begin exploring the world of FPGAs, and maybe even design your own game console. You'll use the 8bitworkshop.com IDE to write Verilog programs that represent digital circuits, and see your code run instantly in the browser.
</p>
</div>
<div class="col-md-2">
</div>
<div class="col-md-4">
</div>
</div>
</div>
@ -236,14 +240,16 @@ At the end of this adventure, you should be well-equipped to begin exploring the
You can even write C or 6502 assembler code for Woz's creation, the
<a href="redir.html?platform=apple2">Apple ][+</a>.
Thrill to the unusual frame buffer layout and one-bit speaker output!
You can even export to cassette tape!
<img class="img-responsive" src="//upload.wikimedia.org/wikipedia/commons/thumb/6/68/Apple_II_Plus.jpg/512px-Apple_II_Plus.jpg">
</div>
<div class="col-md-4">
</div>
</div>
</div>
<a href="https://www.patreon.com/bePatron?u=9081870" data-patreon-widget-type="become-patron-button">Become a Patron!</a><script async src="https://c6.patreon.com/becomePatronButton.bundle.js"></script>
<hr>
<footer>

View File

@ -93,6 +93,7 @@ TODO:
- profiler restarts when paused
- it's pretty easy to add a new file named like a library file (bcd.c)
- put globals into view/controller objects
- open asset editor twice first time?
WEB WORKER FORMAT
@ -216,4 +217,5 @@ Assets come from:
- chr files
- bitmap data in files
- structured data (palette, sprites, metasprites, levels, etc)
- think about new comment format, platform-specific types

View File

@ -387,6 +387,7 @@ function require(modname) {
<script src="gen/views.js"></script>
<script src="gen/recorder.js"></script>
<script src="gen/waveform.js"></script>
<script src="gen/pixed/pixeleditor.js"></script>
<script src="gen/ui.js"></script>
<!-- <script src="src/audio/votrax.js"></script> -->
<!-- <script src="local/lzg.js"></script> -->

View File

@ -1,6 +1,7 @@
"use strict";
import { hex } from "../util";
import { current_project } from "../ui";
type PixelEditorImageFormat = {
w:number
@ -14,7 +15,7 @@ type PixelEditorImageFormat = {
remap?:number[]
brev?:boolean
destfmt?:PixelEditorImageFormat
xform?
xform?:string
};
type PixelEditorPaletteFormat = {
@ -323,7 +324,7 @@ function remapBits(x:number, arr:number[]) : number {
return y;
}
function convertWordsToImages(words:number[], fmt:PixelEditorImageFormat) : Uint8Array[] {
function convertWordsToImages(words:number[] | Uint8Array, fmt:PixelEditorImageFormat) : Uint8Array[] {
var width = fmt.w;
var height = fmt.h;
var count = fmt.count || 1;
@ -562,7 +563,7 @@ function pixelEditorKeypress(e) {
case 104:
currentPixelEditor.flipx();
break;
case 118:
currentPixelEditor.flipy();
break;
default:
@ -584,3 +585,148 @@ var PREDEF_PALETTES = {
,0xFFF8D878 ,0xFFD8F878 ,0xFFB8F8B8 ,0xFFB8F8D8 ,0xFF00FCFC ,0xFFF8D8F8 ,0xFF000000 ,0xFF000000
]
};
/////
export abstract class PixelNode {
left : PixelNode; // toward text editor
right : PixelNode; // toward pixel editor
input?
output?
abstract updateLeft(); // update coming from right
abstract updateRight(); // update coming from left
refreshLeft() {
var p : PixelNode = this;
while (p) {
p.updateLeft();
p = p.left;
}
}
refreshRight() {
var p : PixelNode = this;
while (p) {
p.updateRight();
p = p.right;
}
}
addRight(node : PixelNode) {
this.right = node;
node.left = this;
}
}
export class PixelFileDataNode extends PixelNode {
fileid : string;
output : Uint8Array;
constructor(fileid, data) {
super();
this.fileid = fileid;
this.output = data;
}
updateLeft() {
current_project.updateFile(this.fileid, this.output);
}
updateRight() {
}
}
export class PixelTextDataNode extends PixelNode {
fileid : string;
text : string;
start : number;
end : number;
output : Uint8Array;
constructor(fileid, text, start, end) {
super();
this.fileid = fileid;
this.text = text;
this.start = start;
this.end = end;
}
updateLeft() {
// TODO: reload editors?
current_project.updateFile(this.fileid, this.text);
}
updateRight() {
var datastr = this.text.substring(this.start, this.end);
var words = parseHexWords(datastr);
this.output = new Uint8Array(words); // TODO: 16/32?
}
}
export class PixelMapper extends PixelNode {
fmt : PixelEditorImageFormat;
input : number[] | Uint8Array;
output : Uint8Array[];
updateLeft() {
//TODO
this.input = convertImagesToWords(this.output, this.fmt);
}
updateRight() {
// convert each word array to images
this.input = this.left.output;
this.output = convertWordsToImages(this.input, this.fmt);
}
}
export class PixelPalettizer extends PixelNode {
input : Uint8Array[];
output : Uint32Array[];
palette : Uint32Array;
updateLeft() {
//TODO
}
updateRight() {
var mask = this.palette.length - 1; // must be power of 2
this.input = this.left.output;
// for each image, map bytes to RGB colors
this.output = this.input.map( (im:Uint8Array) => {
var out = new Uint32Array(im.length);
for (var i=0; i<im.length; i++) {
out[i] = this.palette[im[i] & mask];
}
return out;
});
}
}
export class PixelViewer {
parentdiv : HTMLElement;
width : number;
height : number;
canvas : HTMLCanvasElement;
ctx : CanvasRenderingContext2D;
pixdata : ImageData;
recreate() {
this.canvas = this.newCanvas();
this.ctx = this.canvas.getContext('2d');
this.pixdata = this.ctx.createImageData(this.width, this.height);
}
newCanvas() : HTMLCanvasElement {
var c = document.createElement('canvas');
c.width = this.width;
c.height = this.height;
//if (fmt.xform) c.style.transform = fmt.xform;
c.classList.add("pixels");
c.classList.add("pixelated");
return c;
}
updateImage(imdata : Uint32Array) {
new Uint32Array(this.pixdata.data.buffer).set(imdata);
this.ctx.putImageData(this.pixdata, 0, 0);
}
}

View File

@ -225,6 +225,9 @@ function refreshWindowList() {
return new Views.ProfileView();
});
}
addWindowItem('#asseteditor', 'Asset Editor', function() {
return new Views.AssetEditorView();
});
}
// can pass integer or string id
@ -554,9 +557,10 @@ function _downloadSourceFile(e) {
function _downloadProjectZipFile(e) {
loadScript('lib/jszip.min.js', () => {
var zip = new JSZip();
current_project.iterateFiles(function(id, text) {
if (text)
zip.file(getFilenameForPath(id), text);
current_project.iterateFiles( (id, data) => {
if (data) {
zip.file(getFilenameForPath(id), data);
}
});
zip.generateAsync({type:"blob"}).then( (content) => {
saveAs(content, getCurrentMainFilename() + ".zip");

View File

@ -420,3 +420,6 @@ export function clamp(minv:number, maxv:number, v:number) {
return (v < minv) ? minv : (v > maxv) ? maxv : v;
}
export function safeident(s : string) : string {
return s.replace(/\W+/g, "_");
}

View File

@ -3,11 +3,12 @@
import $ = require("jquery");
//import CodeMirror = require("codemirror");
import { CodeProject } from "./project";
import { SourceFile, WorkerError, Segment } from "./workertypes";
import { SourceFile, WorkerError, Segment, FileData } from "./workertypes";
import { Platform, EmuState, ProfilerOutput, lookupSymbol } from "./baseplatform";
import { hex, lpad, rpad } from "./util";
import { hex, lpad, rpad, safeident } from "./util";
import { CodeAnalyzer } from "./analysis";
import { platform, platform_id, compparams, current_project, lastDebugState, projectWindows } from "./ui";
import { PixelViewer, PixelMapper, PixelPalettizer, PixelFileDataNode, PixelTextDataNode, parseHexWords } from "./pixed/pixeleditor";
export interface ProjectView {
createDiv(parent:HTMLElement, text:string) : HTMLElement;
@ -819,7 +820,6 @@ export class MemoryMapView implements ProjectView {
createDiv(parent : HTMLElement) {
this.maindiv = $('<div class="vertical-scroll"/>');
$(parent).append(this.maindiv);
this.refresh();
return this.maindiv[0];
}
@ -949,3 +949,133 @@ export class ProfileView implements ProjectView {
platform.stopProfiling();
}
}
///
export class AssetEditorView implements ProjectView {
maindiv : JQuery;
createDiv(parent : HTMLElement) {
this.maindiv = $('<div class="vertical-scroll"/>');
$(parent).append(this.maindiv);
return this.maindiv[0];
}
scanFileTextForAssets(id : string, data : string) {
// scan file for assets
// /*{json}*/ or ;;{json};;
var result = [];
var re1 = /[/;][*;]([{].+[}])[*;][/;]/g;
var m;
while (m = re1.exec(data)) {
var start = m.index + m[0].length;
var end;
// TODO: verilog end
if (m[0].startsWith(';;')) {
end = data.indexOf(';;', start); // asm
} else {
end = data.indexOf(';', start); // C
}
console.log(id, start, end, m[1]);
if (end > start) {
try {
var jsontxt = m[1].replace(/([A-Za-z]+):/g, '"$1":'); // fix lenient JSON
var json = JSON.parse(jsontxt);
// TODO: name?
result.push({fileid:id,fmt:json,start:start,end:end});
} catch (e) {
console.log(e);
}
}
}
return result;
}
addPixelEditor(filediv:JQuery, firstnode:PixelFileDataNode|PixelTextDataNode, fmt?) {
var adual = $('<div class="asset_dual"/>');
// create tile set
var aedit = $('<div class="asset_grid"/>');
adual.append(aedit);
filediv.append(adual);
// data -> pixels
var mapper = new PixelMapper();
fmt = fmt || {w:8,h:8,bpp:1,count:256/*(data.length>>4)*/,brev:true,np:2,pofs:8,remap:[0,1,2,4,5,6,7,8,9,10,11,12]}; // TODO
fmt.xform = 'scale(2)';
mapper.fmt = fmt;
var imgsperline = fmt.w == 8 ? 16 : 8; // TODO
// TODO: rotate node?
firstnode.addRight(mapper);
// pixels -> RGBA
var palizer = new PixelPalettizer();
if (fmt.bpp*(fmt.np|1) == 1)
palizer.palette = new Uint32Array([0xff000000, 0xffffffff]);
else
palizer.palette = new Uint32Array([0xff000000, 0xffff00ff, 0xffffff00, 0xffffffff]); // TODO
mapper.addRight(palizer);
// refresh
firstnode.refreshRight();
// add view objects (TODO)
// TODO: they need to update when refreshed from right
var i = 0;
var span = null;
for (var imdata of palizer.output) {
var viewer = new PixelViewer();
viewer.width = mapper.fmt.w | 0;
viewer.height = mapper.fmt.h | 0;
viewer.recreate();
viewer.canvas.style.width = (viewer.width*2)+'px'; // TODO
viewer.updateImage(imdata); // TODO
$(viewer.canvas).click((e) => {
console.log(e); // TODO
});
if (!span) {
span = $('<span/>');
aedit.append(span);
}
span.append(viewer.canvas);
if (++i == imgsperline) {
aedit.append($("<br/>"));
span = null;
i = 0;
}
}
}
refreshAssetsInFile(fileid : string, data : FileData) {
let filediv = $('#'+this.getFileDivId(fileid)).empty();
// TODO
// TODO: check if open
if (fileid.endsWith('.chr') && data instanceof Uint8Array) {
let node = new PixelFileDataNode(fileid, data);
this.addPixelEditor(filediv, node);
} else if (typeof data === 'string') {
let textfrags = this.scanFileTextForAssets(fileid, data);
for (let frag of textfrags) {
// is this a bitmap?
if (frag.fmt && frag.fmt.w > 0 && frag.fmt.h > 0) {
let node = new PixelTextDataNode(fileid, data, frag.start, frag.end);
this.addPixelEditor(filediv, node, frag.fmt);
} else {
// TODO: other kinds of resources?
}
}
}
}
getFileDivId(id : string) {
return '__asset__' + safeident(id);
}
refresh() {
this.maindiv.empty();
current_project.iterateFiles((id, data) => {
var divid = this.getFileDivId(id);
var header = $('<div class="asset_file_header"/>').text(id);
var body = $('<div/>').attr('id',divid);
var filediv = $('<div class="asset_file"/>').append(header, body).appendTo(this.maindiv);
this.refreshAssetsInFile(id, data);
});
}
}

View File

@ -177,8 +177,8 @@ var PLATFORM_PARAMS = {
extra_segments:[
//{name:'Work RAM',start:0x0,size:0x800,type:'ram'},
{name:'OAM Buffer',start:0x200,size:0x100,type:'ram'},
{name:'PPU Registers',start:0x2000,size:0x8,type:'io'},
{name:'APU Registers',start:0x4000,size:0x20,type:'io'},
{name:'PPU Registers',start:0x2000,last:0x2008,size:0x2000,type:'io'},
{name:'APU Registers',start:0x4000,last:0x4020,size:0x2000,type:'io'},
{name:'Cartridge RAM',start:0x6000,size:0x2000,type:'ram'},
],
},