Better shaders. Scrolling pool. Better transitions

This commit is contained in:
Mononaut 2021-03-16 20:32:47 -06:00
parent 54a867ffaa
commit 72fdcf2601
12 changed files with 503 additions and 241 deletions

View File

@ -21,15 +21,15 @@
export let running = false export let running = false
// Shader attributes // Shader attributes
// each attribute contains [x: startValue, y: endValue, z: startTime, w: rate]
// shader interpolates between start and end values at the given rate, from the given time
const attribs = { const attribs = {
startTime: { type: 'FLOAT', count: 1, pointer: null }, posX: { type: 'FLOAT', count: 4, pointer: null },
zIndex: { type: 'FLOAT', count: 1, pointer: null }, posY: { type: 'FLOAT', count: 4, pointer: null },
speed: { type: 'FLOAT', count: 1, pointer: null }, sizes: { type: 'FLOAT', count: 4, pointer: null },
positions: { type: 'FLOAT', count: 4, pointer: null }, palettes: { type: 'FLOAT', count: 4, pointer: null },
sizes: { type: 'FLOAT', count: 2, pointer: null }, colors: { type: 'FLOAT', count: 4, pointer: null },
palettes: { type: 'FLOAT', count: 2, pointer: null }, alphas: { type: 'FLOAT', count: 4, pointer: null }
colors: { type: 'FLOAT', count: 2, pointer: null },
alphas: { type: 'FLOAT', count: 2, pointer: null }
} }
// Auto-calculate the number of bytes per vertex based on specified attributes // Auto-calculate the number of bytes per vertex based on specified attributes
const stride = Object.values(attribs).reduce((total, attrib) => { const stride = Object.values(attribs).reduce((total, attrib) => {
@ -58,9 +58,10 @@
function getTxPointArray () { function getTxPointArray () {
if (controller) { if (controller) {
return new Float32Array( return controller.getVertexData()
controller.getScenes().flatMap(scene => scene.getVertexData()) // return new Float32Array(
) // controller.getScenes().flatMap(scene => scene.getVertexData())
// )
} else return [] } else return []
} }
@ -136,7 +137,9 @@
}) })
/* DRAW */ /* DRAW */
gl.drawArrays(gl.POINTS, 0, pointArray.length / 3) if (pointArray.length) {
gl.drawArrays(gl.POINTS, 0, pointArray.length / 3)
}
/* LOOP */ /* LOOP */
window.requestAnimationFrame(currentTime => { window.requestAnimationFrame(currentTime => {
@ -214,6 +217,8 @@
colorTexture = loadColorTexture(gl, '#f7941d', 'rgb(0%,100%,80%)', 500); colorTexture = loadColorTexture(gl, '#f7941d', 'rgb(0%,100%,80%)', 500);
running = true running = true
console.log(this)
}) })
</script> </script>
@ -222,9 +227,9 @@
position: absolute; position: absolute;
left: 0; left: 0;
right: 0; right: 0;
top: -5px; top: 0;
bottom: 0; bottom: 0;
pointer-events: none; /* pointer-events: none; */
overflow: hidden; overflow: hidden;
} }
</style> </style>

View File

@ -3,13 +3,12 @@
import TxController from '../controllers/TxController.js' import TxController from '../controllers/TxController.js'
import TxRender from './TxRender.svelte' import TxRender from './TxRender.svelte'
import getTxStream from '../controllers/TxStream.js' import getTxStream from '../controllers/TxStream.js'
import { darkMode, serverConnected, serverDelay, txQueueLength } from '../stores.js' import { darkMode, serverConnected, serverDelay, txQueueLength, txCount } from '../stores.js'
import BitcoinBlock from '../models/BitcoinBlock.js' import BitcoinBlock from '../models/BitcoinBlock.js'
let width = window.innerWidth let width = window.innerWidth
let height = window.innerHeight let height = window.innerHeight
let txController let txController
let txCount = 0
let blockCount = 0 let blockCount = 0
let running = false let running = false
let txStream = getTxStream() let txStream = getTxStream()
@ -53,6 +52,10 @@
// })) // }))
} }
function fakeTx () {
txController.simulateDumpTx(1)
}
function fakeTxs () { function fakeTxs () {
txController.simulateDumpTx(200) txController.simulateDumpTx(200)
} }
@ -107,6 +110,10 @@
position: absolute; position: absolute;
top: 20px; top: 20px;
right: 20px; right: 20px;
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: flex-end;
.status-light { .status-light {
display: block; display: block;
@ -124,6 +131,20 @@
background: greenyellow; background: greenyellow;
} }
} }
.stat-counter {
margin-top: 5px;
&.red {
color: red;
}
&.amber {
color: yellow;
}
&.green {
color: greenyellow;
}
}
} }
</style> </style>
@ -134,12 +155,14 @@
<TxRender controller={txController} /> <TxRender controller={txController} />
</div> </div>
<div class="sim-controls"> <div class="sim-controls">
<button on:click={fakeTx}>TXN</button>
<button on:click={fakeTxs}>TXNS</button> <button on:click={fakeTxs}>TXNS</button>
<button on:click={fakeBlock}>BLOCK</button> <button on:click={fakeBlock}>BLOCK</button>
<button on:click={toggleDark}>{$darkMode ? 'LIGHT' : 'DARK' }</button> <button on:click={toggleDark}>{$darkMode ? 'LIGHT' : 'DARK' }</button>
</div> </div>
<div class="status-bar"> <div class="status-bar">
<span class="queue-length">{ $txQueueLength }</span>
<div class="status-light {connectionColor}" title={connectionTitle}></div> <div class="status-light {connectionColor}" title={connectionTitle}></div>
<span class="stat-counter {connectionColor}">{ $txQueueLength }</span>
<span class="stat-counter {connectionColor}">{ $txCount }</span>
</div> </div>
</div> </div>

View File

@ -2,13 +2,15 @@ import TxPoolScene from '../models/TxPoolScene.js'
import TxBlockScene from '../models/TxBlockScene.js' import TxBlockScene from '../models/TxBlockScene.js'
import BitcoinTx from '../models/BitcoinTx.js' import BitcoinTx from '../models/BitcoinTx.js'
import BitcoinBlock from '../models/BitcoinBlock.js' import BitcoinBlock from '../models/BitcoinBlock.js'
import { FastVertexArray } from '../utils/memory.js'
import { txQueueLength } from '../stores.js' import { txQueueLength } from '../stores.js'
export default class TxController { export default class TxController {
constructor ({ width, height }) { constructor ({ width, height }) {
this.vertexArray = new FastVertexArray(2048, 24)
this.txs = {} this.txs = {}
this.expiredTxs = {} this.expiredTxs = {}
this.pool = new TxPoolScene({ width, height, layer: 0.0 }) this.pool = new TxPoolScene({ width, height, layer: 0.0, controller: this })
this.blocks = {} this.blocks = {}
this.clearBlockTimeout = null this.clearBlockTimeout = null
@ -19,6 +21,10 @@ export default class TxController {
this.scheduleQueue(1000) this.scheduleQueue(1000)
} }
getVertexData () {
return this.vertexArray.getVertexData()
}
getScenes () { getScenes () {
return [this.pool, ...Object.values(this.blocks)] return [this.pool, ...Object.values(this.blocks)]
} }
@ -30,7 +36,8 @@ export default class TxController {
}) })
} }
addTx (tx) { addTx (txData) {
const tx = new BitcoinTx(txData, this.vertexArray)
if (!this.txs[tx.id] && !this.expiredTxs[tx.id]) { if (!this.txs[tx.id] && !this.expiredTxs[tx.id]) {
this.pendingTxs.push([tx, Date.now()]) this.pendingTxs.push([tx, Date.now()])
txQueueLength.increment() txQueueLength.increment()
@ -77,7 +84,8 @@ export default class TxController {
}, delay) }, delay)
} }
addBlock (block) { addBlock (blockData) {
const block = new BitcoinBlock(blockData)
if (this.clearBlockTimeout) clearTimeout(this.clearBlockTimeout) if (this.clearBlockTimeout) clearTimeout(this.clearBlockTimeout)
this.expiredTxs = {} this.expiredTxs = {}
@ -86,7 +94,7 @@ export default class TxController {
if (!this.blocks[blockId].expired) this.clearBlock(blockId) if (!this.blocks[blockId].expired) this.clearBlock(blockId)
}) })
this.blocks[block.id] = new TxBlockScene({ width: 500, height: 500, layer: 1.0 }) this.blocks[block.id] = new TxBlockScene({ width: 500, height: 500, layer: 1.0, blockId: block.id, controller: this })
let knownCount = 0 let knownCount = 0
let unknownCount = 0 let unknownCount = 0
for (let i = 0; i < block.txns.length; i++) { for (let i = 0; i < block.txns.length; i++) {
@ -99,14 +107,14 @@ export default class TxController {
const tx = new BitcoinTx({ const tx = new BitcoinTx({
...block.txns[i], ...block.txns[i],
block: block.id block: block.id
}) }, this.vertexArray)
this.txs[tx.id] = tx this.txs[tx.id] = tx
this.blocks[block.id].insert(this.txs[tx.id], false) this.blocks[block.id].insert(this.txs[tx.id], false)
} }
this.expiredTxs[block.txns[i].id] = true this.expiredTxs[block.txns[i].id] = true
} }
console.log(`New block with ${knownCount} known transactions and ${unknownCount} unknown transactions`) console.log(`New block with ${knownCount} known transactions and ${unknownCount} unknown transactions`)
this.blocks[block.id].layoutAll() this.blocks[block.id].initialLayout()
setTimeout(() => { this.pool.layoutAll() }, 2000) setTimeout(() => { this.pool.layoutAll() }, 2000)
this.clearBlockTimeout = setTimeout(() => { this.clearBlock(block.id) }, 10000) this.clearBlockTimeout = setTimeout(() => { this.clearBlock(block.id) }, 10000)
@ -129,7 +137,7 @@ export default class TxController {
// } // }
// }) // })
const simulatedTxns = [] const simulatedTxns = []
Object.values(this.txs).forEach(tx => { Object.values(this.pool.txs).forEach(tx => {
if (Math.random() < 0.5) { if (Math.random() < 0.5) {
simulatedTxns.push({ simulatedTxns.push({
version: tx.version, version: tx.version,
@ -150,7 +158,7 @@ export default class TxController {
txn_count: 20, txn_count: 20,
txns: simulatedTxns txns: simulatedTxns
})) }))
}, 2500) }, 0)
} }
simulateDumpTx (n) { simulateDumpTx (n) {
@ -160,22 +168,25 @@ export default class TxController {
time: Date.now(), time: Date.now(),
id: `simulated_${i}_${Math.random()}`, id: `simulated_${i}_${Math.random()}`,
value: Math.floor(Math.random() * 100000) value: Math.floor(Math.random() * 100000)
})) }, this.vertexArray))
} }
} }
clearBlock (id) { clearBlock (id) {
if (this.blocks[id]) { if (this.blocks[id]) {
this.blocks[id].expire() this.blocks[id].expire()
setTimeout(() => {
const txs = this.blocks[id].getTxList()
for (let i = 0; i < txs.length; i++) {
if (this.blocks[id].remove(txs[i])) {
delete this.txs[txs[i]]
}
}
delete this.blocks[id]
}, 3000)
} }
} }
destroyTx (id) {
this.getScenes().forEach(scene => {
scene.remove(id)
})
if (this.txs[id]) this.txs[id].destroy()
delete this.txs[id]
}
destroyBlock (id) {
if (this.blocks) delete this.blocks[id]
}
} }

