diff --git a/src/dreams/dendraclock.ts b/src/dreams/dendraclock.ts index 43100fd..95bd6de 100644 --- a/src/dreams/dendraclock.ts +++ b/src/dreams/dendraclock.ts @@ -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; @@ -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) { @@ -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) { @@ -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 = 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) { @@ -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; +}