parent
9a79537910
commit
6d7bd89f0b
@ -1,397 +1,208 @@ |
||||
const { mk } = require('../utils') |
||||
|
||||
module.exports = function attachDebugger (screen, connection) { |
||||
const debugCanvas = mk('canvas') |
||||
const ctx = debugCanvas.getContext('2d') |
||||
|
||||
const debugCanvas = document.createElement('canvas') |
||||
debugCanvas.classList.add('debug-canvas') |
||||
const ctx = debugCanvas.getContext('2d') |
||||
|
||||
let mouseHoverCell = null |
||||
let updateToolbar |
||||
const toolbar = document.createElement('div') |
||||
toolbar.classList.add('debug-toolbar') |
||||
|
||||
let onMouseMove = e => { |
||||
mouseHoverCell = screen.layout.screenToGrid(e.offsetX, e.offsetY) |
||||
startDrawing() |
||||
updateToolbar() |
||||
let selectedCell = null |
||||
const onMouseMove = (e) => { |
||||
selectedCell = screen.layout.screenToGrid(e.offsetX, e.offsetY) |
||||
} |
||||
const onMouseOut = (e) => { |
||||
selectedCell = null |
||||
} |
||||
let onMouseOut = () => (mouseHoverCell = null) |
||||
|
||||
let addCanvas = function () { |
||||
if (!debugCanvas.parentNode) { |
||||
const updateCanvasSize = function () { |
||||
let { width, height, devicePixelRatio } = screen.layout.window |
||||
let cellSize = screen.layout.getCellSize() |
||||
let padding = Math.round(screen.layout._padding) |
||||
debugCanvas.width = (width * cellSize.width + 2 * padding) * devicePixelRatio |
||||
debugCanvas.height = (height * cellSize.height + 2 * padding) * devicePixelRatio |
||||
debugCanvas.style.width = `${width * cellSize.width + 2 * screen.layout._padding}px` |
||||
debugCanvas.style.height = `${height * cellSize.height + 2 * screen.layout._padding}px` |
||||
} |
||||
|
||||
let startDrawLoop |
||||
let screenAttached = false |
||||
let eventNode |
||||
const setScreenAttached = function (attached) { |
||||
if (attached && !debugCanvas.parentNode) { |
||||
screen.layout.canvas.parentNode.appendChild(debugCanvas) |
||||
screen.layout.canvas.addEventListener('mousemove', onMouseMove) |
||||
screen.layout.canvas.addEventListener('mouseout', onMouseOut) |
||||
} |
||||
} |
||||
let removeCanvas = function () { |
||||
if (debugCanvas.parentNode) { |
||||
eventNode = debugCanvas.parentNode |
||||
eventNode.addEventListener('mousemove', onMouseMove) |
||||
eventNode.addEventListener('mouseout', onMouseOut) |
||||
screen.layout.on('size-update', updateCanvasSize) |
||||
updateCanvasSize() |
||||
screenAttached = true |
||||
startDrawLoop() |
||||
} else if (!attached && debugCanvas.parentNode) { |
||||
debugCanvas.parentNode.removeChild(debugCanvas) |
||||
screen.layout.canvas.removeEventListener('mousemove', onMouseMove) |
||||
screen.layout.canvas.removeEventListener('mouseout', onMouseOut) |
||||
onMouseOut() |
||||
eventNode.removeEventListener('mousemove', onMouseMove) |
||||
eventNode.removeEventListener('mouseout', onMouseOut) |
||||
screen.layout.removeListener('size-update', updateCanvasSize) |
||||
screenAttached = false |
||||
} |
||||
} |
||||
let updateCanvasSize = function () { |
||||
let { width, height, devicePixelRatio } = screen.layout.window |
||||
let cellSize = screen.layout.getCellSize() |
||||
debugCanvas.width = width * cellSize.width * devicePixelRatio |
||||
debugCanvas.height = height * cellSize.height * devicePixelRatio |
||||
debugCanvas.style.width = `${width * cellSize.width}px` |
||||
debugCanvas.style.height = `${height * cellSize.height}px` |
||||
|
||||
const setToolbarAttached = function (attached) { |
||||
if (attached && !toolbar.parentNode) { |
||||
screen.layout.canvas.parentNode.appendChild(toolbar) |
||||
} else if (!attached && toolbar.parentNode) { |
||||
screen.layout.canvas.parentNode.removeChild(toolbar) |
||||
} |
||||
} |
||||
|
||||
let drawInfo = mk('div') |
||||
drawInfo.classList.add('draw-info') |
||||
screen.on('update-window:debug', enabled => { |
||||
setToolbarAttached(enabled) |
||||
}) |
||||
|
||||
let startTime, endTime, lastReason |
||||
let cells = new Map() |
||||
let clippedRects = [] |
||||
let updateFrames = [] |
||||
screen.layout.on('update-window:debug', enabled => { |
||||
setScreenAttached(enabled) |
||||
}) |
||||
|
||||
let startDrawing |
||||
let drawData = { |
||||
reason: '', |
||||
startTime: 0, |
||||
endTime: 0, |
||||
clipped: [], |
||||
frames: [], |
||||
cells: new Map() |
||||
} |
||||
|
||||
screen._debug = screen.layout.renderer._debug = { |
||||
drawStart (reason) { |
||||
lastReason = reason |
||||
startTime = Date.now() |
||||
clippedRects = [] |
||||
drawData.reason = reason |
||||
drawData.startTime = window.performance.now() |
||||
}, |
||||
drawEnd () { |
||||
endTime = Date.now() |
||||
drawInfo.textContent = `Draw: ${lastReason} (${(endTime - startTime)} ms), fancy gfx=${screen.layout.renderer.graphics}` |
||||
startDrawing() |
||||
drawData.endTime = window.performance.now() |
||||
}, |
||||
setCell (cell, flags) { |
||||
cells.set(cell, [flags, Date.now()]) |
||||
}, |
||||
clipRect (...args) { |
||||
clippedRects.push(args) |
||||
drawData.cells.set(cell, [flags, window.performance.now()]) |
||||
}, |
||||
pushFrame (frame) { |
||||
frame.push(Date.now()) |
||||
updateFrames.push(frame) |
||||
startDrawing() |
||||
drawData.frames.push([...frame, window.performance.now()]) |
||||
} |
||||
} |
||||
|
||||
let clipPattern |
||||
{ |
||||
let patternCanvas = document.createElement('canvas') |
||||
patternCanvas.width = patternCanvas.height = 12 |
||||
let pctx = patternCanvas.getContext('2d') |
||||
pctx.lineWidth = 1 |
||||
pctx.strokeStyle = '#00f' |
||||
pctx.beginPath() |
||||
pctx.moveTo(0, 0) |
||||
pctx.lineTo(0 - 4, 12) |
||||
pctx.moveTo(4, 0) |
||||
pctx.lineTo(4 - 4, 12) |
||||
pctx.moveTo(8, 0) |
||||
pctx.lineTo(8 - 4, 12) |
||||
pctx.moveTo(12, 0) |
||||
pctx.lineTo(12 - 4, 12) |
||||
pctx.moveTo(16, 0) |
||||
pctx.lineTo(16 - 4, 12) |
||||
pctx.stroke() |
||||
clipPattern = ctx.createPattern(patternCanvas, 'repeat') |
||||
} |
||||
|
||||
let isDrawing = false |
||||
let lastDrawTime = 0 |
||||
let t = 0 |
||||
|
||||
let drawLoop = function () { |
||||
if (isDrawing) window.requestAnimationFrame(drawLoop) |
||||
if (screenAttached) window.requestAnimationFrame(drawLoop) |
||||
else isDrawing = false |
||||
|
||||
let dt = (Date.now() - lastDrawTime) / 1000 |
||||
lastDrawTime = Date.now() |
||||
t += dt |
||||
let now = window.performance.now() |
||||
|
||||
let { devicePixelRatio, width, height } = screen.layout.window |
||||
let { width: cellWidth, height: cellHeight } = screen.layout.getCellSize() |
||||
let screenLength = width * height |
||||
let now = Date.now() |
||||
let { width, height, devicePixelRatio } = screen.layout.window |
||||
let padding = Math.round(screen.layout._padding) |
||||
let cellSize = screen.layout.getCellSize() |
||||
|
||||
ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0) |
||||
ctx.clearRect(0, 0, width * cellWidth, height * cellHeight) |
||||
ctx.clearRect(0, 0, width * cellSize.width + 2 * padding, height * cellSize.height + 2 * padding) |
||||
ctx.translate(padding, padding) |
||||
|
||||
ctx.lineWidth = 2 |
||||
ctx.lineJoin = 'round' |
||||
|
||||
let activeCells = 0 |
||||
for (let cell = 0; cell < screenLength; cell++) { |
||||
const cells = drawData.cells |
||||
for (let cell = 0; cell < width * height; cell++) { |
||||
// cell does not exist or has no flags set
|
||||
if (!cells.has(cell) || cells.get(cell)[0] === 0) continue |
||||
|
||||
let [flags, timestamp] = cells.get(cell) |
||||
const [flags, timestamp] = cells.get(cell) |
||||
let elapsedTime = (now - timestamp) / 1000 |
||||
|
||||
if (elapsedTime > 1) continue |
||||
if (elapsedTime > 1) { |
||||
cells.delete(cell) |
||||
continue |
||||
} |
||||
|
||||
activeCells++ |
||||
ctx.globalAlpha = 0.5 * Math.max(0, 1 - elapsedTime) |
||||
|
||||
let x = cell % width |
||||
let y = Math.floor(cell / width) |
||||
|
||||
if (flags & 1) { |
||||
// redrawn
|
||||
ctx.fillStyle = '#f0f' |
||||
} |
||||
if (flags & 2) { |
||||
// updated
|
||||
ctx.fillStyle = '#0f0' |
||||
} else if (flags & 1) { |
||||
// redrawn
|
||||
ctx.fillStyle = '#f0f' |
||||
} |
||||
|
||||
ctx.fillRect(x * cellWidth, y * cellHeight, cellWidth, cellHeight) |
||||
if (!(flags & 4)) { |
||||
// outside a clipped region
|
||||
ctx.fillStyle = '#0ff' |
||||
} |
||||
|
||||
ctx.fillRect(x * cellSize.width, y * cellSize.height, cellSize.width, cellSize.height) |
||||
|
||||
if (flags & 4) { |
||||
if (flags & 8) { |
||||
// wide cell
|
||||
ctx.lineWidth = 2 |
||||
ctx.strokeStyle = '#f00' |
||||
ctx.strokeRect(x * cellWidth, y * cellHeight, cellWidth, cellHeight) |
||||
} |
||||
} |
||||
|
||||
if (clippedRects.length) { |
||||
ctx.globalAlpha = 0.5 |
||||
ctx.beginPath() |
||||
|
||||
for (let rect of clippedRects) { |
||||
ctx.rect(...rect) |
||||
ctx.moveTo(x * cellSize.width, (y + 1) * cellSize.height) |
||||
ctx.lineTo((x + 1) * cellSize.width, (y + 1) * cellSize.height) |
||||
ctx.stroke() |
||||
} |
||||
|
||||
ctx.fillStyle = clipPattern |
||||
ctx.fill() |
||||
} |
||||
|
||||
let didDrawUpdateFrames = false |
||||
if (updateFrames.length) { |
||||
let framesToDelete = [] |
||||
for (let frame of updateFrames) { |
||||
let time = frame[4] |
||||
let elapsed = Date.now() - time |
||||
if (elapsed > 1000) framesToDelete.push(frame) |
||||
for (let frame of drawData.frames) { |
||||
let timestamp = frame[4] |
||||
let elapsedTime = (now - timestamp) / 1000 |
||||
if (elapsedTime > 1) framesToDelete.push(frame) |
||||
else { |
||||
didDrawUpdateFrames = true |
||||
ctx.globalAlpha = 1 - elapsed / 1000 |
||||
ctx.globalAlpha = 1 - elapsedTime |
||||
ctx.strokeStyle = '#ff0' |
||||
ctx.lineWidth = 2 |
||||
ctx.strokeRect(frame[0] * cellWidth, frame[1] * cellHeight, frame[2] * cellWidth, frame[3] * cellHeight) |
||||
ctx.strokeRect(frame[0] * cellSize.width, frame[1] * cellSize.height, |
||||
frame[2] * cellSize.width, frame[3] * cellSize.height) |
||||
} |
||||
} |
||||
for (let frame of framesToDelete) { |
||||
updateFrames.splice(updateFrames.indexOf(frame), 1) |
||||
} |
||||
drawData.frames.splice(drawData.frames.indexOf(frame), 1) |
||||
} |
||||
|
||||
if (mouseHoverCell) { |
||||
if (selectedCell !== null) { |
||||
let [x, y] = selectedCell |
||||
|
||||
ctx.save() |
||||
ctx.globalAlpha = 0.5 |
||||
ctx.lineWidth = 1 |
||||
|
||||
// draw X line
|
||||
ctx.beginPath() |
||||
ctx.moveTo(0, y * cellSize.height) |
||||
ctx.lineTo(x * cellSize.width, y * cellSize.height) |
||||
ctx.strokeStyle = '#f00' |
||||
ctx.setLineDash([cellSize.width]) |
||||
ctx.stroke() |
||||
|
||||
// draw Y line
|
||||
ctx.beginPath() |
||||
ctx.moveTo(x * cellSize.width, 0) |
||||
ctx.lineTo(x * cellSize.width, y * cellSize.height) |
||||
ctx.strokeStyle = '#0f0' |
||||
ctx.setLineDash([cellSize.height]) |
||||
ctx.stroke() |
||||
|
||||
ctx.globalAlpha = 1 |
||||
ctx.lineWidth = 1 + 0.5 * Math.sin(t * 10) |
||||
ctx.lineWidth = 1 + 0.5 * Math.sin((now / 1000) * 10) |
||||
ctx.strokeStyle = '#fff' |
||||
ctx.lineJoin = 'round' |
||||
ctx.setLineDash([2, 2]) |
||||
ctx.lineDashOffset = t * 10 |
||||
ctx.strokeRect(mouseHoverCell[0] * cellWidth, mouseHoverCell[1] * cellHeight, cellWidth, cellHeight) |
||||
ctx.lineDashOffset = (now / 1000) * 10 |
||||
ctx.strokeRect(x * cellSize.width, y * cellSize.height, cellSize.width, cellSize.height) |
||||
ctx.lineDashOffset += 2 |
||||
ctx.strokeStyle = '#000' |
||||
ctx.strokeRect(mouseHoverCell[0] * cellWidth, mouseHoverCell[1] * cellHeight, cellWidth, cellHeight) |
||||
ctx.strokeRect(x * cellSize.width, y * cellSize.height, cellSize.width, cellSize.height) |
||||
ctx.restore() |
||||
} |
||||
|
||||
if (activeCells === 0 && !mouseHoverCell && !didDrawUpdateFrames) { |
||||
isDrawing = false |
||||
removeCanvas() |
||||
} |
||||
} |
||||
|
||||
startDrawing = function () { |
||||
startDrawLoop = function () { |
||||
if (isDrawing) return |
||||
addCanvas() |
||||
updateCanvasSize() |
||||
isDrawing = true |
||||
lastDrawTime = Date.now() |
||||
drawLoop() |
||||
} |
||||
|
||||
// debug toolbar
|
||||
const toolbar = mk('div') |
||||
toolbar.classList.add('debug-toolbar') |
||||
let toolbarAttached = false |
||||
|
||||
const heartbeat = mk('div') |
||||
heartbeat.classList.add('heartbeat') |
||||
heartbeat.textContent = '❤' |
||||
toolbar.appendChild(heartbeat) |
||||
|
||||
const dataDisplay = mk('div') |
||||
dataDisplay.classList.add('data-display') |
||||
toolbar.appendChild(dataDisplay) |
||||
|
||||
const internalDisplay = mk('div') |
||||
internalDisplay.classList.add('internal-display') |
||||
toolbar.appendChild(internalDisplay) |
||||
|
||||
toolbar.appendChild(drawInfo) |
||||
|
||||
const buttons = mk('div') |
||||
buttons.classList.add('toolbar-buttons') |
||||
toolbar.appendChild(buttons) |
||||
|
||||
// heartbeat
|
||||
connection.on('heartbeat', () => { |
||||
heartbeat.classList.remove('beat') |
||||
window.requestAnimationFrame(() => { |
||||
heartbeat.classList.add('beat') |
||||
}) |
||||
}) |
||||
|
||||
{ |
||||
const redraw = mk('button') |
||||
redraw.textContent = 'Redraw' |
||||
redraw.addEventListener('click', e => { |
||||
screen.layout.renderer.resetDrawn() |
||||
screen.layout.renderer.draw('debug-redraw') |
||||
}) |
||||
buttons.appendChild(redraw) |
||||
|
||||
const fancyGraphics = mk('button') |
||||
fancyGraphics.textContent = 'Toggle Fancy Graphics' |
||||
fancyGraphics.addEventListener('click', e => { |
||||
screen.layout.renderer.graphics = +!screen.layout.renderer.graphics |
||||
screen.layout.renderer.draw('set-graphics') |
||||
}) |
||||
buttons.appendChild(fancyGraphics) |
||||
} |
||||
|
||||
const attachToolbar = function () { |
||||
screen.layout.canvas.parentNode.appendChild(toolbar) |
||||
} |
||||
const detachToolbar = function () { |
||||
toolbar.parentNode.removeChild(toolbar) |
||||
} |
||||
|
||||
screen.on('update-window:debug', debug => { |
||||
if (debug !== toolbarAttached) { |
||||
toolbarAttached = debug |
||||
if (debug) attachToolbar() |
||||
else { |
||||
detachToolbar() |
||||
removeCanvas() |
||||
} |
||||
} |
||||
}) |
||||
|
||||
const displayCellAttrs = attrs => { |
||||
let result = attrs.toString(16) |
||||
if (attrs & 1 || attrs & 2) { |
||||
result += ':has(' |
||||
if (attrs & 1) result += 'fg' |
||||
if (attrs & 2) result += (attrs & 1 ? ',' : '') + 'bg' |
||||
result += ')' |
||||
} |
||||
let attributes = [] |
||||
if (attrs & (1 << 2)) attributes.push('\\[bold]bold\\()') |
||||
if (attrs & (1 << 3)) attributes.push('\\[underline]underln\\()') |
||||
if (attrs & (1 << 4)) attributes.push('\\[invert]invert\\()') |
||||
if (attrs & (1 << 5)) attributes.push('blink') |
||||
if (attrs & (1 << 6)) attributes.push('\\[italic]italic\\()') |
||||
if (attrs & (1 << 7)) attributes.push('\\[strike]strike\\()') |
||||
if (attrs & (1 << 8)) attributes.push('\\[overline]overln\\()') |
||||
if (attrs & (1 << 9)) attributes.push('\\[faint]faint\\()') |
||||
if (attrs & (1 << 10)) attributes.push('fraktur') |
||||
if (attributes.length) result += ':' + attributes.join() |
||||
return result.trim() |
||||
} |
||||
|
||||
const formatColor = color => color < 256 ? color : `#${`000000${(color - 256).toString(16)}`.substr(-6)}` |
||||
const getCellData = cell => { |
||||
if (cell < 0 || cell > screen.screen.length) return '(-)' |
||||
let cellAttrs = screen.layout.renderer.drawnScreenAttrs[cell] | 0 |
||||
let cellFG = screen.layout.renderer.drawnScreenFG[cell] | 0 |
||||
let cellBG = screen.layout.renderer.drawnScreenBG[cell] | 0 |
||||
let fgText = formatColor(cellFG) |
||||
let bgText = formatColor(cellBG) |
||||
fgText += `\\[color=${screen.layout.renderer.getColor(cellFG).replace(/ /g, '')}]●\\[]` |
||||
bgText += `\\[color=${screen.layout.renderer.getColor(cellBG).replace(/ /g, '')}]●\\[]` |
||||
let cellCode = (screen.layout.renderer.drawnScreen[cell] || '').codePointAt(0) | 0 |
||||
let hexcode = cellCode.toString(16).toUpperCase() |
||||
if (hexcode.length < 4) hexcode = `0000${hexcode}`.substr(-4) |
||||
hexcode = `U+${hexcode}` |
||||
let x = cell % screen.window.width |
||||
let y = Math.floor(cell / screen.window.width) |
||||
return `((${y},${x})=${cell}:\\[bold]${hexcode}\\[]:F${fgText}:B${bgText}:A(${displayCellAttrs(cellAttrs)}))` |
||||
} |
||||
|
||||
const setFormattedText = (node, text) => { |
||||
node.innerHTML = '' |
||||
|
||||
let match |
||||
let attrs = {} |
||||
|
||||
let pushSpan = content => { |
||||
let span = mk('span') |
||||
node.appendChild(span) |
||||
span.textContent = content |
||||
for (let key in attrs) span[key] = attrs[key] |
||||
} |
||||
|
||||
while ((match = text.match(/\\\[(.*?)\]/))) { |
||||
if (match.index > 0) pushSpan(text.substr(0, match.index)) |
||||
|
||||
attrs = { style: '' } |
||||
let data = match[1].split(' ') |
||||
for (let attr of data) { |
||||
if (!attr) continue |
||||
let key, value |
||||
if (attr.indexOf('=') > -1) { |
||||
key = attr.substr(0, attr.indexOf('=')) |
||||
value = attr.substr(attr.indexOf('=') + 1) |
||||
} else { |
||||
key = attr |
||||
value = true |
||||
} |
||||
|
||||
if (key === 'bold') attrs.style += 'font-weight:bold;' |
||||
if (key === 'italic') attrs.style += 'font-style:italic;' |
||||
if (key === 'underline') attrs.style += 'text-decoration:underline;' |
||||
if (key === 'invert') attrs.style += 'background:#000;filter:invert(1);' |
||||
if (key === 'strike') attrs.style += 'text-decoration:line-through;' |
||||
if (key === 'overline') attrs.style += 'text-decoration:overline;' |
||||
if (key === 'faint') attrs.style += 'opacity:0.5;' |
||||
else if (key === 'color') attrs.style += `color:${value};` |
||||
else attrs[key] = value |
||||
} |
||||
|
||||
text = text.substr(match.index + match[0].length) |
||||
} |
||||
|
||||
if (text) pushSpan(text) |
||||
} |
||||
|
||||
let internalInfo = {} |
||||
|
||||
updateToolbar = () => { |
||||
if (!toolbarAttached) return |
||||
let text = `C((${screen.cursor.y},${screen.cursor.x}),hang:${screen.cursor.hanging},vis:${screen.cursor.visible})` |
||||
if (mouseHoverCell) { |
||||
text += ' m' + getCellData(mouseHoverCell[1] * screen.window.width + mouseHoverCell[0]) |
||||
} |
||||
setFormattedText(dataDisplay, text) |
||||
|
||||
if ('flags' in internalInfo) { |
||||
// we got ourselves some internal data
|
||||
let text = ' ' |
||||
text += ` flags:${internalInfo.flags.toString(2)}` |
||||
text += ` curAttrs:${internalInfo.cursorAttrs.toString(2)}` |
||||
text += ` Region:${internalInfo.regionStart}->${internalInfo.regionEnd}` |
||||
text += ` Charset:${internalInfo.charsetGx} (0:${internalInfo.charsetG0},1:${internalInfo.charsetG1})` |
||||
text += ` Heap:${internalInfo.freeHeap}` |
||||
text += ` Clients:${internalInfo.clientCount}` |
||||
setFormattedText(internalDisplay, text) |
||||
} |
||||
} |
||||
|
||||
screen.on('draw', updateToolbar) |
||||
screen.on('internal', data => { |
||||
internalInfo = data |
||||
updateToolbar() |
||||
}) |
||||
} |
||||
|
Loading…
Reference in new issue