Skip to content

Commit

Permalink
Improve dendraclock performance
Browse files Browse the repository at this point in the history
  • Loading branch information
Barafu committed Oct 21, 2024
1 parent f309987 commit e4b649e
Showing 1 changed file with 150 additions and 88 deletions.
238 changes: 150 additions & 88 deletions src/dreams/dendraclock.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
let _performance_text: string = "Not set";

class Hand {
startX: number;
startY: number;
length: number;
angle: number;
endX: number = -1000;
endY: number = -1000;

constructor(startX: number, startY: number, length: number, angle: number) {
this.startX = startX;
Expand All @@ -19,10 +23,8 @@ class Hand {
const deltaY = this.length * Math.sin(angle);

// Calculate end point
const endX = this.startX + deltaX;
const endY = this.startY + deltaY; // Subtract because y-axis is inverted in most computer graphics systems

return { x: endX, y: endY };
this.endX = this.startX + deltaX;
this.endY = this.startY + deltaY; // Subtract because y-axis is inverted in most computer graphics systems
}

rotateClockwise(angleInRadians: number) {
Expand Down Expand Up @@ -79,44 +81,18 @@ class AnalogClock {
);
}

draw(ctx: CanvasRenderingContext2D) {
const arm_width =
this.settings.START_LINE_WIDTH *
Math.pow(this.settings.WIDTH_FACTOR, this.current_depth);
const transparency_factor = Math.pow(
this.settings.LUMINANCE_FACTOR,
this.current_depth - 1,
);
const color = `rgba(255, 255, 255, ${transparency_factor})`;
ctx.lineWidth = arm_width;
ctx.lineCap = "round";
ctx.strokeStyle = color;

if (this.current_depth == 1) {
const hourEndPoint = this.hourHand.calculateEndPoint();
ctx.beginPath();
ctx.moveTo(this.centerX, this.centerY);
ctx.lineTo(hourEndPoint.x, hourEndPoint.y);
ctx.stroke();
}

const minuteEndPoint = this.minuteHand.calculateEndPoint();
ctx.beginPath();
ctx.moveTo(this.centerX, this.centerY);
ctx.lineTo(minuteEndPoint.x, minuteEndPoint.y);
ctx.stroke();

const secondEndPoint = this.secondHand.calculateEndPoint();
ctx.beginPath();
ctx.moveTo(this.centerX, this.centerY);
ctx.lineTo(secondEndPoint.x, secondEndPoint.y);
ctx.stroke();
calculateEndPoints() {
this.hourHand.calculateEndPoint();
this.minuteHand.calculateEndPoint();
this.secondHand.calculateEndPoint();
}

rotateClockwise(angleInRadians: number) {
this.hourHand.rotateClockwise(angleInRadians);
this.minuteHand.rotateClockwise(angleInRadians);
this.secondHand.rotateClockwise(angleInRadians);

this.calculateEndPoints();
}

rotateToHour(hour_angle: number) {
Expand All @@ -129,69 +105,109 @@ class AnalogClock {
class DendraClockPersistentOptions {
ZOOM = 0.25;
START_LINE_WIDTH = 10;
DEPTH = 9;
DEPTH = 8;
LENGTH_FACTOR = 0.9;
LUMINANCE_FACTOR = 0.9;
WIDTH_FACTOR = 0.7;
START_ARM_LENGTH = 150;
}

class ClockTask {
x: number;
y: number;
depth: number;
rotation: number;
constructor(x: number, y: number, depth: number, rotation: number) {
this.x = x;
this.y = y;
this.depth = depth;
this.rotation = rotation;
}
}

export function dendraClock(canvas: HTMLCanvasElement) {
var startTime = performance.now();
const settings = new DendraClockPersistentOptions();
const ctx = canvas.getContext("2d")!;
ctx.globalCompositeOperation = "destination-over";
const now = new Date();

// Prepare hands storage
let hands_map: Map<number, Hand[]> = new Map();
for (let i = 0; i <= settings.DEPTH; i++) {
hands_map.set(i, []);
}
let ctx = canvas.getContext("2d")!;
ctx.clearRect(0, 0, canvas.width, canvas.height);
dendra_clock_recursive(
settings,
now,
ctx,
0,
canvas.width / 2,
canvas.height / 2,
0.0,
);
}

function dendra_clock_recursive(
settings: DendraClockPersistentOptions,
now: Date,
ctx: CanvasRenderingContext2D,
current_depth: number,
x: number,
y: number,
extra_rotation: number,
) {
if (current_depth == settings.DEPTH) return;
current_depth++;
const clock = new AnalogClock(now, x, y, current_depth, settings);
if (current_depth != 1) {
clock.rotateToHour(extra_rotation);
// ===== Calculate stage =====
// Calculate all arms positions and save them to hands_map
const now = new Date();
let clock_tasks: ClockTask[] = [];

clock_tasks.push(new ClockTask(canvas.width / 2, canvas.height / 2, 0, 0));

while (clock_tasks.length > 0) {
const clock_task = clock_tasks.pop()!;
if (clock_task.depth > settings.DEPTH) continue;
const clock = new AnalogClock(now, clock_task.x, clock_task.y, clock_task.depth, settings);

// Store hands in hands array for current depth
let hands_array = hands_map.get(clock_task.depth);
if (hands_array == undefined) {
console.assert(hands_array != undefined);
hands_array = []; // To please TS checker only
}

if (clock_task.depth != 0) {
clock.rotateToHour(clock_task.rotation);
}
else {
clock.calculateEndPoints();
hands_array.push(clock.hourHand);
}
hands_array.push(clock.minuteHand);
hands_array.push(clock.secondHand);

let mt = new ClockTask(clock.minuteHand.endX, clock.minuteHand.endY, clock_task.depth + 1, clock.minuteHand.angle);
let st = new ClockTask(clock.secondHand.endX, clock.secondHand.endY, clock_task.depth + 1, clock.secondHand.angle);
clock_tasks.push(mt);
clock_tasks.push(st);
}


console.assert(hands_map.get(0)?.length == 3);
const calc_timestamp = performance.now();

// ===== Draw stage =====
ctx.globalCompositeOperation = "destination-over";
ctx.lineCap = "round";

// Draw hands, seeting style according to depth
for (let [current_depth, hands_array] of hands_map) {
const arm_width =
settings.START_LINE_WIDTH *
Math.pow(settings.WIDTH_FACTOR, current_depth);
const transparency_factor = Math.pow(
settings.LUMINANCE_FACTOR,
current_depth - 1,
);
const color = `rgba(255, 255, 255, ${transparency_factor})`;
ctx.lineWidth = arm_width;
ctx.strokeStyle = color;

ctx.beginPath();
for (let hand of hands_array) {
ctx.moveTo(hand.startX, hand.startY);
ctx.lineTo(hand.endX, hand.endY);
}
ctx.stroke();
}
clock.draw(ctx);
//const hour_pos = clock.hourHand.calculateEndPoint();
const minute_pos = clock.minuteHand.calculateEndPoint();
const seconds_pos = clock.secondHand.calculateEndPoint();
const minutes_rotation = clock.minuteHand.angle;
const seconds_rotation = clock.secondHand.angle;
dendra_clock_recursive(
settings,
now,
ctx,
current_depth,
minute_pos.x,
minute_pos.y,
minutes_rotation,
);
dendra_clock_recursive(
settings,
now,
ctx,
current_depth,
seconds_pos.x,
seconds_pos.y,
seconds_rotation,
);
// ctx.fillStyle = "yellow";
// ctx.font = "20px serif";
// ctx.fillText(performance_text, 10, 20);
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
const draw_timestamp = performance.now();

performanses(startTime, calc_timestamp, draw_timestamp);
}

function sum_rotations(rotation1: number, rotation2: number) {
Expand All @@ -205,3 +221,49 @@ function sum_rotations(rotation1: number, rotation2: number) {
}
return sum;
}

let stage_calc: number[] = [];
let stage_draw: number[] = [];

/**
* Measures and logs the performance of calculation and drawing stages.
*
* This function calculates the time taken for calculation and drawing stages
* in a rendering process. It stores these times in separate arrays for
* calculation and drawing. Once enough data points are collected, it computes
* the average performance for each stage and logs it to the console.
*
* @param {DOMHighResTimeStamp} start - The timestamp marking the start of the calculation stage.
* @param {DOMHighResTimeStamp} calc - The timestamp marking the end of the calculation stage and start of the drawing stage.
* @param {DOMHighResTimeStamp} draw - The timestamp marking the end of the drawing stage.
*/
function performanses(start: DOMHighResTimeStamp, calc: DOMHighResTimeStamp, draw: DOMHighResTimeStamp) {
const calc_time: number = calc - start;
const draw_time: number = draw - calc;

stage_calc.push(calc_time);
stage_draw.push(draw_time);

if (stage_calc.length < 300) {
return;
}

/**
* Process a stage of timings, log the average to the console and
* clear the array.
* @param {number[]} stage - array of timings
* @param {string} text - string to prefix the log message with
*/
function process_stage(stage: number[]) {
let sum = stage.reduce((partialSum, a) => partialSum + a, 0);
let avg = sum / stage.length;
return 1000 / avg;
}
const calc_perf = process_stage(stage_calc);
const draw_perf = process_stage(stage_draw);

_performance_text = `Calculation: ${calc_perf.toFixed(2)}hz, Drawing: ${draw_perf.toFixed(2)}hz`;

stage_calc.length = 0;
stage_draw.length = 0;
}

0 comments on commit e4b649e

Please sign in to comment.