started on asset editor
This commit is contained in:
parent
f190bf2d58
commit
22c2fb3c2f
23
css/ui.css
23
css/ui.css
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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> -->
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
10
src/ui.ts
10
src/ui.ts
|
@ -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");
|
||||
|
|
|
@ -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, "_");
|
||||
}
|
||||
|
|
136
src/views.ts
136
src/views.ts
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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'},
|
||||
],
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue