I built a fractal clock using canvas 2dContext, but found I was getting performance issues when the layers increased above 6. In an attempt to improve performance and increase the layer limit I rebuilt the same clock using PIXI.js.
The problem I have is that the performance improvement was minimal - on my desktop it went from a maximum of 6 layers with 2dContext to 7 layers with PIXI.
Is there anything obviously wrong with how I'm approaching the PIXI solution that could be limiting its performance?
I suspect the best solution is to move all of the calculations into shaders, and start from boiler plate webgl.
Code to save clicking links:
const config = {
numLevels: 7, // bottleneck here, higher means poor performance
levelAngleOffset: 0,
levelScale: .7,
levelOpacityMultiplier: .7,
levelHueRotation: 40,
initialRadius: .4,
initialOpacity: 1,
initialHue: 0,
dialOpacity: .1,
handOpacity: 1,
secondHandLength: .9,
secondHandWidth: .015,
minuteHandLength: .8,
minuteHandWidth: .03,
hourHandLength: .6,
hourHandWidth: .05,
// set by code
width: 0,
height: 0,
};
// https://stackoverflow.com/a/44134328/3282374
function hslToHex(h, s, l) {
l /= 100;
const a = s * Math.min(l, 1 - l) / 100;
const f = n => {
const k = (n + h / 30) % 12;
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color).toString(16).padStart(2, '0'); // convert to Hex and prefix "0" if needed
};
return `0x${f(0)}${f(8)}${f(4)}`;
}
class Hand extends PIXI.Graphics {
constructor(x,y,length, rotation, width, opacity) {
super();
this.length = length;
this.lineStyle({
width: width,
color: 0xFFFFFF,
alpha: config.handOpacity * opacity,
cap: PIXI.LINE_CAP.ROUND
});
this.moveTo(0, 0);
this.lineTo(0, -length);
this.rotation = rotation;
this.x = x;
this.y = y;
this.endFill();
}
setClock(clock) {
this.hasClock = true;
this.clock = clock;
clock.x = 0;
clock.y = this.length;
this.addChild(clock);
}
update(rotation, angles) {
this.rotation = rotation;
if (this.hasClock) {
this.clock.update(angles, 0, -this.length, rotation);
}
}
}
class Clock extends PIXI.Graphics {
constructor(cx, cy, radius, rotation, hue, opacity) {
super();
this.lineStyle(0);
this.beginFill(hslToHex(hue, 100, 50), opacity * config.dialOpacity);
this.drawCircle(0, 0, radius);
this.endFill();
this.x = cx;
this.y = cy;
this.rotation = rotation;
this.hourHand = new Hand(0, 0, config.hourHandLength * radius, 0, config.hourHandWidth * radius, opacity);
this.minuteHand = new Hand(0, 0, config.minuteHandLength * radius, 0, config.minuteHandWidth * radius, opacity);
this.secondHand = new Hand(0, 0, config.secondHandLength * radius, 0, config.secondHandWidth * radius, opacity);
this.addChild(this.hourHand, this.minuteHand, this.secondHand);
}
setChildClocks(hour, minute, second) {
this.hourHand.setClock(hour);
this.minuteHand.setClock(minute);
this.secondHand.setClock(second);
}
update(angles, cx, cy, rotation) {
this.x = cx;
this.y = cy;
this.rotation = rotation;
this.hourHand.update(angles.hour, angles);
this.minuteHand.update(angles.minute, angles);
this.secondHand.update(angles.second, angles);
}
}
function getTimeAngles() {
const time = new Date();
const millisecond = time.getMilliseconds();
const second = time.getSeconds() + millisecond / 1000;
const minute = time.getMinutes() + second / 60;
const hour = time.getHours() % 12 + minute / 60;
const hourAngle = Math.PI * 2 * hour / 12;
const minuteAngle = Math.PI * 2 * minute / 60;
const secondAngle = Math.PI * 2 * second / 60;
return {
hour: hourAngle,
minute: minuteAngle,
second: secondAngle
};
}
let clock;
function initClock() {
const center = Math.min(config.width, config.height) / 2;
clock = new Clock(
center,
center,
center * config.initialRadius,
Math.PI / 2,
config.initialHue,
config.initialOpacity);
let level = 0;
let clocks = [clock];
while (level < config.numLevels) {
level++;
const nextClocks = [];
for (const parent of clocks) {
const children = [];
for (var i = 0; i < 3; i++) {
const child = new Clock(
center,
center,
center * config.initialRadius * config.levelScale**level,
0,
config.initialHue + config.levelHueRotation * level,
config.initialOpacity * config.levelOpacityMultiplier ** level);
children.push(child);
}
parent.setChildClocks(...children);
nextClocks.push(...children);
}
clocks = nextClocks;
}
}
function step() {
const angles = getTimeAngles();
clock.update(angles, config.width/2, config.height/2, 0);
}
function init() {
PIXI.utils.skipHello();
const app = new PIXI.Application({ antialias: true, transparent: true });
document.body.appendChild(app.view);
const canvas = document.querySelector('canvas');
const resize = () => {
const {width, height } = canvas.getBoundingClientRect();
config.width = width;
config.height = height;
app.renderer.resize(width, height);
}
window.addEventListener('resize', resize);
resize();
initClock();
app.stage.addChild(clock);
app.ticker.add(step);
}
init();
html, body {
padding: 0;
margin: 0;
width: 100%;
height: 100%;
}
body {
display: flex;
align-items: center;
justify-content: center;
background: #222;
}
canvas {
width: 99vw;
height: 99vh;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/5.3.9/pixi.min.js"></script>