View File

@ -1,5 +1,3 @@
import BitcoinTx from '../models/BitcoinTx.js'
import BitcoinBlock from '../models/BitcoinBlock.js'
import { serverConnected, serverDelay } from '../stores.js' import { serverConnected, serverDelay } from '../stores.js'
class TxStream { class TxStream {
@ -88,12 +86,10 @@ class TxStream {
} else { } else {
const msg = JSON.parse(event.data) const msg = JSON.parse(event.data)
if (msg && msg.type === 'txn') { if (msg && msg.type === 'txn') {
const tx = new BitcoinTx(msg.txn) window.dispatchEvent(new CustomEvent('bitcoin_tx', { detail: msg.txn }))
window.dispatchEvent(new CustomEvent('bitcoin_tx', { detail: tx }))
} else if (msg && msg.type === 'block') { } else if (msg && msg.type === 'block') {
const block = new BitcoinBlock(msg.block) console.log('Block recieved: ', msg.block)
console.log('Block recieved: ', block) window.dispatchEvent(new CustomEvent('bitcoin_block', { detail: msg.block }))
window.dispatchEvent(new CustomEvent('bitcoin_block', { detail: block }))
} else { } else {
console.log('unknown message from websocket: ', msg) console.log('unknown message from websocket: ', msg)
} }

View File

@ -1,9 +1,10 @@
import TxView from './TxView.js' import TxView from './TxView.js'
export default class BitcoinTx { export default class BitcoinTx {
constructor ({ version, id, value, inputs, outputs, witnesses, time, block }) { constructor ({ version, id, value, inputs, outputs, witnesses, time, block }, vertexArray) {
this.version = version this.version = version
this.id = id this.id = id
this.vertexArray = vertexArray
if (inputs && outputs) { if (inputs && outputs) {
this.inputs = inputs this.inputs = inputs
@ -20,6 +21,10 @@ export default class BitcoinTx {
this.view = new TxView(this) this.view = new TxView(this)
} }
destroy () {
if (this.view) this.view.destroy()
}
calcValue () { calcValue () {
if (this.outputs && this.outputs.length) { if (this.outputs && this.outputs.length) {
return this.outputs.reduce((acc, output) => { return this.outputs.reduce((acc, output) => {
@ -37,7 +42,11 @@ export default class BitcoinTx {
if (this.view) this.view.update(update) if (this.view) this.view.update(update)
} }
setPosition (position) {
this.position = position
}
getPosition () { getPosition () {
if (this.view) return this.view.getPosition() if (this.position) return this.position
} }
} }

View File

@ -1,10 +1,12 @@
import TxPoolScene from './TxPoolScene.js' import TxPoolScene from './TxPoolScene.js'
export default class TxBlockScene extends TxPoolScene { export default class TxBlockScene extends TxPoolScene {
constructor ({ width, height, unit = 4, padding = 1, layer }) { constructor ({ width, height, unit = 4, padding = 1, layer, blockId, controller }) {
super({ width, height, unit, padding, layer }) super({ width, height, unit, padding, layer, controller })
this.heightLimit = null this.heightLimit = null
this.expired = false this.expired = false
this.layedOut = false
this.blockId = blockId
} }
resize ({ width, height, unit, padding }) { resize ({ width, height, unit, padding }) {
@ -48,7 +50,8 @@ export default class TxBlockScene extends TxPoolScene {
state: 'ready' state: 'ready'
}) })
} }
const flyToBlock = {
this.updateTx(tx, {
display: { display: {
position, position,
size: 4, size: 4,
@ -60,33 +63,44 @@ export default class TxBlockScene extends TxPoolScene {
}, },
duration: 1500, duration: 1500,
delay: 0, delay: 0,
state: 'flytoblock', state: 'block'
next: { })
display: {}, }
state: 'block'
} prepareTx (tx, sequence) {
} const position = this.place(tx.id, sequence)
if (tx.view.state === 'pool' || tx.view.state === 'colorfade') { if (!tx.view.initialised) {
this.updateTx(tx, { this.updateTx(tx, {
display: { display: {
layer: this.layer, layer: this.layer,
position: {
x: window.innerWidth * ((position.x - this.scene.offset.x) / this.width),
y: window.innerHeight * (1 + ((position.y - this.scene.offset.y) / this.height))
},
size: 8,
color: { color: {
palette: 0, palette: 0,
index: 0, index: 0,
alpha: 1 alpha: 1
} }
}, },
duration: 1000, duration: 1500,
delay: 0, delay: 0,
state: 'colorfade', state: 'ready'
next: flyToBlock
})
} else {
this.updateTx(tx, {
...flyToBlock,
duration: 2500
}) })
} }
this.updateTx(tx, {
display: {
layer: this.layer,
color: {
palette: 0,
index: 0,
alpha: 1
}
},
duration: 2000,
delay: 0
})
} }
expireTx (tx) { expireTx (tx) {
@ -103,9 +117,30 @@ export default class TxBlockScene extends TxPoolScene {
}) })
} }
prepareAll () {
this.resize({})
this.scene.count = 0
let ids = this.getHiddenTxList()
for (let i = 0; i < ids.length; i++) {
this.txs[ids[i]] = this.hiddenTxs[ids[i]]
delete this.hiddenTxs[ids[i]]
}
ids = this.getActiveTxList()
for (let i = 0; i < ids.length; i++) {
this.prepareTx(this.txs[ids[i]], this.scene.count++)
}
}
layoutAll () { layoutAll () {
this.resize({}) this.resize({})
super.layoutAll(4) super.layoutAll()
}
initialLayout () {
this.prepareAll()
setTimeout(() => {
this.layoutAll()
}, 2000)
} }
expire () { expire () {
@ -114,5 +149,12 @@ export default class TxBlockScene extends TxPoolScene {
for (let i = 0; i < ids.length; i++) { for (let i = 0; i < ids.length; i++) {
this.expireTx(this.txs[ids[i]]) this.expireTx(this.txs[ids[i]])
} }
setTimeout(() => {
const txIds = this.getTxList()
for (let i = 0; i < txIds.length; i++) {
if (this.txs[txIds[i]]) this.controller.destroyTx(txIds[i])
}
this.controller.destroyBlock(this.blockId)
}, 3000)
} }
} }

View File

@ -1,14 +1,20 @@
export default class TxPoolScene { export default class TxPoolScene {
constructor ({ width, height, unit, padding, layer }) { constructor ({ width, height, unit, padding, layer, controller }) {
this.init({ width, height, unit, padding, layer }) this.init({ width, height, unit, padding, layer, controller })
} }
init ({ width, height, unit = 8, padding = 1, layer }) { init ({ width, height, unit = 8, padding = 1, layer, controller }) {
this.controller = controller
this.layer = layer this.layer = layer
this.resize({ width, height, unit, padding }) this.resize({ width, height, unit, padding })
this.txs = {} this.txs = {}
this.hiddenTxs = {} this.hiddenTxs = {}
this.heightLimit = this.height - 50
this.heightLimit = Math.max(150, height / 4)
this.heightBound = this.height - this.heightLimit
this.poolTop = this.height
this.poolBottom = this.height
this.scene = { this.scene = {
width: width, width: width,
height: height, height: height,
@ -21,6 +27,8 @@ export default class TxPoolScene {
} }
this.scrollRateLimitTimer = null this.scrollRateLimitTimer = null
this.initialised = true this.initialised = true
console.log('pool', this)
} }
resize ({ width, height, unit, padding }) { resize ({ width, height, unit, padding }) {
@ -37,14 +45,8 @@ export default class TxPoolScene {
if (tx) tx.updateView(update) if (tx) tx.updateView(update)
} }
scroll (offset) { getPoolHeight () {
if (!this.scrollRateLimitTimer || Date.now() < (this.scrollRateLimitTimer + 10000)) { return this.poolBottom - this.poolTop
console.log(`scrolling pool by ${offset}`)
this.scrollRateLimitTimer = Date.now()
this.doScroll(offset)
} else {
console.log('scroll recharging')
}
} }
insert (tx, autoLayout=true) { insert (tx, autoLayout=true) {
@ -56,36 +58,81 @@ export default class TxPoolScene {
} }
} }
clearOffscreenTx (tx) {
const currentTargetPosition = tx.getPosition()
if (currentTargetPosition && (currentTargetPosition.y + this.scene.scroll) > this.height + 150) {
this.controller.destroyTx(tx.id)
}
}
clearOffscreenTxs () {
if (this.poolBottom + this.scene.scroll > (this.height + 150)) {
const ids = this.getTxList()
for (let i = 0; i < ids.length; i++) {
this.clearOffscreenTx(this.txs[ids[i]])
}
}
}
scrollTx (tx, scrollDistance) { scrollTx (tx, scrollDistance) {
if (tx.view.initialised) { if (tx.view.initialised) {
let currentPosition = tx.getPosition() let currentTargetPosition = tx.getPosition()
this.updateTx(tx, { if (currentTargetPosition) {
display: { this.updateTx(tx, {
position: { display: {
y: currentPosition.y + scrollDistance position: {
} y: currentTargetPosition.y + scrollDistance
} }
}) },
duration: 500,
minDuration: 250,
adjust: true
})
}
} }
} }
doScroll (offset) { doScroll (offset) {
const ids = this.getActiveTxList() const ids = this.getTxList()
for (let i = 0; i < ids.length; i++) {
this.scrollTx(this.txs[ids[i]], offset)
}
this.scene.scroll -= offset this.scene.scroll -= offset
for (let i = 0; i < ids.length; i++) {
this.scrollTx(this.txs[ids[i]], this.scene.scroll)
}
this.clearOffscreenTxs()
}
scroll (offset, force) {
if (!this.scrollRateLimitTimer || force || Date.now() > (this.scrollRateLimitTimer + 1000)) {
this.scrollRateLimitTimer = Date.now()
this.doScroll(offset)
}
}
txSize (value) {
// let scale = Math.log10(value)
// let size = (scale*scale) / 5
// let rounded = Math.pow(2, Math.ceil(Math.log2(size)))
// return Math.max(4, rounded)
return this.unitWidth
} }
layoutTx (tx, sequence) { layoutTx (tx, sequence) {
const position = this.place(tx.id, sequence) const rawPosition = this.place(tx.id, sequence)
// if (this.heightLimit && position.y < this.heightLimit) this.scroll(position.y - this.heightLimit) const scrolledPosition = {
x: rawPosition.x,
y: rawPosition.y + this.scene.scroll
}
tx.setPosition(rawPosition)
if (this.heightLimit && scrolledPosition.y < this.heightBound) {
this.scroll(scrolledPosition.y - this.heightBound)
scrolledPosition.y = rawPosition.y + this.scene.scroll
}
if (!tx.view.initialised) { if (!tx.view.initialised) {
this.updateTx(tx, { this.updateTx(tx, {
display: { display: {
layer: this.layer, layer: this.layer,
position: { position: {
x: position.x, x: scrolledPosition.x,
y: 0 y: 0
}, },
size: this.unitWidth, size: this.unitWidth,
@ -102,8 +149,9 @@ export default class TxPoolScene {
this.updateTx(tx, { this.updateTx(tx, {
display: { display: {
layer: this.layer, layer: this.layer,
position, position: scrolledPosition,
size: this.unitWidth, // size: this.unitWidth,
size: this.txSize(tx.value),
color: { color: {
palette: 0, palette: 0,
index: 0, index: 0,
@ -112,60 +160,35 @@ export default class TxPoolScene {
}, },
duration: 1500, duration: 1500,
delay: 0, delay: 0,
state: 'entering', state: 'pool'
next: { })
display: { this.updateTx(tx, {
color: { display: {
palette: 0, color: {
index: 1 palette: 0,
} index: 1
},
duration: 30000,
delay: 100,
state: 'colorfade',
next: {
display: {},
state: 'pool'
} }
} },
duration: 30000,
delay: 0
}) })
} else { } else {
const shuffleUpdate = { this.updateTx(tx, {
display: { display: {
position position: scrolledPosition
}, },
duration: 1000, duration: 1000,
minDuration: 1000,
delay: 0, delay: 0,
state: 'shuffling', adjust: true
next: { })
display: {
color: {
index: 1
}
},
state: 'colorfade',
duration: 15000,
delay: 100
}
}
// if (tx.view.state === 'pool' || tx.view.state === 'colorfade') {
// this.updateTx(tx, {
// display: {
// layer: this.layer
// },
// duration: 2000,
// delay: 0,
// state: 'pause',
// next: shuffleUpdate
// })
// } else {
this.updateTx(tx, shuffleUpdate)
// }
} }
} }
layoutAll () { layoutAll () {
this.scene.count = 0 this.scene.count = 0
this.poolTop = Infinity
this.poolBottom = -Infinity
let ids = this.getHiddenTxList() let ids = this.getHiddenTxList()
for (let i = 0; i < ids.length; i++) { for (let i = 0; i < ids.length; i++) {
this.txs[ids[i]] = this.hiddenTxs[ids[i]] this.txs[ids[i]] = this.hiddenTxs[ids[i]]
@ -175,6 +198,14 @@ export default class TxPoolScene {
for (let i = 0; i < ids.length; i++) { for (let i = 0; i < ids.length; i++) {
this.layoutTx(this.txs[ids[i]], this.scene.count++) this.layoutTx(this.txs[ids[i]], this.scene.count++)
} }
if (this.heightLimit && ((this.poolTop + this.scene.scroll) < this.heightBound)) {
let scrollAmount = (this.poolTop + this.scene.scroll) - this.heightBound
this.scroll(scrollAmount, true)
} else if (this.heightLimit && ((this.poolTop + this.scene.scroll) > this.heightBound)) {
let scrollAmount = Math.min(this.scene.scroll, (this.poolTop + this.scene.scroll) - this.heightBound)
this.scroll(scrollAmount, true)
}
} }
remove (id) { remove (id) {
@ -201,10 +232,16 @@ export default class TxPoolScene {
} }
place (id, position) { place (id, position) {
return { const placement = {
x: this.scene.offset.x + (this.paddedUnitWidth * (1 + Math.floor(position % this.blockWidth))), x: this.scene.offset.x + (this.paddedUnitWidth * (1 + Math.floor(position % this.blockWidth))),
y: this.scene.offset.y + this.height - (this.paddedUnitWidth * (0.5 + (Math.floor(position / this.blockWidth)))) + this.scene.scroll y: this.scene.offset.y + this.height - (this.paddedUnitWidth * (1 + (Math.floor(position / this.blockWidth))))
} }
if (placement.y < this.poolTop) {
this.poolTop = placement.y
} else if (placement.y > this.poolBottom) {
this.poolBottom = placement.y
}
return placement
} }
getVertexData () { getVertexData () {

View File

@ -1,105 +1,132 @@
function interpolateAnimationState (from, to, progress) { function interpolateAttributeStart(attribute, now, label) {
const clampedProgress = Math.max(0,Math.min(1,progress)) if (attribute.v == 0 || (attribute.t + attribute.d) <= now) {
return Object.keys(from).reduce((result, key) => { // transition finished, next transition starts from current end state
if (to[key] != null) { // (clamp to 1)
result[key] = from[key] + ((to[key] - from[key]) * clampedProgress) attribute.a = attribute.b
} attribute.v = 0
return result attribute.d = 0
}, from) } else if (attribute.t > now) {
// transition not started
// (clamp to 0)
} else {
// transition in progress
// (interpolate)
let progress = (now - attribute.t)
attribute.a = attribute.a + ((progress / attribute.d) * (attribute.b - attribute.a))
attribute.d = attribute.d - progress
attribute.v = 1 / attribute.d
}
} }
export default class TxSprite { export default class TxSprite {
constructor({ now, id, value, layer, position, size, palette, color, alpha }) { constructor({ now = Date.now(), id, value, layer, position, size, palette, color, alpha }, vertexArray) {
this.id = id this.id = id
this.value = value this.value = value
this.layer = layer this.layer = layer
this.vertexArray = vertexArray
this.duration = 0 this.attributes = {
this.v = 0 x: { a: position.x, b: position.x, t: now, v: 0, d: 0 },
this.start = now y: { a: position.y, b: position.y, t: now, v: 0, d: 0 },
r: { a: size, b: size, t: now, v: 0, d: 0 },
p: { a: palette, b: palette, t: now, v: 0, d: 0 },
c: { a: color, b: color, t: now, v: 0, d: 0 },
a: { a: alpha, b: alpha, t: now, v: 0, d: 0 },
}
this.from = { this.vertexPointer = this.vertexArray.insert(this)
x: position.x,
y: position.y, this.compile()
}
update({ now, layer, position, size, palette, color, alpha, duration, minDuration, adjust }) {
const v = duration > 0 ? (1 / duration) : 0
let update = {
x: position ? position.x : null,
y: position ? position.y: null,
r: size, r: size,
p: palette, p: palette,
c: color, c: color,
a: alpha a: alpha
} }
this.to = {
...this.from
}
this.compile() for (const key of Object.keys(update)) {
} // for each non-null attribute:
if (update[key] != null) {
update({ now, duration, layer, position, size, palette, color, alpha }) { // calculate current interpolated value, and set as 'from'
// Save a copy of the current target display interpolateAttributeStart(this.attributes[key], now, key)
const currentTarget = this.to // set 'start' to now
this.attributes[key].t = now
// Check if we're mid-transition // if 'adjust' flag set
const progress = (this.duration && this.start) ? ((now - this.start) / this.duration) : 0 // set 'duration' to Max(remaining time, 'duration')
if (progress >= 1 || progress <= 0) { if (!adjust || (duration && this.attributes[key].d == 0)) {
// Transition finished or hasn't started: this.attributes[key].v = v
// so next transition starts from the current display state this.attributes[key].d = duration
this.from = { } else if (minDuration > this.attributes[key].d) {
...currentTarget this.attributes[key].v = 1 / minDuration
this.attributes[key].d = minDuration
}
// set 'to' to target value
this.attributes[key].b = update[key]
} }
} else {
// Mid-transition:
// we want to start the next transition from our current intermediate state
// so find that by interpolating between the last transitions start and end states
this.from = interpolateAnimationState(this.from, currentTarget, progress)
} }
// Build new target display, inheriting from the current target where necessary
this.to = {
x: (position && position.x != null) ? position.x : this.from.x,
y: (position && position.y != null) ? position.y : this.from.y,
r: (size != null) ? size : this.from.r,
p: (palette != null) ? palette : this.from.p,
c: (color != null) ? color : this.from.c,
a: (alpha != null) ? alpha : this.from.a
}
if (layer != null) this.layer = layer
if (duration != null) {
this.duration = duration
this.v = duration ? (1 / duration) : 0
}
this.start = now
this.compile() this.compile()
} }
compile () { compile () {
this.vertexData = [ this.vertexData = [
this.start, this.attributes.x.a,
this.layer, this.attributes.x.b,
this.v, this.attributes.x.t,
this.from.x, this.attributes.x.v,
this.from.y,
this.to.x, this.attributes.y.a,
this.to.y, this.attributes.y.b,
this.from.r, this.attributes.y.t,
this.to.r, this.attributes.y.v,
this.from.p,
this.to.p, this.attributes.r.a,
this.from.c, this.attributes.r.b,
this.to.c, this.attributes.r.t,
this.from.a, this.attributes.r.v,
this.to.a,
this.attributes.p.a,
this.attributes.p.b,
this.attributes.p.t,
this.attributes.p.v,
this.attributes.c.a,
this.attributes.c.b,
this.attributes.c.t,
this.attributes.c.v,
this.attributes.a.a,
this.attributes.a.b,
this.attributes.a.t,
this.attributes.a.v,
] ]
this.vertexArray.setData(this.vertexPointer, this.vertexData)
}
moveVertexPointer (index) {
this.vertexPointer = index
} }
getPosition () { getPosition () {
return { return {
x: this.to.x, x: this.attributes.x.b,
y: this.to.y y: this.attributes.y.b
} }
} }
getVertexData () { getVertexData () {
return this.vertexData return this.vertexData
} }
destroy () {
this.vertexArray.remove(this.vertexPointer)
this.vertexPointer = null
}
} }

View File

@ -2,21 +2,28 @@ import TxSprite from './TxSprite.js'
import { timeOffset } from '../utils/time.js' import { timeOffset } from '../utils/time.js'
// converts from this class's update format to TxSprite's update format // converts from this class's update format to TxSprite's update format
function toSpriteUpdate(display, duration, start) { // now, id, value, layer, position, size, palette, color, alpha, duration, adjust
function toSpriteUpdate(display, duration, start, adjust) {
return { return {
now: start ? start - timeOffset : Date.now() - timeOffset, now: start ? start - timeOffset : Date.now() - timeOffset,
duration, duration: duration,
...display, ...display,
...(display.color ? { palette: display.color.palette, color: display.color.index, alpha: display.color.alpha } : { color: null }) ...(display.color ? { palette: display.color.palette, color: display.color.index, alpha: display.color.alpha } : { color: null }),
adjust
} }
} }
export default class TxView { export default class TxView {
constructor ({ id, time, value }) { constructor ({ id, time, value, vertexArray }) {
this.id = id this.id = id
this.time = time this.time = time
this.value = value this.value = value
this.initialised = false this.initialised = false
this.vertexArray = vertexArray
}
destroy () {
if (this.sprite) this.sprite.destroy()
} }
/* /*
@ -30,28 +37,18 @@ export default class TxView {
duration: of the tweening animation from the previous display state duration: of the tweening animation from the previous display state
delay: for queued transitions, how long to wait after current transition delay: for queued transitions, how long to wait after current transition
completes to start. completes to start.
next: another transition to animate after this one completes
(you can nest several transitions like this)
Performing another direct update cancels any pending transitions
*/ */
update ({ display, duration, delay, state, next, start }) { update ({ display, duration, delay, state, start, adjust }) {
if (next) {
if (this.awaiting) clearTimeout(this.awaiting)
this.awaiting = setTimeout(() => {
this.update(next)
}, (duration || 0) + (next.delay || 0))
}
this.state = state this.state = state
if (!this.initialised) { if (!this.initialised || !this.sprite) {
this.initialised = true this.initialised = true
const update = toSpriteUpdate(display, duration, start) const update = toSpriteUpdate(display, duration, start)
update.id = this.id update.id = this.id
update.value = this.value update.value = this.value
this.sprite = new TxSprite(update) this.sprite = new TxSprite(update, this.vertexArray)
} else { } else {
const update = toSpriteUpdate(display, duration, start) const update = toSpriteUpdate(display, duration, start, adjust)
this.sprite.update(update) this.sprite.update(update)
} }
} }

View File

@ -1,17 +1,16 @@
varying lowp vec4 vColor; varying lowp vec4 vColor;
attribute float startTime; // each attribute contains [x: startValue, y: endValue, z: startTime, w: rate]
attribute float zIndex; // shader interpolates between start and end values at the given rate, from the given time
attribute float speed; attribute vec4 posX;
attribute vec4 positions; attribute vec4 posY;
attribute vec2 sizes; attribute vec4 sizes;
attribute vec2 colors; attribute vec4 colors;
attribute vec2 palettes; attribute vec4 palettes;
attribute vec2 alphas; attribute vec4 alphas;
uniform vec2 screenSize; uniform vec2 screenSize;
uniform float now; uniform float now;
// uniform float opacityTarget; // target opacity for fading points
uniform sampler2D colorTexture; uniform sampler2D colorTexture;
vec3 selectPalette(float index) { vec3 selectPalette(float index) {
@ -22,17 +21,20 @@ vec3 selectPalette(float index) {
} }
} }
float interpolateAttribute(vec4 attr) {
float delta = clamp((now - attr.z) * attr.w, 0.0, 1.0);
return mix(attr.x, attr.y, delta);
}
void main() { void main() {
vec4 screenTransform = vec4(2.0 / screenSize.x, -2.0 / screenSize.y, -1.0, 1.0); vec4 screenTransform = vec4(2.0 / screenSize.x, -2.0 / screenSize.y, -1.0, 1.0);
float delta = clamp((now - startTime) * speed, 0.0, 1.0); vec2 position = vec2(interpolateAttribute(posX), interpolateAttribute(posY));
gl_PointSize = interpolateAttribute(sizes);
gl_Position = vec4(position * screenTransform.xy + screenTransform.zw, 1.0, 1.0);
vec2 position = mix(positions.xy, positions.zw, delta); float colorIndex = interpolateAttribute(colors);
gl_PointSize = mix(sizes.x, sizes.y, delta); float alpha = interpolateAttribute(alphas);
gl_Position = vec4(position * screenTransform.xy + screenTransform.zw, zIndex, 1.0);
float colorIndex = mix(colors.x, colors.y, delta);
float alpha = mix(alphas.x, alphas.y, delta);
if (palettes.y < 1.0) { // texture color if (palettes.y < 1.0) { // texture color
vec4 texel = texture2D(colorTexture, vec2(colorIndex, 0.0)); vec4 texel = texture2D(colorTexture, vec2(colorIndex, 0.0));
vColor = vec4(texel.rgb, alpha); vColor = vec4(texel.rgb, alpha);

View File

@ -296,3 +296,4 @@ function createCounter () {
} }
export const txQueueLength = createCounter() export const txQueueLength = createCounter()
export const txCount = createCounter()

112
src/utils/memory.js Normal file
View File

@ -0,0 +1,112 @@
import { txCount } from '../stores.js'
/*
Utility class for access and management of low-level sprite data
Maintains a single Float32Array of sprite data, keeping track of empty slots
to allow constant-time insertion and deletion
Automatically resizes by copying to a new, larger Float32Array when necessary,
or compacting into a smaller Float32Array when there's space to do so.
*/
export class FastVertexArray {
constructor (length, stride) {
// console.log(`Creating Fast Vertex Array with length ${length} and stride ${stride} `)
this.length = length
this.count = 0
this.stride = stride
this.sprites = []
this.data = new Float32Array(this.length * this.stride)
this.freeSlots = []
this.lastSlot = 0
this.nullSprite = new Float32Array(this.stride)
// this.print()
}
print () {
// console.log(`Length: ${this.length}, Free slots: ${this.freeSlots.length}, last slot: ${this.lastSlot}`)
// console.log(this.freeSlots)
// console.log(this.data)
}
insert (sprite) {
// console.log('inserting into FVA')
this.count++
txCount.increment()
let position
if (this.freeSlots.length) {
position = this.freeSlots.shift()
} else {
position = this.lastSlot
this.lastSlot++
if (this.lastSlot > this.length) {
this.expand()
}
}
// this.print()
this.sprites[position] = sprite
return position
}
remove (index) {
this.count--
txCount.decrement()
this.setData(index, this.nullSprite)
this.freeSlots.push(index)
this.sprites[index] = null
if (this.length > 2048 && this.count < (this.length * 0.4)) this.compact()
// this.print()
}
setData (index, dataChunk) {
// console.log(`Updating chunk at ${index} (${index * this.stride})`)
this.data.set(dataChunk, (index * this.stride))
// this.print()
}
getData (index) {
return this.data.subarray(index, this.stride)
}
expand () {
// console.log('Expanding FVA')
this.length *= 2
const newData = new Float32Array(this.length * this.stride)
newData.set(this.data)
this.data = newData
this.print()
}
compact () {
// console.log('Compacting FVA')
// console.log(this.sprites)
// New array length is the smallest power of 2 larger than the sprite count
const newLength = Math.pow(2, Math.ceil(Math.log2(this.count)))
if (this.newLength != this.length) {
// console.log(`compacting from ${this.length} to ${newLength}`)
this.length = newLength
this.data = new Float32Array(this.length * this.stride)
let sprite
const newSprites = []
let i = 0
for (var index in this.sprites) {
sprite = this.sprites[index]
if (sprite) {
newSprites.push(sprite)
sprite.moveVertexPointer(i)
sprite.compile()
i++
}
}
this.sprites = newSprites
this.freeSlots = []
this.lastSlot = i
}
this.print()
}
getVertexData () {
return this.data
}
}