mirror of
https://github.com/MiSTer-devel/GnW_MiSTer.git
synced 2026-05-24 03:03:28 +00:00
532 lines
19 KiB
HTML
532 lines
19 KiB
HTML
<html>
|
|
|
|
<script
|
|
src="https://code.jquery.com/jquery-3.5.0.min.js"
|
|
integrity="sha256-xNzN2a4ltkB44Mc/Jz3pT4iU1cmeR0FkXs4pru/JxaQ="
|
|
crossorigin="anonymous"></script>
|
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/js/all.min.js" crossorigin="anonymous"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/rgbquant@1.1.2/src/rgbquant.min.js"></script>
|
|
<body>
|
|
|
|
<div class="container">
|
|
|
|
<div class="row">
|
|
<div class="col-sm mt-3">
|
|
<canvas id="cvs" width="720" height="480"></canvas>
|
|
<div id="svg" style="display: none"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-sm mt-3">
|
|
<label>mask expansion <i class="fa-solid fa-expand"></i></label>
|
|
<div class="badge">
|
|
<input id="maskexp" type="text" value="10" size="2">
|
|
</div>
|
|
<input type="checkbox" onchange="javascript:dither=!dither;refresh()" id="VGA">
|
|
<label for="dither">Dither</label>
|
|
<div class="badge">
|
|
x:<input type="text" onchange="trans(sel, 0, this.value)" id="xc" size="2" value="0">
|
|
y:<input type="text" onchange="trans(sel, 1, this.value)" id="yc" size="2" value="0">
|
|
w:<input type="text" onchange="trans(sel, 2, this.value)" id="xs" size="2" value="0">
|
|
h:<input type="text" onchange="trans(sel, 3, this.value)" id="ys" size="2" value="0">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<div class="badge badge-pill badge-warning">
|
|
<i class="fa-solid fa-layer-group"></i>
|
|
<select onchange="selectLayer(this.value)">
|
|
<option value="0">BG1</option>
|
|
<option value="1">BG2</option>
|
|
<option value="2">BG3</option>
|
|
<option value="3">SVG1</option>
|
|
<option value="4">SVG2</option>
|
|
</select>
|
|
<label id="upbtn" role="button" class="m-0" for="upload"><i class="fa-solid fa-upload"></i></label>
|
|
<input id="upload" type="file" onchange="upload()" style="display:none">
|
|
</div>
|
|
<div class="badge badge-pill badge-warning">
|
|
<i class="fa-solid fa-arrows-alt"></i>
|
|
<button onmousedown="startTransform(sel, 0, -1)" onmouseup="stopTransform()"><i class="fa-solid fa-long-arrow-alt-left"></i></button>
|
|
<button onmousedown="startTransform(sel, 0, 1)" onmouseup="stopTransform()"><i class="fa-solid fa-long-arrow-alt-right"></i></button>
|
|
<button onmousedown="startTransform(sel, 1, -1)" onmouseup="stopTransform()"><i class="fa-solid fa-long-arrow-alt-up"></i></button>
|
|
<button onmousedown="startTransform(sel, 1, 1)" onmouseup="stopTransform()"><i class="fa-solid fa-long-arrow-alt-down"></i></button>
|
|
</div>
|
|
<div class="badge badge-pill badge-warning">
|
|
<i class="fa-solid fa-arrows-alt-h"></i>
|
|
<button onmousedown="startTransform(sel, 2, 1)" onmouseup="stopTransform()"><i class="fa-solid fa-plus"></i></button>
|
|
<button onmousedown="startTransform(sel, 2, -1)" onmouseup="stopTransform()"><i class="fa-solid fa-minus"></i></button>
|
|
</div>
|
|
<div class="badge badge-pill badge-warning">
|
|
<i class="fa-solid fa-arrows-alt-v"></i>
|
|
<button onmousedown="startTransform(sel, 3, 1)" onmouseup="stopTransform()"><i class="fa-solid fa-plus"></i></button>
|
|
<button onmousedown="startTransform(sel, 3, -1)" onmouseup="stopTransform()"><i class="fa-solid fa-minus"></i></button>
|
|
</div>
|
|
<div class="badge badge-pill badge-warning" style="padding: 6px">
|
|
<label id="LROM" role="button" class="m-0" for="ROM">
|
|
<i class="fa-solid fa-upload"></i>
|
|
<span class="pl-1">no rom binary</span>
|
|
</label>
|
|
<input id="ROM" type="file" onchange="importROM()" style="display:none">
|
|
</div>
|
|
<div class="badge badge-pill badge-warning" style="padding: 6px" role="button" onclick="exportBIN()">
|
|
<i class="fa-solid fa-save"></i> export
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-sm">
|
|
<label>MCU</label>
|
|
<select id="mcuid">
|
|
<option value="0" disabled>SM-5A</option>
|
|
<option value="1" selected>SM-510</option>
|
|
<option value="2" disabled>SM-511</option>
|
|
<option value="3" disabled>SM-530</option>
|
|
<option value="4" disabled>SM-590</option>
|
|
</select>
|
|
<label>Time location $</label>
|
|
<input id="timeloc" type="text" value="14" size="2">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row" id="keys">
|
|
<div class="col">
|
|
|
|
<div class="row">
|
|
<div class="col-sm"></div>
|
|
<div class="col-sm">K1</div>
|
|
<div class="col-sm">K2</div>
|
|
<div class="col-sm">K3</div>
|
|
<div class="col-sm">K4</div>
|
|
</div>
|
|
<div class="row bg-light">
|
|
<div class="col-sm">S1</div>
|
|
<div class="col-sm"><select id="k0"></select></div>
|
|
<div class="col-sm"><select id="k1"></select></div>
|
|
<div class="col-sm"><select id="k2"></select></div>
|
|
<div class="col-sm"><select id="k3"></select></div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-sm">S2</div>
|
|
<div class="col-sm"><select id="k4"></select></div>
|
|
<div class="col-sm"><select id="k5"></select></div>
|
|
<div class="col-sm"><select id="k6"></select></div>
|
|
<div class="col-sm"><select id="k7"></select></div>
|
|
</div>
|
|
<div class="row bg-light">
|
|
<div class="col-sm">S3</div>
|
|
<div class="col-sm"><select id="k8"></select></div>
|
|
<div class="col-sm"><select id="k9"></select></div>
|
|
<div class="col-sm"><select id="k10"></select></div>
|
|
<div class="col-sm"><select id="k11"></select></div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-sm">S4</div>
|
|
<div class="col-sm"><select id="k12"></select></div>
|
|
<div class="col-sm"><select id="k13"></select></div>
|
|
<div class="col-sm"><select id="k14"></select></div>
|
|
<div class="col-sm"><select id="k15"></select></div>
|
|
</div>
|
|
<div class="row bg-light">
|
|
<div class="col-sm">S5</div>
|
|
<div class="col-sm"><select id="k16"></select></div>
|
|
<div class="col-sm"><select id="k17"></select></div>
|
|
<div class="col-sm"><select id="k18"></select></div>
|
|
<div class="col-sm"><select id="k19"></select></div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-sm">S6</div>
|
|
<div class="col-sm"><select id="k20"></select></div>
|
|
<div class="col-sm"><select id="k21"></select></div>
|
|
<div class="col-sm"><select id="k22"></select></div>
|
|
<div class="col-sm"><select id="k23"></select></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container w-100" id="config">
|
|
<div class="row">
|
|
<div class="badge">
|
|
<button onclick="saveconf()"><i class="fa-solid fa-file-arrow-down" title="refresh config"></i></button>
|
|
<button onclick="loadconf()"><i class="fa-solid fa-file-arrow-up" title="import config"></i></button>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<textarea style="width: 100%; height: 80px; font-size: 12px"></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="clearfix"></div>
|
|
</div>
|
|
|
|
<br>
|
|
</body>
|
|
|
|
|
|
<script>
|
|
const width = 720
|
|
const height = 480
|
|
|
|
let DOMURL = window.URL || window.webkitURL || window
|
|
let canvas = document.getElementById('cvs')
|
|
let ctx = canvas.getContext('2d')
|
|
let ROM
|
|
let BGImg = []
|
|
let SVGImg = []
|
|
let AASVGImg = []
|
|
let tfs = [
|
|
[0, 0, width, height], [0, 0, width, height],
|
|
[0, 0, width, height], [0, 0, width, height],
|
|
[0, 0, width, height]
|
|
]
|
|
let sel = 0
|
|
let dither = false
|
|
|
|
let options = [
|
|
'right', 'left', 'down', 'up',
|
|
'jump', 'time', 'game A', 'game B',
|
|
'alarm', 'action 1', 'action 2', 'action 3'
|
|
]
|
|
|
|
$('#keys select').each((i, e) => {
|
|
for(k in options) {
|
|
var opt = $('<option>')
|
|
$(e).append(opt.val(k).text(options[k]))
|
|
}
|
|
$(e).append(opt.val(15).text('none'))
|
|
$(e).val(15) // none
|
|
})
|
|
|
|
refresh()
|
|
|
|
function selectLayer(v) {
|
|
sel = v
|
|
refresh()
|
|
}
|
|
|
|
function upload() {
|
|
switch (true) {
|
|
case sel < 3: importBG(); break;
|
|
case sel >= 3: importSVG(); break;
|
|
}
|
|
}
|
|
|
|
function importBG() {
|
|
let BG = document.getElementById('upload').files[0]
|
|
if (BG.type.match(/.+\/svg/)) {
|
|
alert('Please use a SVG channel')
|
|
document.getElementById('upload').value = ''
|
|
return;
|
|
}
|
|
if (BG) loadBGFile(BG)
|
|
}
|
|
|
|
function importSVG() {
|
|
let SVG = document.getElementById('upload').files[0]
|
|
if (!SVG.type.match(/.+\/svg/)) {
|
|
alert('Please use a BG channel')
|
|
document.getElementById('upload').value = ''
|
|
return;
|
|
}
|
|
if (SVG) loadSVGFile(SVG)
|
|
}
|
|
|
|
function importROM() {
|
|
let RF = document.getElementById('ROM').files[0]
|
|
if (RF) loadROMFile(RF)
|
|
}
|
|
|
|
function loadBGFile(BG) {
|
|
let reader = new FileReader()
|
|
reader.readAsDataURL(BG)
|
|
reader.onload = BGLoaded
|
|
}
|
|
|
|
function loadSVGFile(SVG) {
|
|
let reader = new FileReader()
|
|
reader.readAsText(SVG, "UTF-8")
|
|
reader.onload = SVGLoaded
|
|
}
|
|
|
|
function loadROMFile(RF) {
|
|
let reader = new FileReader()
|
|
reader.readAsArrayBuffer(RF)
|
|
reader.onload = evt => {
|
|
ROM = evt.target.result
|
|
document.querySelector('#LROM span').innerHTML = 'rom: ' + RF.name
|
|
}
|
|
}
|
|
|
|
function BGLoaded(evt) {
|
|
if (!BGImg[sel]) BGImg[sel] = new Image()
|
|
BGImg[sel].src = evt.target.result
|
|
BGImg[sel].onload = function() {
|
|
refresh()
|
|
}
|
|
}
|
|
|
|
function drawBG() {
|
|
BGImg.forEach((img, i) => {
|
|
ctx.drawImage(img, tfs[i][0], tfs[i][1], tfs[i][2], tfs[i][3])
|
|
})
|
|
return dither ? doRGBQuant() : false;
|
|
}
|
|
|
|
function drawSVG() {
|
|
ctx.imageSmoothingEnabled = false;
|
|
SVGImg.forEach((img, i) => {
|
|
ctx.drawImage(img, tfs[i][0], tfs[i][1], tfs[i][2], tfs[i][3])
|
|
})
|
|
}
|
|
|
|
function drawAASVG(pal) {
|
|
ctx.imageSmoothingEnabled = false;
|
|
AASVGImg.forEach((img, i) => {
|
|
ctx.drawImage(img, tfs[i][0], tfs[i][1], tfs[i][2], tfs[i][3])
|
|
})
|
|
return doRGBQuant(pal)
|
|
}
|
|
|
|
function drawHelpers() {
|
|
ctx.strokeStyle = 'red'
|
|
ctx.beginPath()
|
|
ctx.moveTo(width/2, 0)
|
|
ctx.lineTo(width/2, height)
|
|
ctx.stroke()
|
|
ctx.moveTo(0, height/2)
|
|
ctx.lineTo(width, height/2)
|
|
ctx.stroke()
|
|
}
|
|
|
|
function refresh(helpers) {
|
|
var h = helpers || 1
|
|
ctx.fillStyle = "#000"
|
|
ctx.fillRect(0, 0, width, height)
|
|
drawBG()
|
|
drawSVG()
|
|
if (h) drawHelpers()
|
|
$('#xc').val(tfs[sel][0])
|
|
$('#yc').val(tfs[sel][1])
|
|
$('#xs').val(tfs[sel][2])
|
|
$('#ys').val(tfs[sel][3])
|
|
}
|
|
|
|
function doRGBQuant(pal) {
|
|
var opts = pal ? { palette: pal } : {};
|
|
var q = new RgbQuant(opts)
|
|
q.sample(canvas)
|
|
var pal = q.palette(true, false)
|
|
var out = q.reduce(canvas, 2)
|
|
let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
|
|
let data = imageData.data
|
|
let j = 0
|
|
for (var i = 0; i < data.length; i += 4) {
|
|
data[i ] = pal[out[j]][0]
|
|
data[i+1] = pal[out[j]][1]
|
|
data[i+2] = pal[out[j]][2]
|
|
j++
|
|
}
|
|
ctx.putImageData(imageData, 0, 0);
|
|
return { pal: pal, img: out }
|
|
}
|
|
|
|
function doRGB332() {
|
|
let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
|
|
let data = imageData.data
|
|
for (var i = 0; i < data.length; i += 4) {
|
|
data[i ] = data[i ] & 0b11100000
|
|
data[i+1] = data[i+1] & 0b11100000
|
|
data[i+2] = data[i+2] & 0b11000000
|
|
}
|
|
ctx.putImageData(imageData, 0, 0);
|
|
}
|
|
|
|
function trans(i, j, n) {
|
|
tfs[i][j] = n
|
|
refresh()
|
|
}
|
|
|
|
let transform
|
|
function startTransform(i, j, n) {
|
|
transform = setInterval(_ => {
|
|
var nv = tfs[i][j] + n
|
|
trans(i, j, nv)
|
|
}, 50)
|
|
}
|
|
|
|
function stopTransform() {
|
|
clearInterval(transform)
|
|
}
|
|
|
|
function SVGLoaded(evt) {
|
|
$('#svg').html(evt.target.result)
|
|
$('#svg svg').attr('width', width).removeAttr('height')
|
|
|
|
// should be the big white rectangle, pls check
|
|
// can we use it for automatic segment positioning?
|
|
$('svg #layer2').hide()
|
|
|
|
let mask_expansion = $("#maskexp").val()
|
|
|
|
let data = $('#svg').html()
|
|
let svg = new Blob([data], {type: 'image/svg+xml'})
|
|
let url = DOMURL.createObjectURL(svg)
|
|
if (!AASVGImg[sel]) AASVGImg[sel] = new Image()
|
|
AASVGImg[sel].src = url
|
|
|
|
let elements = $('#layer1 path,#layer1 g')
|
|
elements.each((k, e) => {
|
|
let title = $(e).find('title')
|
|
if (title.length) {
|
|
let id = title.text().split('.')
|
|
let col = ((id[0] & 0b11) << 6) | ((id[1] & 0b1111) << 2) | (id[2] & 0b11)
|
|
let style = `fill-opacity:1;fill:#00${('00'+(col).toString(16)).substr(-2)}00;`
|
|
style += `stroke:#00${('00'+(col).toString(16)).substr(-2)}00;`
|
|
style += `stroke-width: ${mask_expansion}px`
|
|
console.log(`found ${title.text()}, converted to ${col.toString(16)}`)
|
|
if ($(e).prop('tagName') == 'g') {
|
|
$(e).find('path').each((i, e) => $(e).attr('style', style))
|
|
$(e).find('polygon').each((i, e) => $(e).attr('style', style))
|
|
}
|
|
else {
|
|
$(e).attr('style', style)
|
|
}
|
|
}
|
|
})
|
|
|
|
$('svg path').each((k, e) => {
|
|
$(e).attr('shape-rendering', 'crispEdges')
|
|
})
|
|
|
|
data = $('#svg').html()
|
|
svg = new Blob([data], {type: 'image/svg+xml'})
|
|
url = DOMURL.createObjectURL(svg)
|
|
if (!SVGImg[sel]) SVGImg[sel] = new Image()
|
|
SVGImg[sel].src = url
|
|
SVGImg[sel].onload = function() {
|
|
refresh()
|
|
}
|
|
|
|
}
|
|
|
|
function saveconf() {
|
|
let keys = []
|
|
for (var i = 0; i < 23; i+=2) {
|
|
keys.push($('#k' + i).val())
|
|
keys.push($('#k' + (i+1)).val())
|
|
}
|
|
let conf = {
|
|
tfs: tfs,
|
|
mcuid: $('#mcuid').val(),
|
|
timeloc: $('#timeloc').val(),
|
|
keys: keys
|
|
}
|
|
let confstr = JSON.stringify(conf)
|
|
$('#config textarea').val(window.btoa(confstr))
|
|
}
|
|
|
|
function loadconf() {
|
|
let conf = JSON.parse(window.atob($('#config textarea').val()))
|
|
for (var i = 0; i < 23; i+=2) {
|
|
$('#k' + i).val(conf.keys[i])
|
|
$('#k' + (i+1)).val(conf.keys[i+1])
|
|
}
|
|
$('#mcuid').val(conf.mcuid)
|
|
$('#timeloc').val(conf.timeloc)
|
|
tfs = conf.tfs
|
|
refresh()
|
|
}
|
|
|
|
function exportBIN() {
|
|
if (!ROM) return
|
|
|
|
// mcu id (1)
|
|
// config size (1)
|
|
// joystick config (12)
|
|
// image size (32)
|
|
// mask+img data (XxYx2)
|
|
// palette (256x3)
|
|
// rom binary
|
|
|
|
let mcu_size = 1
|
|
let config_size = 1
|
|
let config = 13
|
|
let img_size = width*height*3-1
|
|
let buffer_size = mcu_size + config_size + config + 32 + img_size + 256*3-1 + ROM.byteLength;
|
|
let buffer = new Uint8Array(buffer_size)
|
|
|
|
let ptr = 0
|
|
|
|
// 1 byte for mcu
|
|
buffer[ptr++] = $('#mcuid').val()
|
|
|
|
// config size
|
|
buffer[ptr++] = config
|
|
|
|
// first config byte is time location
|
|
buffer[ptr++] = $("#timeloc").val()
|
|
|
|
// 12 config bytes for joystick
|
|
for (var i = 0; i < 23; i+=2) {
|
|
var id1 = '#k' + i
|
|
var id2 = '#k' + (i+1)
|
|
buffer[ptr++] = parseInt($(id1).val()) << 4 | parseInt($(id2).val())
|
|
}
|
|
|
|
// img size
|
|
for (var i = 24; i >= 0; i -= 8) {
|
|
buffer[ptr++] = (img_size >> i) & 0xff
|
|
}
|
|
|
|
// mask
|
|
ctx.fillStyle = "#ffffff"
|
|
ctx.fillRect(0, 0, width, height)
|
|
drawSVG()
|
|
let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
|
|
let maskData = imageData.data
|
|
let mask = []
|
|
for (var i = 0; i < maskData.length; i += 4) {
|
|
mask.push(maskData[i+1]) // green channel
|
|
}
|
|
|
|
dither = true
|
|
$('#VGA').prop('checked', dither)
|
|
ctx.fillStyle = "#000"
|
|
ctx.fillRect(0, 0, width, height)
|
|
let qdat = drawBG()
|
|
|
|
let segs = drawAASVG(qdat.pal)
|
|
|
|
for (var i = 0; i < qdat.img.length; i++) {
|
|
buffer[ptr++] = mask[i]
|
|
buffer[ptr++] = segs.img[i]
|
|
buffer[ptr++] = qdat.img[i]
|
|
}
|
|
|
|
for (var i = 0; i < qdat.pal.length; i++) {
|
|
buffer[ptr++] = qdat.pal[i][0]
|
|
buffer[ptr++] = qdat.pal[i][1]
|
|
buffer[ptr++] = qdat.pal[i][2]
|
|
}
|
|
|
|
buffer.set(new Uint8Array(ROM), ptr)
|
|
|
|
let binary = new Blob([buffer], {type: 'application/octet-binary'})
|
|
let url = DOMURL.createObjectURL(binary)
|
|
var a = document.createElement('a')
|
|
document.body.appendChild(a)
|
|
a.style = 'display: none'
|
|
a.href = url
|
|
a.download = 'download.bin'
|
|
a.click()
|
|
window.URL.revokeObjectURL(url)
|
|
|
|
refresh()
|
|
}
|
|
|
|
</script>
|
|
|
|
</html> |