mirror of
https://github.com/Retropex/bitfeed.git
synced 2025-05-13 03:30:47 +02:00
Better shaders. Scrolling pool. Better transitions
This commit is contained in:
parent
54a867ffaa
commit
72fdcf2601
@ -21,15 +21,15 @@
|
||||
export let running = false
|
||||
|
||||
// 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 = {
|
||||
startTime: { type: 'FLOAT', count: 1, pointer: null },
|
||||
zIndex: { type: 'FLOAT', count: 1, pointer: null },
|
||||
speed: { type: 'FLOAT', count: 1, pointer: null },
|
||||
positions: { type: 'FLOAT', count: 4, pointer: null },
|
||||
sizes: { type: 'FLOAT', count: 2, pointer: null },
|
||||
palettes: { type: 'FLOAT', count: 2, pointer: null },
|
||||
colors: { type: 'FLOAT', count: 2, pointer: null },
|
||||
alphas: { type: 'FLOAT', count: 2, pointer: null }
|
||||
posX: { type: 'FLOAT', count: 4, pointer: null },
|
||||
posY: { type: 'FLOAT', count: 4, pointer: null },
|
||||
sizes: { type: 'FLOAT', count: 4, pointer: null },
|
||||
palettes: { type: 'FLOAT', count: 4, pointer: null },
|
||||
colors: { type: 'FLOAT', count: 4, pointer: null },
|
||||
alphas: { type: 'FLOAT', count: 4, pointer: null }
|
||||
}
|
||||
// Auto-calculate the number of bytes per vertex based on specified attributes
|
||||
const stride = Object.values(attribs).reduce((total, attrib) => {
|
||||
@ -58,9 +58,10 @@
|
||||
|
||||
function getTxPointArray () {
|
||||
if (controller) {
|
||||
return new Float32Array(
|
||||
controller.getScenes().flatMap(scene => scene.getVertexData())
|
||||
)
|
||||
return controller.getVertexData()
|
||||
// return new Float32Array(
|
||||
// controller.getScenes().flatMap(scene => scene.getVertexData())
|
||||
// )
|
||||
} else return []
|
||||
}
|
||||
|
||||
@ -136,7 +137,9 @@
|
||||
})
|
||||
|
||||
/* DRAW */
|
||||
gl.drawArrays(gl.POINTS, 0, pointArray.length / 3)
|
||||
if (pointArray.length) {
|
||||
gl.drawArrays(gl.POINTS, 0, pointArray.length / 3)
|
||||
}
|
||||
|
||||
/* LOOP */
|
||||
window.requestAnimationFrame(currentTime => {
|
||||
@ -214,6 +217,8 @@
|
||||
colorTexture = loadColorTexture(gl, '#f7941d', 'rgb(0%,100%,80%)', 500);
|
||||
|
||||
running = true
|
||||
|
||||
console.log(this)
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -222,9 +227,9 @@
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: -5px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
/* pointer-events: none; */
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
@ -3,13 +3,12 @@
|
||||
import TxController from '../controllers/TxController.js'
|
||||
import TxRender from './TxRender.svelte'
|
||||
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'
|
||||
|
||||
let width = window.innerWidth
|
||||
let height = window.innerHeight
|
||||
let txController
|
||||
let txCount = 0
|
||||
let blockCount = 0
|
||||
let running = false
|
||||
let txStream = getTxStream()
|
||||
@ -53,6 +52,10 @@
|
||||
// }))
|
||||
}
|
||||
|
||||
function fakeTx () {
|
||||
txController.simulateDumpTx(1)
|
||||
}
|
||||
|
||||
function fakeTxs () {
|
||||
txController.simulateDumpTx(200)
|
||||
}
|
||||
@ -107,6 +110,10 @@
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-end;
|
||||
|
||||
.status-light {
|
||||
display: block;
|
||||
@ -124,6 +131,20 @@
|
||||
background: greenyellow;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-counter {
|
||||
margin-top: 5px;
|
||||
|
||||
&.red {
|
||||
color: red;
|
||||
}
|
||||
&.amber {
|
||||
color: yellow;
|
||||
}
|
||||
&.green {
|
||||
color: greenyellow;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -134,12 +155,14 @@
|
||||
<TxRender controller={txController} />
|
||||
</div>
|
||||
<div class="sim-controls">
|
||||
<button on:click={fakeTx}>TXN</button>
|
||||
<button on:click={fakeTxs}>TXNS</button>
|
||||
<button on:click={fakeBlock}>BLOCK</button>
|
||||
<button on:click={toggleDark}>{$darkMode ? 'LIGHT' : 'DARK' }</button>
|
||||
</div>
|
||||
<div class="status-bar">
|
||||
<span class="queue-length">{ $txQueueLength }</span>
|
||||
<div class="status-light {connectionColor}" title={connectionTitle}></div>
|
||||
<span class="stat-counter {connectionColor}">{ $txQueueLength }</span>
|
||||
<span class="stat-counter {connectionColor}">{ $txCount }</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,13 +2,15 @@ import TxPoolScene from '../models/TxPoolScene.js'
|
||||
import TxBlockScene from '../models/TxBlockScene.js'
|
||||
import BitcoinTx from '../models/BitcoinTx.js'
|
||||
import BitcoinBlock from '../models/BitcoinBlock.js'
|
||||
import { FastVertexArray } from '../utils/memory.js'
|
||||
import { txQueueLength } from '../stores.js'
|
||||
|
||||
export default class TxController {
|
||||
constructor ({ width, height }) {
|
||||
this.vertexArray = new FastVertexArray(2048, 24)
|
||||
this.txs = {}
|
||||
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.clearBlockTimeout = null
|
||||
|
||||
@ -19,6 +21,10 @@ export default class TxController {
|
||||
this.scheduleQueue(1000)
|
||||
}
|
||||
|
||||
getVertexData () {
|
||||
return this.vertexArray.getVertexData()
|
||||
}
|
||||
|
||||
getScenes () {
|
||||
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]) {
|
||||
this.pendingTxs.push([tx, Date.now()])
|
||||
txQueueLength.increment()
|
||||
@ -77,7 +84,8 @@ export default class TxController {
|
||||
}, delay)
|
||||
}
|
||||
|
||||
addBlock (block) {
|
||||
addBlock (blockData) {
|
||||
const block = new BitcoinBlock(blockData)
|
||||
if (this.clearBlockTimeout) clearTimeout(this.clearBlockTimeout)
|
||||
|
||||
this.expiredTxs = {}
|
||||
@ -86,7 +94,7 @@ export default class TxController {
|
||||
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 unknownCount = 0
|
||||
for (let i = 0; i < block.txns.length; i++) {
|
||||
@ -99,14 +107,14 @@ export default class TxController {
|
||||
const tx = new BitcoinTx({
|
||||
...block.txns[i],
|
||||
block: block.id
|
||||
})
|
||||
}, this.vertexArray)
|
||||
this.txs[tx.id] = tx
|
||||
this.blocks[block.id].insert(this.txs[tx.id], false)
|
||||
}
|
||||
this.expiredTxs[block.txns[i].id] = true
|
||||
}
|
||||
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)
|
||||
|
||||
this.clearBlockTimeout = setTimeout(() => { this.clearBlock(block.id) }, 10000)
|
||||
@ -129,7 +137,7 @@ export default class TxController {
|
||||
// }
|
||||
// })
|
||||
const simulatedTxns = []
|
||||
Object.values(this.txs).forEach(tx => {
|
||||
Object.values(this.pool.txs).forEach(tx => {
|
||||
if (Math.random() < 0.5) {
|
||||
simulatedTxns.push({
|
||||
version: tx.version,
|
||||
@ -150,7 +158,7 @@ export default class TxController {
|
||||
txn_count: 20,
|
||||
txns: simulatedTxns
|
||||
}))
|
||||
}, 2500)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
simulateDumpTx (n) {
|
||||
@ -160,22 +168,25 @@ export default class TxController {
|
||||
time: Date.now(),
|
||||
id: `simulated_${i}_${Math.random()}`,
|
||||
value: Math.floor(Math.random() * 100000)
|
||||
}))
|
||||
}, this.vertexArray))
|
||||
}
|
||||
}
|
||||
|
||||
clearBlock (id) {
|
||||
if (this.blocks[id]) {
|
||||
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]
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,3 @@
|
||||
import BitcoinTx from '../models/BitcoinTx.js'
|
||||
import BitcoinBlock from '../models/BitcoinBlock.js'
|
||||
import { serverConnected, serverDelay } from '../stores.js'
|
||||
|
||||
class TxStream {
|
||||
@ -88,12 +86,10 @@ class TxStream {
|
||||
} else {
|
||||
const msg = JSON.parse(event.data)
|
||||
if (msg && msg.type === 'txn') {
|
||||
const tx = new BitcoinTx(msg.txn)
|
||||
window.dispatchEvent(new CustomEvent('bitcoin_tx', { detail: tx }))
|
||||
window.dispatchEvent(new CustomEvent('bitcoin_tx', { detail: msg.txn }))
|
||||
} else if (msg && msg.type === 'block') {
|
||||
const block = new BitcoinBlock(msg.block)
|
||||
console.log('Block recieved: ', block)
|
||||
window.dispatchEvent(new CustomEvent('bitcoin_block', { detail: block }))
|
||||
console.log('Block recieved: ', msg.block)
|
||||
window.dispatchEvent(new CustomEvent('bitcoin_block', { detail: msg.block }))
|
||||
} else {
|
||||
console.log('unknown message from websocket: ', msg)
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
import TxView from './TxView.js'
|
||||
|
||||
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.id = id
|
||||
this.vertexArray = vertexArray
|
||||
|
||||
if (inputs && outputs) {
|
||||
this.inputs = inputs
|
||||
@ -20,6 +21,10 @@ export default class BitcoinTx {
|
||||
this.view = new TxView(this)
|
||||
}
|
||||
|
||||
destroy () {
|
||||
if (this.view) this.view.destroy()
|
||||
}
|
||||
|
||||
calcValue () {
|
||||
if (this.outputs && this.outputs.length) {
|
||||
return this.outputs.reduce((acc, output) => {
|
||||
@ -37,7 +42,11 @@ export default class BitcoinTx {
|
||||
if (this.view) this.view.update(update)
|
||||
}
|
||||
|
||||
setPosition (position) {
|
||||
this.position = position
|
||||
}
|
||||
|
||||
getPosition () {
|
||||
if (this.view) return this.view.getPosition()
|
||||
if (this.position) return this.position
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
import TxPoolScene from './TxPoolScene.js'
|
||||
|
||||
export default class TxBlockScene extends TxPoolScene {
|
||||
constructor ({ width, height, unit = 4, padding = 1, layer }) {
|
||||
super({ width, height, unit, padding, layer })
|
||||
constructor ({ width, height, unit = 4, padding = 1, layer, blockId, controller }) {
|
||||
super({ width, height, unit, padding, layer, controller })
|
||||
this.heightLimit = null
|
||||
this.expired = false
|
||||
this.layedOut = false
|
||||
this.blockId = blockId
|
||||
}
|
||||
|
||||
resize ({ width, height, unit, padding }) {
|
||||
@ -48,7 +50,8 @@ export default class TxBlockScene extends TxPoolScene {
|
||||
state: 'ready'
|
||||
})
|
||||
}
|
||||
const flyToBlock = {
|
||||
|
||||
this.updateTx(tx, {
|
||||
display: {
|
||||
position,
|
||||
size: 4,
|
||||
@ -60,33 +63,44 @@ export default class TxBlockScene extends TxPoolScene {
|
||||
},
|
||||
duration: 1500,
|
||||
delay: 0,
|
||||
state: 'flytoblock',
|
||||
next: {
|
||||
display: {},
|
||||
state: 'block'
|
||||
}
|
||||
}
|
||||
if (tx.view.state === 'pool' || tx.view.state === 'colorfade') {
|
||||
state: 'block'
|
||||
})
|
||||
}
|
||||
|
||||
prepareTx (tx, sequence) {
|
||||
const position = this.place(tx.id, sequence)
|
||||
if (!tx.view.initialised) {
|
||||
this.updateTx(tx, {
|
||||
display: {
|
||||
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: {
|
||||
palette: 0,
|
||||
index: 0,
|
||||
alpha: 1
|
||||
}
|
||||
},
|
||||
duration: 1000,
|
||||
duration: 1500,
|
||||
delay: 0,
|
||||
state: 'colorfade',
|
||||
next: flyToBlock
|
||||
})
|
||||
} else {
|
||||
this.updateTx(tx, {
|
||||
...flyToBlock,
|
||||
duration: 2500
|
||||
state: 'ready'
|
||||
})
|
||||
}
|
||||
this.updateTx(tx, {
|
||||
display: {
|
||||
layer: this.layer,
|
||||
color: {
|
||||
palette: 0,
|
||||
index: 0,
|
||||
alpha: 1
|
||||
}
|
||||
},
|
||||
duration: 2000,
|
||||
delay: 0
|
||||
})
|
||||
}
|
||||
|
||||
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 () {
|
||||
this.resize({})
|
||||
super.layoutAll(4)
|
||||
super.layoutAll()
|
||||
}
|
||||
|
||||
initialLayout () {
|
||||
this.prepareAll()
|
||||
setTimeout(() => {
|
||||
this.layoutAll()
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
expire () {
|
||||
@ -114,5 +149,12 @@ export default class TxBlockScene extends TxPoolScene {
|
||||
for (let i = 0; i < ids.length; 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)
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,20 @@
|
||||
export default class TxPoolScene {
|
||||
constructor ({ width, height, unit, padding, layer }) {
|
||||
this.init({ width, height, unit, padding, layer })
|
||||
constructor ({ width, height, unit, padding, layer, controller }) {
|
||||
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.resize({ width, height, unit, padding })
|
||||
this.txs = {}
|
||||
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 = {
|
||||
width: width,
|
||||
height: height,
|
||||
@ -21,6 +27,8 @@ export default class TxPoolScene {
|
||||
}
|
||||
this.scrollRateLimitTimer = null
|
||||
this.initialised = true
|
||||
|
||||
console.log('pool', this)
|
||||
}
|
||||
|
||||
resize ({ width, height, unit, padding }) {
|
||||
@ -37,14 +45,8 @@ export default class TxPoolScene {
|
||||
if (tx) tx.updateView(update)
|
||||
}
|
||||
|
||||
scroll (offset) {
|
||||
if (!this.scrollRateLimitTimer || Date.now() < (this.scrollRateLimitTimer + 10000)) {
|
||||
console.log(`scrolling pool by ${offset}`)
|
||||
this.scrollRateLimitTimer = Date.now()
|
||||
this.doScroll(offset)
|
||||
} else {
|
||||
console.log('scroll recharging')
|
||||
}
|
||||
getPoolHeight () {
|
||||
return this.poolBottom - this.poolTop
|
||||
}
|
||||
|
||||
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) {
|
||||
if (tx.view.initialised) {
|
||||
let currentPosition = tx.getPosition()
|
||||
this.updateTx(tx, {
|
||||
display: {
|
||||
position: {
|
||||
y: currentPosition.y + scrollDistance
|
||||
}
|
||||
}
|
||||
})
|
||||
let currentTargetPosition = tx.getPosition()
|
||||
if (currentTargetPosition) {
|
||||
this.updateTx(tx, {
|
||||
display: {
|
||||
position: {
|
||||
y: currentTargetPosition.y + scrollDistance
|
||||
}
|
||||
},
|
||||
duration: 500,
|
||||
minDuration: 250,
|
||||
adjust: true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
doScroll (offset) {
|
||||
const ids = this.getActiveTxList()
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
this.scrollTx(this.txs[ids[i]], offset)
|
||||
}
|
||||
const ids = this.getTxList()
|
||||
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) {
|
||||
const position = this.place(tx.id, sequence)
|
||||
// if (this.heightLimit && position.y < this.heightLimit) this.scroll(position.y - this.heightLimit)
|
||||
const rawPosition = this.place(tx.id, sequence)
|
||||
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) {
|
||||
this.updateTx(tx, {
|
||||
display: {
|
||||
layer: this.layer,
|
||||
position: {
|
||||
x: position.x,
|
||||
x: scrolledPosition.x,
|
||||
y: 0
|
||||
},
|
||||
size: this.unitWidth,
|
||||
@ -102,8 +149,9 @@ export default class TxPoolScene {
|
||||
this.updateTx(tx, {
|
||||
display: {
|
||||
layer: this.layer,
|
||||
position,
|
||||
size: this.unitWidth,
|
||||
position: scrolledPosition,
|
||||
// size: this.unitWidth,
|
||||
size: this.txSize(tx.value),
|
||||
color: {
|
||||
palette: 0,
|
||||
index: 0,
|
||||
@ -112,60 +160,35 @@ export default class TxPoolScene {
|
||||
},
|
||||
duration: 1500,
|
||||
delay: 0,
|
||||
state: 'entering',
|
||||
next: {
|
||||
display: {
|
||||
color: {
|
||||
palette: 0,
|
||||
index: 1
|
||||
}
|
||||
},
|
||||
duration: 30000,
|
||||
delay: 100,
|
||||
state: 'colorfade',
|
||||
next: {
|
||||
display: {},
|
||||
state: 'pool'
|
||||
state: 'pool'
|
||||
})
|
||||
this.updateTx(tx, {
|
||||
display: {
|
||||
color: {
|
||||
palette: 0,
|
||||
index: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
duration: 30000,
|
||||
delay: 0
|
||||
})
|
||||
} else {
|
||||
const shuffleUpdate = {
|
||||
this.updateTx(tx, {
|
||||
display: {
|
||||
position
|
||||
position: scrolledPosition
|
||||
},
|
||||
duration: 1000,
|
||||
minDuration: 1000,
|
||||
delay: 0,
|
||||
state: 'shuffling',
|
||||
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)
|
||||
// }
|
||||
adjust: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
layoutAll () {
|
||||
this.scene.count = 0
|
||||
this.poolTop = Infinity
|
||||
this.poolBottom = -Infinity
|
||||
let ids = this.getHiddenTxList()
|
||||
for (let i = 0; i < ids.length; 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++) {
|
||||
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) {
|
||||
@ -201,10 +232,16 @@ export default class TxPoolScene {
|
||||
}
|
||||
|
||||
place (id, position) {
|
||||
return {
|
||||
const placement = {
|
||||
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 () {
|
||||
|
@ -1,105 +1,132 @@
|
||||
function interpolateAnimationState (from, to, progress) {
|
||||
const clampedProgress = Math.max(0,Math.min(1,progress))
|
||||
return Object.keys(from).reduce((result, key) => {
|
||||
if (to[key] != null) {
|
||||
result[key] = from[key] + ((to[key] - from[key]) * clampedProgress)
|
||||
}
|
||||
return result
|
||||
}, from)
|
||||
function interpolateAttributeStart(attribute, now, label) {
|
||||
if (attribute.v == 0 || (attribute.t + attribute.d) <= now) {
|
||||
// transition finished, next transition starts from current end state
|
||||
// (clamp to 1)
|
||||
attribute.a = attribute.b
|
||||
attribute.v = 0
|
||||
attribute.d = 0
|
||||
} 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 {
|
||||
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.value = value
|
||||
this.layer = layer
|
||||
this.vertexArray = vertexArray
|
||||
|
||||
this.duration = 0
|
||||
this.v = 0
|
||||
this.start = now
|
||||
this.attributes = {
|
||||
x: { a: position.x, b: position.x, t: now, v: 0, d: 0 },
|
||||
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 = {
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
this.vertexPointer = this.vertexArray.insert(this)
|
||||
|
||||
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,
|
||||
p: palette,
|
||||
c: color,
|
||||
a: alpha
|
||||
}
|
||||
this.to = {
|
||||
...this.from
|
||||
}
|
||||
|
||||
this.compile()
|
||||
}
|
||||
|
||||
update({ now, duration, layer, position, size, palette, color, alpha }) {
|
||||
// Save a copy of the current target display
|
||||
const currentTarget = this.to
|
||||
|
||||
// Check if we're mid-transition
|
||||
const progress = (this.duration && this.start) ? ((now - this.start) / this.duration) : 0
|
||||
if (progress >= 1 || progress <= 0) {
|
||||
// Transition finished or hasn't started:
|
||||
// so next transition starts from the current display state
|
||||
this.from = {
|
||||
...currentTarget
|
||||
for (const key of Object.keys(update)) {
|
||||
// for each non-null attribute:
|
||||
if (update[key] != null) {
|
||||
// calculate current interpolated value, and set as 'from'
|
||||
interpolateAttributeStart(this.attributes[key], now, key)
|
||||
// set 'start' to now
|
||||
this.attributes[key].t = now
|
||||
// if 'adjust' flag set
|
||||
// set 'duration' to Max(remaining time, 'duration')
|
||||
if (!adjust || (duration && this.attributes[key].d == 0)) {
|
||||
this.attributes[key].v = v
|
||||
this.attributes[key].d = duration
|
||||
} else if (minDuration > this.attributes[key].d) {
|
||||
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()
|
||||
}
|
||||
|
||||
compile () {
|
||||
this.vertexData = [
|
||||
this.start,
|
||||
this.layer,
|
||||
this.v,
|
||||
this.from.x,
|
||||
this.from.y,
|
||||
this.to.x,
|
||||
this.to.y,
|
||||
this.from.r,
|
||||
this.to.r,
|
||||
this.from.p,
|
||||
this.to.p,
|
||||
this.from.c,
|
||||
this.to.c,
|
||||
this.from.a,
|
||||
this.to.a,
|
||||
this.attributes.x.a,
|
||||
this.attributes.x.b,
|
||||
this.attributes.x.t,
|
||||
this.attributes.x.v,
|
||||
|
||||
this.attributes.y.a,
|
||||
this.attributes.y.b,
|
||||
this.attributes.y.t,
|
||||
this.attributes.y.v,
|
||||
|
||||
this.attributes.r.a,
|
||||
this.attributes.r.b,
|
||||
this.attributes.r.t,
|
||||
this.attributes.r.v,
|
||||
|
||||
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 () {
|
||||
return {
|
||||
x: this.to.x,
|
||||
y: this.to.y
|
||||
x: this.attributes.x.b,
|
||||
y: this.attributes.y.b
|
||||
}
|
||||
}
|
||||
|
||||
getVertexData () {
|
||||
return this.vertexData
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.vertexArray.remove(this.vertexPointer)
|
||||
this.vertexPointer = null
|
||||
}
|
||||
}
|
||||
|
@ -2,21 +2,28 @@ import TxSprite from './TxSprite.js'
|
||||
import { timeOffset } from '../utils/time.js'
|
||||
|
||||
// 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 {
|
||||
now: start ? start - timeOffset : Date.now() - timeOffset,
|
||||
duration,
|
||||
duration: duration,
|
||||
...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 {
|
||||
constructor ({ id, time, value }) {
|
||||
constructor ({ id, time, value, vertexArray }) {
|
||||
this.id = id
|
||||
this.time = time
|
||||
this.value = value
|
||||
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
|
||||
delay: for queued transitions, how long to wait after current transition
|
||||
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 }) {
|
||||
if (next) {
|
||||
if (this.awaiting) clearTimeout(this.awaiting)
|
||||
this.awaiting = setTimeout(() => {
|
||||
this.update(next)
|
||||
}, (duration || 0) + (next.delay || 0))
|
||||
}
|
||||
|
||||
update ({ display, duration, delay, state, start, adjust }) {
|
||||
this.state = state
|
||||
|
||||
if (!this.initialised) {
|
||||
if (!this.initialised || !this.sprite) {
|
||||
this.initialised = true
|
||||
const update = toSpriteUpdate(display, duration, start)
|
||||
update.id = this.id
|
||||
update.value = this.value
|
||||
this.sprite = new TxSprite(update)
|
||||
this.sprite = new TxSprite(update, this.vertexArray)
|
||||
} else {
|
||||
const update = toSpriteUpdate(display, duration, start)
|
||||
const update = toSpriteUpdate(display, duration, start, adjust)
|
||||
this.sprite.update(update)
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,16 @@
|
||||
varying lowp vec4 vColor;
|
||||
|
||||
attribute float startTime;
|
||||
attribute float zIndex;
|
||||
attribute float speed;
|
||||
attribute vec4 positions;
|
||||
attribute vec2 sizes;
|
||||
attribute vec2 colors;
|
||||
attribute vec2 palettes;
|
||||
attribute vec2 alphas;
|
||||
// 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
|
||||
attribute vec4 posX;
|
||||
attribute vec4 posY;
|
||||
attribute vec4 sizes;
|
||||
attribute vec4 colors;
|
||||
attribute vec4 palettes;
|
||||
attribute vec4 alphas;
|
||||
|
||||
uniform vec2 screenSize;
|
||||
uniform float now;
|
||||
// uniform float opacityTarget; // target opacity for fading points
|
||||
uniform sampler2D colorTexture;
|
||||
|
||||
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() {
|
||||
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);
|
||||
gl_PointSize = mix(sizes.x, sizes.y, delta);
|
||||
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);
|
||||
float colorIndex = interpolateAttribute(colors);
|
||||
float alpha = interpolateAttribute(alphas);
|
||||
if (palettes.y < 1.0) { // texture color
|
||||
vec4 texel = texture2D(colorTexture, vec2(colorIndex, 0.0));
|
||||
vColor = vec4(texel.rgb, alpha);
|
||||
|
@ -296,3 +296,4 @@ function createCounter () {
|
||||
}
|
||||
|
||||
export const txQueueLength = createCounter()
|
||||
export const txCount = createCounter()
|
||||
|
112
src/utils/memory.js
Normal file
112
src/utils/memory.js
Normal 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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user