Skip to content

Instantly share code, notes, and snippets.

@Ljzn
Created February 21, 2019 06:43
Show Gist options
  • Save Ljzn/72373cd644620476dc2203a5943d9d50 to your computer and use it in GitHub Desktop.
Save Ljzn/72373cd644620476dc2203a5943d9d50 to your computer and use it in GitHub Desktop.
JS Bin // source https://jsbin.com/fiwuted
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<canvas id="canvas"></canvas>
<button id="start">START</button>
<button id="replay">REPLAY</button>
<button id="clear">CLEAR</button>
<p>put and connect to make a GRAPH, then click START</p>
<script id="jsbin-javascript">
// CONSTS
const CWidth = 400
const CHeight = 400
const VRadius = 20
const ColorPlatte = ['orange', 'yellow', 'green', 'cyan', 'blue', 'purple']
const canvas = document.getElementById('canvas')
const BClear = document.getElementById('clear')
const BStart = document.getElementById('start')
const BReplay = document.getElementById('replay')
const ctx = canvas.getContext('2d')
canvas.width = CWidth
canvas.height = CHeight
const ROUNDTIME = 10
const ReROUNDTIME = 3000
// fake STRUCTS
const Point = (event) => {
return {x: event.clientX, y: event.clientY}
}
const Edge = (v1, v2) => {
return {v1: v1, v2: v2}
}
const Msg = (type, data, timeStamp) => {
return {type: type, data: data, timeStamp: timeStamp}
}
// HELPER FUNCTIONS
const drawVertex = (v) => {
let color = v.selected ? 'black' : v.color
let loc = v.location
ctx.fillStyle = color
ctx.beginPath()
ctx.arc(loc.x, loc.y, VRadius, 0, 2 * Math.PI, 0)
ctx.fill()
}
const drawEdge = (e) => {
let a = e.v1.location
let b = e.v2.location
ctx.beginPath()
ctx.lineWidth = 3
ctx.moveTo(a.x, a.y)
ctx.lineTo(b.x, b.y)
ctx.stroke()
}
const drawRound = (r) => {
ctx.fillStyle = 'cyan'
ctx.font = '28px serif'
ctx.fillText('ROUND: ' + r, 20, 40)
}
const drawMsgText = (p, text) => {
ctx.fillStyle = 'purple'
ctx.font = '14px serif'
ctx.fillText(text, p.x, p.y)
}
const drawMsg = (from, to, text, beginTime) => {
let time = new Date()
let currentTime = time.getTime()
let t = currentTime - beginTime
if(t < ReROUNDTIME) {
let getPosition = (a, b) => {
return {
x: (b.x - a.x) / ReROUNDTIME * t + a.x,
y: (b.y - a.y) / ReROUNDTIME * t + a.y
}
}
let p = getPosition(from.location, to.location)
drawMsgText(p, text)
}
}
// ACTORS
class GraphMaker {
constructor() {
this.vertexs = []
this.edges = []
this.timer = false
this.timeStamp = 0
this.logQ = []
}
start() {
this.timer = setInterval(() => {
this.round()
}, ROUNDTIME)
}
putVertex(loc) {
let id = this.vertexs.length
let v = new Vertex(id, loc)
this.vertexs.push(v)
drawVertex(v)
}
round() {
if(this.timeStamp !== 50) {
this.timeStamp += 1
this.vertexs.forEach((c) => {
c.round(this.timeStamp)
})
} else {
if(this.timer) {
clearInterval(this.timer)
}
}
}
inVertex(p) {
return this.vertexs.find((c) => {
return c.inField(p)
})
}
addEdge(v1, v2) {
let e = this.edges.find((e) => {
return (e.v1 === v1 && e.v2 === v2) || (e.v1 === v2 && e.v2 === v1)
})
if(!e) {
let nEdge = Edge(v1, v2)
this.edges.push(nEdge)
v1.addNeighbor(v2)
v2.addNeighbor(v1)
drawEdge(nEdge)
}
}
render() {
ctx.clearRect(0, 0, CWidth, CHeight)
drawRound(this.timeStamp)
this.vertexs.forEach((v) => drawVertex(v))
this.edges.forEach((e) => drawEdge(e))
this.logQ.forEach((l) => {
drawMsg(l.from, l.to, l.msg.data, l.reTime)
})
}
clear() {
ctx.clearRect(0, 0, CWidth, CHeight)
this.vertexs = []
this.edges = []
this.logQ = []
if(this.timer) {
clearInterval(this.timer)
}
this.timer = false
this.timeStamp = 0
}
resetColor() {
this.vertexs.forEach((v) => v.color = 'gray')
}
}
const graphMaker = new GraphMaker()
class LogCenter {
constructor() {
this.db = []
}
log(from, to, msg) {
this.db.push({
from: from,
to: to,
msg: msg,
reTime: false
})
}
clear() {
this.db = []
}
}
const logCenter = new LogCenter()
class Visualizer {
constructor() {
this.timer = false
}
clear() {
if(this.timer) {
clearTimeout(this.timer)
}
this.timer = false
}
replay() {
let round = 0
graphMaker.resetColor()
graphMaker.logQ = []
if(this.timer) {
clearTimeout(this.timer)
}
let roundState = (ts) => {
if(ts < 50) {
ts += 1
graphMaker.timeStamp = ts
logCenter.db.forEach((log) => {
if(log.msg.timeStamp === ts) {
if(log.msg.type === 'color') {
log.from.color = log.msg.data
}
let time = new Date()
log.reTime = time.getTime()
graphMaker.logQ.push(log)
}
})
this.timer = setTimeout(() => roundState(ts), ReROUNDTIME)
}
}
roundState(round)
}
}
const visualizer = new Visualizer()
class Vertex {
constructor(id, location) {
this.id = id
this.location = location
this.color = 'gray'
this.neighbors = []
this.freeColors = ColorPlatte
this.selected = false
this.state = 'id'
this.mailBox = []
}
select() {
this.selected = true
}
free() {
this.selected = false
}
round(ts) {
let nMsg
let localCompute = () => {
let consumeColor = (c) => {
this.freeColors = this.freeColors.filter((x) => x !== c)
}
let usedColors = this.msgOfType('color').map((m) => {
return m.msg.data
})
usedColors.forEach((c) => consumeColor(c))
let msg = (type, data) => {
return Msg(type, data, ts)
}
switch(this.state) {
case 'id':
this.state = 'deciding'
nMsg = msg('id', this.id)
break
case 'deciding':
if(this.isBiggerUncolored()) {
nMsg = msg('plain', 'undecided')
} else {
this.setColor()
this.state = 'colored'
nMsg = msg('color', this.color)
}
break
case 'colored':
break
}
this.mailBox = []
if(nMsg) {
this.neighbors.forEach((v) => {
this.send(v, nMsg)
})}
}
setTimeout(localCompute, ROUNDTIME * 0.5)
}
send(d, msg) {
d.recv(this, msg)
logCenter.log(this, d, msg)
}
recv(from, msg) {
this.mailBox.push({from: from, msg: msg})
}
setColor() {
this.color = this.freeColors[0]
}
addNeighbor(v) {
this.neighbors.push(v)
}
isBiggerUncolored() {
return !!this.mailBox.find((m) => {
return (m.from.id > this.id) && (m.msg.type !== 'color')
})
}
inField(p) {
let inCircle = (c, p) => {
let d2 = (c.x-p.x)*(c.x-p.x)+(c.y-p.y)*(c.y-p.y)
// field is a bit larger than size
return d2 < (4*VRadius*VRadius)
}
return inCircle(this.location, p)
}
msgOfType(type) {
return this.mailBox.filter((m) => {
return m.msg.type === type
})
}
}
class UserAgent {
constructor() {
this.state = 'putVertex'
this.currentV = false
}
handleEvent(event) {
let g = graphMaker
let p = Point(event)
let v = g.inVertex(p)
switch (this.state) {
case 'putVertex':
if(v) {
v.select()
this.currentV = v
this.state = 'chooseOne'
} else {
g.putVertex(p)
}
break
case 'chooseOne':
if(v) {
if(v !== this.currentV) {
this.currentV.free()
g.addEdge(v, this.currentV)
}
} else {
this.currentV.free()
}
this.state = 'putVertex'
break
}
}
clear() {
this.state = 'putVertex'
}
}
const userAgent = new UserAgent()
canvas.addEventListener('click', event => {
userAgent.handleEvent(event)
})
// Clear all
BClear.addEventListener('click', event => {
graphMaker.clear()
userAgent.clear()
logCenter.clear()
visualizer.clear()
})
// Start algorithm
BStart.addEventListener('click', e => {
graphMaker.start()
})
BReplay.addEventListener('click', e => {
visualizer.replay()
})
const draw = () => {
graphMaker.render()
window.requestAnimationFrame(draw)
}
window.requestAnimationFrame(draw)
</script>
<script id="jsbin-source-javascript" type="text/javascript">// CONSTS
const CWidth = 400
const CHeight = 400
const VRadius = 20
const ColorPlatte = ['orange', 'yellow', 'green', 'cyan', 'blue', 'purple']
const canvas = document.getElementById('canvas')
const BClear = document.getElementById('clear')
const BStart = document.getElementById('start')
const BReplay = document.getElementById('replay')
const ctx = canvas.getContext('2d')
canvas.width = CWidth
canvas.height = CHeight
const ROUNDTIME = 10
const ReROUNDTIME = 3000
// fake STRUCTS
const Point = (event) => {
return {x: event.clientX, y: event.clientY}
}
const Edge = (v1, v2) => {
return {v1: v1, v2: v2}
}
const Msg = (type, data, timeStamp) => {
return {type: type, data: data, timeStamp: timeStamp}
}
// HELPER FUNCTIONS
const drawVertex = (v) => {
let color = v.selected ? 'black' : v.color
let loc = v.location
ctx.fillStyle = color
ctx.beginPath()
ctx.arc(loc.x, loc.y, VRadius, 0, 2 * Math.PI, 0)
ctx.fill()
}
const drawEdge = (e) => {
let a = e.v1.location
let b = e.v2.location
ctx.beginPath()
ctx.lineWidth = 3
ctx.moveTo(a.x, a.y)
ctx.lineTo(b.x, b.y)
ctx.stroke()
}
const drawRound = (r) => {
ctx.fillStyle = 'cyan'
ctx.font = '28px serif'
ctx.fillText('ROUND: ' + r, 20, 40)
}
const drawMsgText = (p, text) => {
ctx.fillStyle = 'purple'
ctx.font = '14px serif'
ctx.fillText(text, p.x, p.y)
}
const drawMsg = (from, to, text, beginTime) => {
let time = new Date()
let currentTime = time.getTime()
let t = currentTime - beginTime
if(t < ReROUNDTIME) {
let getPosition = (a, b) => {
return {
x: (b.x - a.x) / ReROUNDTIME * t + a.x,
y: (b.y - a.y) / ReROUNDTIME * t + a.y
}
}
let p = getPosition(from.location, to.location)
drawMsgText(p, text)
}
}
// ACTORS
class GraphMaker {
constructor() {
this.vertexs = []
this.edges = []
this.timer = false
this.timeStamp = 0
this.logQ = []
}
start() {
this.timer = setInterval(() => {
this.round()
}, ROUNDTIME)
}
putVertex(loc) {
let id = this.vertexs.length
let v = new Vertex(id, loc)
this.vertexs.push(v)
drawVertex(v)
}
round() {
if(this.timeStamp !== 50) {
this.timeStamp += 1
this.vertexs.forEach((c) => {
c.round(this.timeStamp)
})
} else {
if(this.timer) {
clearInterval(this.timer)
}
}
}
inVertex(p) {
return this.vertexs.find((c) => {
return c.inField(p)
})
}
addEdge(v1, v2) {
let e = this.edges.find((e) => {
return (e.v1 === v1 && e.v2 === v2) || (e.v1 === v2 && e.v2 === v1)
})
if(!e) {
let nEdge = Edge(v1, v2)
this.edges.push(nEdge)
v1.addNeighbor(v2)
v2.addNeighbor(v1)
drawEdge(nEdge)
}
}
render() {
ctx.clearRect(0, 0, CWidth, CHeight)
drawRound(this.timeStamp)
this.vertexs.forEach((v) => drawVertex(v))
this.edges.forEach((e) => drawEdge(e))
this.logQ.forEach((l) => {
drawMsg(l.from, l.to, l.msg.data, l.reTime)
})
}
clear() {
ctx.clearRect(0, 0, CWidth, CHeight)
this.vertexs = []
this.edges = []
this.logQ = []
if(this.timer) {
clearInterval(this.timer)
}
this.timer = false
this.timeStamp = 0
}
resetColor() {
this.vertexs.forEach((v) => v.color = 'gray')
}
}
const graphMaker = new GraphMaker()
class LogCenter {
constructor() {
this.db = []
}
log(from, to, msg) {
this.db.push({
from: from,
to: to,
msg: msg,
reTime: false
})
}
clear() {
this.db = []
}
}
const logCenter = new LogCenter()
class Visualizer {
constructor() {
this.timer = false
}
clear() {
if(this.timer) {
clearTimeout(this.timer)
}
this.timer = false
}
replay() {
let round = 0
graphMaker.resetColor()
graphMaker.logQ = []
if(this.timer) {
clearTimeout(this.timer)
}
let roundState = (ts) => {
if(ts < 50) {
ts += 1
graphMaker.timeStamp = ts
logCenter.db.forEach((log) => {
if(log.msg.timeStamp === ts) {
if(log.msg.type === 'color') {
log.from.color = log.msg.data
}
let time = new Date()
log.reTime = time.getTime()
graphMaker.logQ.push(log)
}
})
this.timer = setTimeout(() => roundState(ts), ReROUNDTIME)
}
}
roundState(round)
}
}
const visualizer = new Visualizer()
class Vertex {
constructor(id, location) {
this.id = id
this.location = location
this.color = 'gray'
this.neighbors = []
this.freeColors = ColorPlatte
this.selected = false
this.state = 'id'
this.mailBox = []
}
select() {
this.selected = true
}
free() {
this.selected = false
}
round(ts) {
let nMsg
let localCompute = () => {
let consumeColor = (c) => {
this.freeColors = this.freeColors.filter((x) => x !== c)
}
let usedColors = this.msgOfType('color').map((m) => {
return m.msg.data
})
usedColors.forEach((c) => consumeColor(c))
let msg = (type, data) => {
return Msg(type, data, ts)
}
switch(this.state) {
case 'id':
this.state = 'deciding'
nMsg = msg('id', this.id)
break
case 'deciding':
if(this.isBiggerUncolored()) {
nMsg = msg('plain', 'undecided')
} else {
this.setColor()
this.state = 'colored'
nMsg = msg('color', this.color)
}
break
case 'colored':
break
}
this.mailBox = []
if(nMsg) {
this.neighbors.forEach((v) => {
this.send(v, nMsg)
})}
}
setTimeout(localCompute, ROUNDTIME * 0.5)
}
send(d, msg) {
d.recv(this, msg)
logCenter.log(this, d, msg)
}
recv(from, msg) {
this.mailBox.push({from: from, msg: msg})
}
setColor() {
this.color = this.freeColors[0]
}
addNeighbor(v) {
this.neighbors.push(v)
}
isBiggerUncolored() {
return !!this.mailBox.find((m) => {
return (m.from.id > this.id) && (m.msg.type !== 'color')
})
}
inField(p) {
let inCircle = (c, p) => {
let d2 = (c.x-p.x)*(c.x-p.x)+(c.y-p.y)*(c.y-p.y)
// field is a bit larger than size
return d2 < (4*VRadius*VRadius)
}
return inCircle(this.location, p)
}
msgOfType(type) {
return this.mailBox.filter((m) => {
return m.msg.type === type
})
}
}
class UserAgent {
constructor() {
this.state = 'putVertex'
this.currentV = false
}
handleEvent(event) {
let g = graphMaker
let p = Point(event)
let v = g.inVertex(p)
switch (this.state) {
case 'putVertex':
if(v) {
v.select()
this.currentV = v
this.state = 'chooseOne'
} else {
g.putVertex(p)
}
break
case 'chooseOne':
if(v) {
if(v !== this.currentV) {
this.currentV.free()
g.addEdge(v, this.currentV)
}
} else {
this.currentV.free()
}
this.state = 'putVertex'
break
}
}
clear() {
this.state = 'putVertex'
}
}
const userAgent = new UserAgent()
canvas.addEventListener('click', event => {
userAgent.handleEvent(event)
})
// Clear all
BClear.addEventListener('click', event => {
graphMaker.clear()
userAgent.clear()
logCenter.clear()
visualizer.clear()
})
// Start algorithm
BStart.addEventListener('click', e => {
graphMaker.start()
})
BReplay.addEventListener('click', e => {
visualizer.replay()
})
const draw = () => {
graphMaker.render()
window.requestAnimationFrame(draw)
}
window.requestAnimationFrame(draw)
</script></body>
</html>
// CONSTS
const CWidth = 400
const CHeight = 400
const VRadius = 20
const ColorPlatte = ['orange', 'yellow', 'green', 'cyan', 'blue', 'purple']
const canvas = document.getElementById('canvas')
const BClear = document.getElementById('clear')
const BStart = document.getElementById('start')
const BReplay = document.getElementById('replay')
const ctx = canvas.getContext('2d')
canvas.width = CWidth
canvas.height = CHeight
const ROUNDTIME = 10
const ReROUNDTIME = 3000
// fake STRUCTS
const Point = (event) => {
return {x: event.clientX, y: event.clientY}
}
const Edge = (v1, v2) => {
return {v1: v1, v2: v2}
}
const Msg = (type, data, timeStamp) => {
return {type: type, data: data, timeStamp: timeStamp}
}
// HELPER FUNCTIONS
const drawVertex = (v) => {
let color = v.selected ? 'black' : v.color
let loc = v.location
ctx.fillStyle = color
ctx.beginPath()
ctx.arc(loc.x, loc.y, VRadius, 0, 2 * Math.PI, 0)
ctx.fill()
}
const drawEdge = (e) => {
let a = e.v1.location
let b = e.v2.location
ctx.beginPath()
ctx.lineWidth = 3
ctx.moveTo(a.x, a.y)
ctx.lineTo(b.x, b.y)
ctx.stroke()
}
const drawRound = (r) => {
ctx.fillStyle = 'cyan'
ctx.font = '28px serif'
ctx.fillText('ROUND: ' + r, 20, 40)
}
const drawMsgText = (p, text) => {
ctx.fillStyle = 'purple'
ctx.font = '14px serif'
ctx.fillText(text, p.x, p.y)
}
const drawMsg = (from, to, text, beginTime) => {
let time = new Date()
let currentTime = time.getTime()
let t = currentTime - beginTime
if(t < ReROUNDTIME) {
let getPosition = (a, b) => {
return {
x: (b.x - a.x) / ReROUNDTIME * t + a.x,
y: (b.y - a.y) / ReROUNDTIME * t + a.y
}
}
let p = getPosition(from.location, to.location)
drawMsgText(p, text)
}
}
// ACTORS
class GraphMaker {
constructor() {
this.vertexs = []
this.edges = []
this.timer = false
this.timeStamp = 0
this.logQ = []
}
start() {
this.timer = setInterval(() => {
this.round()
}, ROUNDTIME)
}
putVertex(loc) {
let id = this.vertexs.length
let v = new Vertex(id, loc)
this.vertexs.push(v)
drawVertex(v)
}
round() {
if(this.timeStamp !== 50) {
this.timeStamp += 1
this.vertexs.forEach((c) => {
c.round(this.timeStamp)
})
} else {
if(this.timer) {
clearInterval(this.timer)
}
}
}
inVertex(p) {
return this.vertexs.find((c) => {
return c.inField(p)
})
}
addEdge(v1, v2) {
let e = this.edges.find((e) => {
return (e.v1 === v1 && e.v2 === v2) || (e.v1 === v2 && e.v2 === v1)
})
if(!e) {
let nEdge = Edge(v1, v2)
this.edges.push(nEdge)
v1.addNeighbor(v2)
v2.addNeighbor(v1)
drawEdge(nEdge)
}
}
render() {
ctx.clearRect(0, 0, CWidth, CHeight)
drawRound(this.timeStamp)
this.vertexs.forEach((v) => drawVertex(v))
this.edges.forEach((e) => drawEdge(e))
this.logQ.forEach((l) => {
drawMsg(l.from, l.to, l.msg.data, l.reTime)
})
}
clear() {
ctx.clearRect(0, 0, CWidth, CHeight)
this.vertexs = []
this.edges = []
this.logQ = []
if(this.timer) {
clearInterval(this.timer)
}
this.timer = false
this.timeStamp = 0
}
resetColor() {
this.vertexs.forEach((v) => v.color = 'gray')
}
}
const graphMaker = new GraphMaker()
class LogCenter {
constructor() {
this.db = []
}
log(from, to, msg) {
this.db.push({
from: from,
to: to,
msg: msg,
reTime: false
})
}
clear() {
this.db = []
}
}
const logCenter = new LogCenter()
class Visualizer {
constructor() {
this.timer = false
}
clear() {
if(this.timer) {
clearTimeout(this.timer)
}
this.timer = false
}
replay() {
let round = 0
graphMaker.resetColor()
graphMaker.logQ = []
if(this.timer) {
clearTimeout(this.timer)
}
let roundState = (ts) => {
if(ts < 50) {
ts += 1
graphMaker.timeStamp = ts
logCenter.db.forEach((log) => {
if(log.msg.timeStamp === ts) {
if(log.msg.type === 'color') {
log.from.color = log.msg.data
}
let time = new Date()
log.reTime = time.getTime()
graphMaker.logQ.push(log)
}
})
this.timer = setTimeout(() => roundState(ts), ReROUNDTIME)
}
}
roundState(round)
}
}
const visualizer = new Visualizer()
class Vertex {
constructor(id, location) {
this.id = id
this.location = location
this.color = 'gray'
this.neighbors = []
this.freeColors = ColorPlatte
this.selected = false
this.state = 'id'
this.mailBox = []
}
select() {
this.selected = true
}
free() {
this.selected = false
}
round(ts) {
let nMsg
let localCompute = () => {
let consumeColor = (c) => {
this.freeColors = this.freeColors.filter((x) => x !== c)
}
let usedColors = this.msgOfType('color').map((m) => {
return m.msg.data
})
usedColors.forEach((c) => consumeColor(c))
let msg = (type, data) => {
return Msg(type, data, ts)
}
switch(this.state) {
case 'id':
this.state = 'deciding'
nMsg = msg('id', this.id)
break
case 'deciding':
if(this.isBiggerUncolored()) {
nMsg = msg('plain', 'undecided')
} else {
this.setColor()
this.state = 'colored'
nMsg = msg('color', this.color)
}
break
case 'colored':
break
}
this.mailBox = []
if(nMsg) {
this.neighbors.forEach((v) => {
this.send(v, nMsg)
})}
}
setTimeout(localCompute, ROUNDTIME * 0.5)
}
send(d, msg) {
d.recv(this, msg)
logCenter.log(this, d, msg)
}
recv(from, msg) {
this.mailBox.push({from: from, msg: msg})
}
setColor() {
this.color = this.freeColors[0]
}
addNeighbor(v) {
this.neighbors.push(v)
}
isBiggerUncolored() {
return !!this.mailBox.find((m) => {
return (m.from.id > this.id) && (m.msg.type !== 'color')
})
}
inField(p) {
let inCircle = (c, p) => {
let d2 = (c.x-p.x)*(c.x-p.x)+(c.y-p.y)*(c.y-p.y)
// field is a bit larger than size
return d2 < (4*VRadius*VRadius)
}
return inCircle(this.location, p)
}
msgOfType(type) {
return this.mailBox.filter((m) => {
return m.msg.type === type
})
}
}
class UserAgent {
constructor() {
this.state = 'putVertex'
this.currentV = false
}
handleEvent(event) {
let g = graphMaker
let p = Point(event)
let v = g.inVertex(p)
switch (this.state) {
case 'putVertex':
if(v) {
v.select()
this.currentV = v
this.state = 'chooseOne'
} else {
g.putVertex(p)
}
break
case 'chooseOne':
if(v) {
if(v !== this.currentV) {
this.currentV.free()
g.addEdge(v, this.currentV)
}
} else {
this.currentV.free()
}
this.state = 'putVertex'
break
}
}
clear() {
this.state = 'putVertex'
}
}
const userAgent = new UserAgent()
canvas.addEventListener('click', event => {
userAgent.handleEvent(event)
})
// Clear all
BClear.addEventListener('click', event => {
graphMaker.clear()
userAgent.clear()
logCenter.clear()
visualizer.clear()
})
// Start algorithm
BStart.addEventListener('click', e => {
graphMaker.start()
})
BReplay.addEventListener('click', e => {
visualizer.replay()
})
const draw = () => {
graphMaker.render()
window.requestAnimationFrame(draw)
}
window.requestAnimationFrame(draw)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment