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
// 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>

View File

@ -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>

View File

@ -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]
}
}

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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 () {

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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);

View File

@ -296,3 +296,4 @@ function 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
}
}