diff --git a/deno.jsonc b/deno.jsonc index f8c3e73..8e6e398 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,22 +1,23 @@ { "tasks": { - "example_opengl": "deno run -A --unstable examples/opengl.ts", - "example_imgui": "deno run -A --unstable examples/imgui.ts", - "example_imgui2": "deno run -A --unstable examples/imgui2.ts", - "example_canvas": "deno run -A --unstable examples/canvas.ts", - "example_canvas_chart": "deno run -A --unstable examples/canvas-chart.ts", - "example_canvas_svg": "deno run -A --unstable examples/canvas-svg.ts", - "example_cursor": "deno run -A --unstable examples/cursor.ts", - "example_cube": "deno run -A --unstable examples/cube.ts", - "example_cube2": "deno run -A --unstable examples/cube2.ts", - "example_styles": "deno run -A --unstable examples/styles.ts", - "example_events": "deno run -A --unstable examples/events.ts", - "example_multi_window": "deno run -A --unstable examples/multi-window.ts", - "example_transparent": "deno run -A --unstable examples/transparent.ts", - "example_window": "deno run -A --unstable examples/window.ts", - "example_capture": "deno run -A --unstable examples/mouse-capture.ts", - "example_custom_cursor": "deno run -A --unstable examples/custom-cursor.ts", - "example_custom_icon": "deno run -A --unstable examples/custom-icon.ts" + "example:opengl": "deno run -A --unstable examples/opengl.ts", + "example:clock": "deno run -A --unstable examples/clock/main.ts", + "example:imgui": "deno run -A --unstable examples/imgui.ts", + "example:imgui2": "deno run -A --unstable examples/imgui2.ts", + "example:canvas": "deno run -A --unstable examples/canvas.ts", + "example:canvas_chart": "deno run -A --unstable examples/canvas-chart.ts", + "example:canvas_svg": "deno run -A --unstable examples/canvas-svg.ts", + "example:cursor": "deno run -A --unstable examples/cursor.ts", + "example:cube": "deno run -A --unstable examples/cube.ts", + "example:cube2": "deno run -A --unstable examples/cube2.ts", + "example:styles": "deno run -A --unstable examples/styles.ts", + "example:events": "deno run -A --unstable examples/events.ts", + "example:multi_window": "deno run -A --unstable examples/multi-window.ts", + "example:transparent": "deno run -A --unstable examples/transparent.ts", + "example:window": "deno run -A --unstable examples/window.ts", + "example:capture": "deno run -A --unstable examples/mouse-capture.ts", + "example:custom_cursor": "deno run -A --unstable examples/custom-cursor.ts", + "example:custom_icon": "deno run -A --unstable examples/custom-icon.ts" }, "lint": { diff --git a/examples/clock/clockNumber.ts b/examples/clock/clockNumber.ts new file mode 100644 index 0000000..e39da2e --- /dev/null +++ b/examples/clock/clockNumber.ts @@ -0,0 +1,211 @@ +import { activateDot, renderDot } from "./dotPool.ts"; +import { DOT_HEIGHT, DOT_WIDTH } from "./main.ts"; + +/** + * This class creates an array of physical locations + * that will be used as a 4 x 7 dot matrix. + * This array will be manipulted to represent a + * seven segment numeric display. + * Each dot position will be active or inactive + * based on a set of numeric 'masks' that represent + * the numbers 0 to 9 + */ +export interface ClockNumber { + x: number; + y: number; + drawPixels(px: number[][]): void; +} + +export const MatrixWidth = 4; +export const MatrixHeight = 7; + +let dot = { x: 0, y: 0 }; + +/** + * Create a new ClockNumber object and + * initialize its dot array based on the + * passed in location parameters. + */ +export function createNumber(x: number, y: number): ClockNumber { + // set location for this display + const left = x; + const top = y; + + /** + * A 2 dimensional array of points + * as a 4 x 7 matrix, that contains a mask + * of values 0 or 1 to indicate active pixels + */ + let currentPixelMask: number[][]; + + /** A 2 dimensional array of point locations(dots) as a 4 x 7 matrix */ + const dotLocations: { x: number; y: number }[][] = new Array(MatrixHeight); + + for (let i = 0; i < MatrixHeight; ++i) { + dotLocations[i] = new Array(MatrixWidth); + } + + // calculate/set each location of our dots + for (let y = 0; y < MatrixHeight; ++y) { + for (let x = 0; x < MatrixWidth; ++x) { + const xx = left + (x * DOT_WIDTH); + const yy = top + (y * DOT_HEIGHT); + dotLocations[y][x] = { + x: xx, + y: yy, + }; + } + } + + /** + * Draw the visual pixels(dots) for a given number, + * based on a lookup in an array of pixel masks. + * SEE: the PIXELS array below. + * If a value in the mask is set to 1, that position in + * the display will have a visual dot displayed. + * . + * . + * On a number change, any active dot that is not required + * to be active in the new number, will be set free ... + * That is, it will be 'activated' in the DotPool, becoming + * an animated dot. + */ + return { + x: left, + y: top, + drawPixels: (newPixelMask: number[][]) => { + for (y = 0; y < MatrixHeight; ++y) { + for (x = 0; x < MatrixWidth; ++x) { + dot = dotLocations[y][x]; + if (currentPixelMask != null) { + // if this dot is 'on', and it is not required for the new number + if ((currentPixelMask[y][x] !== 0) && (newPixelMask[y][x] === 0)) { + // activate it as a 'free' animated dot + activateDot(dot.x, dot.y); + } + } + // if this dot is an active member of this number mask + if (newPixelMask[y][x] === 1) { + // render it to the canvas + renderDot(dot.x, dot.y); + } + } + } + // Set the current pixel mask to this new mask. Used to + // evaluate pixels to be 'freed' during next update. + currentPixelMask = newPixelMask; + }, + }; +} + +/** + * A lookup array of 10 pixel masks(0-9). + * Each mask(array) represents the pixels of + * a 4 x 7 matrix of dots that are used to + * display a 7 segment numeric display. + * If a value in the mask is set to 1, that position + * in this display will have a visual dot displayed. + * A value of 0 will not be displayed. + */ +export const PIXELS = [ + // 'zero' + [ + [1, 1, 1, 1], + [1, 0, 0, 1], + [1, 0, 0, 1], + [1, 0, 0, 1], + [1, 0, 0, 1], + [1, 0, 0, 1], + [1, 1, 1, 1], + ], + // 'one' + [ + [0, 0, 0, 1], + [0, 0, 0, 1], + [0, 0, 0, 1], + [0, 0, 0, 1], + [0, 0, 0, 1], + [0, 0, 0, 1], + [0, 0, 0, 1], + ], + // 'two' + [ + [1, 1, 1, 1], + [0, 0, 0, 1], + [0, 0, 0, 1], + [1, 1, 1, 1], + [1, 0, 0, 0], + [1, 0, 0, 0], + [1, 1, 1, 1], + ], + // 'three' + [ + [1, 1, 1, 1], + [0, 0, 0, 1], + [0, 0, 0, 1], + [1, 1, 1, 1], + [0, 0, 0, 1], + [0, 0, 0, 1], + [1, 1, 1, 1], + ], + // 'four' + [ + [1, 0, 0, 1], + [1, 0, 0, 1], + [1, 0, 0, 1], + [1, 1, 1, 1], + [0, 0, 0, 1], + [0, 0, 0, 1], + [0, 0, 0, 1], + ], + // 'five' + [ + [1, 1, 1, 1], + [1, 0, 0, 0], + [1, 0, 0, 0], + [1, 1, 1, 1], + [0, 0, 0, 1], + [0, 0, 0, 1], + [1, 1, 1, 1], + ], + // 'six' + [ + [1, 1, 1, 1], + [1, 0, 0, 0], + [1, 0, 0, 0], + [1, 1, 1, 1], + [1, 0, 0, 1], + [1, 0, 0, 1], + [1, 1, 1, 1], + ], + // 'seven' + [ + [1, 1, 1, 1], + [0, 0, 0, 1], + [0, 0, 0, 1], + [0, 0, 0, 1], + [0, 0, 0, 1], + [0, 0, 0, 1], + [0, 0, 0, 1], + ], + // 'eight' + [ + [1, 1, 1, 1], + [1, 0, 0, 1], + [1, 0, 0, 1], + [1, 1, 1, 1], + [1, 0, 0, 1], + [1, 0, 0, 1], + [1, 1, 1, 1], + ], + // 'nine' + [ + [1, 1, 1, 1], + [1, 0, 0, 1], + [1, 0, 0, 1], + [1, 1, 1, 1], + [0, 0, 0, 1], + [0, 0, 0, 1], + [1, 1, 1, 1], + ], +]; diff --git a/examples/clock/dotPool.ts b/examples/clock/dotPool.ts new file mode 100644 index 0000000..e50f38f --- /dev/null +++ b/examples/clock/dotPool.ts @@ -0,0 +1,403 @@ +import { ctx, HEIGHT, WIDTH } from "./main.ts"; + +/** A gravitational pull in the X direction + * (positive = right and negative = left) + * default = 0 + */ +const gravityX = 0; + +/** A gravitational pull in the positive Y direction(down to floor) + * Have some fun! ... try a negative value + * default = 50 + */ +let gravityY = 2000; +export const setGravityY = (value: number) => { + gravityY = value; +}; + +/** + * The coefficient of restitution (COR) is the ratio + * of the final to initial relative velocity between + * two objects after they collide. + * + * This represents the amount of 'bounce' a dot will exibit. + * 1.0 = full rebound, and 0.5 will rebound only + * half as high as the distance fallen. + * default = 0.5 + */ +let Restitution = 0.5; +export const setRestitution = (value: number) => { + Restitution = value; +}; + +/** + * The radius of dots + * default = 14px + */ +const Radius = 14.0; + +/** + * Half the Radius. Used in the rendering calculation of arcs(circles). + * We pre-calculated this value to prevent the cost of calculations in loops. + * default = 7px + */ +const HalfRadius = 7.0; + +/** + * Radius Squared is used in the calculation of distances between dots. + * We pre-calculated this value to prevent the cost of calculations in loops. + * default = 14 * 14 + */ +const Radius_Sqrd = 14 * 14; + +/** + * The Maximum Velocity that a dot may take when it recieves a random velocity. + * default = 1500 + */ +let MaxVelocity = 1500.0; +export const setMaxVelocity = (value: number) => { + MaxVelocity = value; +}; + +/** + * Our default dot color (blue) + */ +const Color = "#44f"; + +/** + * Here we draw a dot(circle) on the screen (canvas). + * This method is used to create our 'static' + * time-value 'numbers' and 'colons' on the screen. + * These are rendered as simple circles. + * + * A similar method, DotPool.renderFreeDot, is used to + * render animated dots using lines instead of circles. + * This will help emulate 'particle-com-trails'. (SEE: renderFreeDot below) + */ +export const renderDot = (x: number, y: number, color?: string) => { + ctx.fillStyle = color || Color; + ctx.beginPath(); + ctx.arc(x, y, HalfRadius, 0, 2 * Math.PI, true); + ctx.closePath(); + ctx.fill(); +}; + +let idx = 0; +let i = 0; +let j = 0; +let distanceX = 0; +let distanceY = 0; +const delta = 0; +let thisDistanceSquared = 0; +let velocityDiffX = 0; +let velocityDiffY = 0; +let actualDistance = 0; + +let ratioX = 0; +let ratioY = 0; +let impactSpeed = 0; +let newDotAx = 0; +let newDotAy = 0; +let newDotBx = 0; +let newDotBy = 0; + +/** + * Rather than using a variable sized set of individual Dot objects, + * we build several fixed size arrays that provide all required + * attributes that represent a pool of dots. + * + * The main benefit, is the elimination of most garbage collection + * that building and destroying many dots at 60 frames per second + * would produce. + * + * We simply activate or inactivate an index(dot), by setting the value + * of the posX array. A positive integer in the posX array indicates + * 'active', and a value of -1 indicates an inactive index. + * Any index with an active posX value will be updated, tested for + * collisions, and rendered to the canvas. + * + * New dot activations are always set at the lowest inactive posX-index. + * We also maintain a 'tail pointer' to point to the highest active index. + * This 'tail pointer' allows all 'loops' to only loop over elements presumed + * to be active. + * These loops, from 0 to TailPointer, will also short circuit any index in + * the loop that is inactive (has a posX value of -1). + * + * When a dot falls off the edge of the canvas, its posX is set inactive(-1). + * If that dots index is equal to the tail-pointer value, we decrement + * the TailPointer, effectively reducing the active pool size. + * + * Whenever a time-change(tick) causes the production of new 'animated' dots, + * we simply find the first inactive index, and set it active by setting its + * posX value to the x location of the 'time-dot' that is being set free. + * If that first free-index is greater than the current tail, we set the tail-pointer + * value to this new index, effectivly increasing the active pool size. + * + * This is a very efficient use of memory, and provides very efficient dot-animation + * updates and collision-detection, as no new memory is required for each 'tick'. + * This reduced presure on the garbage collector eliminates 'jank' that is common with + * many forms of javascript animation where objects are created and destroyed per 'tick'. + */ +//export class DotPool { + +/** A 'fixed' maximum number of dots this pool will contain. */ +const POOL_SIZE = 500; + +/** An array of horizontal dot position values */ +const posX: number[] = []; + +/** An array of vertical dot position values */ +const posY: number[] = []; + +/** An array of last-known horizontal location values */ +const lastX: number[] = []; + +/** An array of last-known vertical location values */ +const lastY: number[] = []; + +/** An array of horizontal velocity values */ +const velocityX: number[] = []; + +/** An array of vertical velocity values */ +const velocityY: number[] = []; + +/** Points to the highest index that is currently set active. */ +let tailPointer = 0; + +/** + * Returns a random velocity value + * clamped by the value of MaxVelocity + */ +const randomVelocity = () => { + return (Math.random() - 0.4) * MaxVelocity; +}; + +/** + * Initializes all DotPool value arrays. + */ +export function initializeDotPool() { + for (i = 0; i < POOL_SIZE; i++) { + posX[i] = -1; + posY[i] = 0; + lastX[i] = -1; + lastY[i] = 0; + velocityX[i] = randomVelocity(); + velocityY[i] = randomVelocity(); + } +} + +/** + * The main entry point for DotPool animations. + * (called from the ClockFace animation loop 'ClockFace.tick()'). + * ClockFace.tick() is triggered by window.requestAnimationFrame(). + * We would expect ~ 60 frames per second here. + */ +export const tickDots = (delta: number) => { + delta /= 1000; + updateDotPositions(delta); + testForCollisions(delta); +}; + +/** + * This method recalculates dot locations and velocities + * based on a time-delta (time-change since last update). + * + * This method also mutates velocity/restitution whenever + * a wall or floor collision is detected. + */ +function updateDotPositions(delta: number) { + // loop over all 'active' dots (all dots up to the tail pointer) + for (i = 0; i < tailPointer + 2; i++) { + // if this dot is inactive, skip over it and go on to the next + if (posX[i] === -1) continue; + + // use gravity to calculate our new velocity and position + velocityX[i] += gravityX * delta; + velocityY[i] += gravityY * delta; + posX[i] += velocityX[i] * delta; + posY[i] += velocityY[i] * delta; + + // did we hit a wall? + if ((posX[i] <= Radius) || (posX[i] >= WIDTH)) { + // has it rolled off either end on the floor? + if (posY[i] >= HEIGHT - 2) { + posX[i] = -1; // -1 will inactivate this dot + + // if this was the tail, decrement the tailPointer + if (i === tailPointer) { + tailPointer--; + } + continue; + + // it was'nt on the floor so ... boune it off the wall + } else { + if (posX[i] <= Radius) posX[i] = Radius; + if (posX[i] >= WIDTH) posX[i] = WIDTH; + // bounce it off the wall (restitution represents bounciness) + velocityX[i] *= -Restitution; + } + } + + // did we hit the floor? If so, bounce it off the floor + if (posY[i] >= HEIGHT) { + posY[i] = HEIGHT; + // bounce it off the floor (restitution represents bounciness) + velocityY[i] *= -Restitution; + } + + // did we hit the ceiling? If so, bounce it off the ceiling + if (posY[i] <= Radius) { + posY[i] = Radius; + // bounce it off the ceiling (restitution represents bounciness) + velocityY[i] *= -Restitution; + } + + // draw this dot + renderFreeDot(i); + } +} + +/** + * This method tests for dots colliding with other dots. + * When a collision is detected, we mutate the velocity values + * of both of the colliding dots. + */ +function testForCollisions(delta: number) { + // loop over all active dots in the pool + for (i = 0; i < tailPointer + 2; i++) { + // is this dot active? + if (posX[i] === -1) continue; + // test this active dot against all other active dots + for (j = 0; j < tailPointer + 2; j++) { + if (i === j) continue; // same dot, can't collide with self + if (posX[j] === -1) continue; // not an active dot + distanceX = Math.abs(posX[i] - posX[j]); + distanceY = Math.abs(posY[i] - posY[j]); + + // for efficiency, we use only the squared-distance + // not the square-root of the squared-distance. square-root is very expensive + thisDistanceSquared = distanceX ** 2 + distanceY ** 2; + + // Are we about to collide? + // here we compare the squared-distance to the squared-radius of a dot + // again, we avoid expensive square-root calculations + if (thisDistanceSquared < Radius_Sqrd) { + // the distance apart is less than a dots radius ... is it about to get greater? + // To see if dots are moving away from each other + // we calculate a future position based on the last delta. + if (newDistanceSquared(delta, i, j) > thisDistanceSquared) { + // distance apart is increasing, so these dots are moving away from each other + // just ignor and continue + continue; + } + // if we got here we've collided + collideDots(i, j, distanceX, distanceY); + } + } + } +} + +/** + * This method will calculate new velocity values + * for both of the colliding dots. + */ +function collideDots( + dotA: number, + dotB: number, + distanceX: number, + distanceY: number, +) { + thisDistanceSquared = distanceX ** 2 + distanceY ** 2; + + velocityDiffX = velocityX[dotA] - velocityX[dotB]; + velocityDiffY = velocityY[dotA] - velocityY[dotB]; + + // get the actual absolute distance (hypotenuse) + actualDistance = Math.sqrt(thisDistanceSquared); + + // now we can callculate each dots new velocities + + // convert the distances to ratios + ratioX = distanceX / actualDistance; + ratioY = distanceY / actualDistance; + + // apply the speed (based on the ratios) to the velocity vectors + impactSpeed = (velocityDiffX * ratioX) + (velocityDiffY * ratioY); + velocityX[dotA] -= ratioX * impactSpeed; + velocityY[dotA] -= ratioY * impactSpeed; + velocityX[dotB] += ratioX * impactSpeed; + velocityY[dotB] += ratioY * impactSpeed; +} + +/** + * Calculates a 'future' distance between two dots, + * based on the last-known time-delta for the animations. + * This is used to determin if the two dots are + * moving toward, or away, from one another. + */ +function newDistanceSquared(delta: number, a: number, b: number) { + newDotAx = posX[a] + (velocityX[a] * delta); + newDotAy = posY[a] + (velocityY[a] * delta); + newDotBx = posX[b] + (velocityX[b] * delta); + newDotBy = posY[b] + (velocityY[b] * delta); + return (Math.abs(newDotAx - newDotBx) ** 2) + + (Math.abs(newDotAy - newDotBy) ** 2); +} + +/** + * Activates a dot-pool index, to create a new animated dot. + * Whenever a time-number change causes one or more + * dots to be 'freed' from the number display, we animated + * them as if they exploded out of the number display. + * We do this by activating the next available index, + * setting its position to the position of the freed-dot, + * and then assigning a random velocity to it. + * If we have activated the array index pointed to by + * tailPointer, we increment the tailPointer to maintain + * our active pool size. + */ +export function activateDot(x: number, y: number) { + // loop though the pool to find an unused index + // a value of '-1' for posX is used to indicate 'inactive' + for (idx = 0; idx < tailPointer + 2; idx++) { + if (posX[idx] === -1) { + // add values for this dots location (this makes it 'active') + posX[idx] = x; + posY[idx] = y; + lastX[idx] = x; + lastY[idx] = y; + velocityX[idx] = randomVelocity(); + velocityY[idx] = randomVelocity(); + // if this is past the tail, make this the tailPointer + if (idx > tailPointer) tailPointer = idx; + + // we're all done, break out of this loop + break; + } + } +} + +/** + * This method renders a track of an animated(free) + * dot in the dot pool. + * + * Rather than static circles, we actually draw short lines + * that represent the distance traveled since the last update. + * These lines are drawn with round ends to better represent + * a moving dot(circle). These short lines are automatically + * faded to black over time, to simulate a particle with a 'com-trail'. + * SEE: ClockFace.tick() to understand this phenomenon. + */ +const renderFreeDot = (i: number) => { + ctx.beginPath(); + ctx.fillStyle = Color; + ctx.strokeStyle = Color; + ctx.lineWidth = Radius; + ctx.moveTo(lastX[i] - Radius, lastY[i] - Radius); + ctx.lineTo(posX[i] - Radius, posY[i] - Radius); + ctx.stroke(); + ctx.closePath(); + ctx.fill(); + lastX[i] = posX[i]; + lastY[i] = posY[i]; +}; diff --git a/examples/clock/main.ts b/examples/clock/main.ts new file mode 100644 index 0000000..fc922dd --- /dev/null +++ b/examples/clock/main.ts @@ -0,0 +1,244 @@ +// thank you to https://github.com/nhrones/SkiaClock for the original code + +import { mainloop, WindowCanvas } from "./window.ts"; +import { + ClockNumber, + createNumber, + MatrixHeight, + MatrixWidth, + PIXELS, +} from "./clockNumber.ts"; +import { initializeDotPool, renderDot, tickDots } from "./dotPool.ts"; + +export const WIDTH = 900; +export const HEIGHT = 450; + +/** used for rgba masking */ +const RGBA = "rgba(0, 0, 0, 0.2)"; +let delta = 0; +let lastTime = performance.now(); + +const wc = new WindowCanvas({ + title: "SKIA-CANVAS-CLOCK", + width: WIDTH, + height: HEIGHT, +}); + +export const { window, canvas, ctx } = wc; + +window.position = { x: 600, y: 250 }; +window.makeContextCurrent; + +ctx.fillStyle = "white"; +ctx.strokeStyle = "white"; +ctx.font = "bold 16px sanserif"; + +// a few required clock-face constants +// these are used to define the shape of +// our 4 x 7 dot matrix, for the 7-segment 'LED' numbers +export const NUMBER_SPACING = 16; +export const DOT_WIDTH = 16; +export const DOT_HEIGHT = 16; +let hSize = 0; +let vSize = 0; +let i; + +/** The current horizontal location to render to */ +let currentX = 0; + +/** the current vertical location to render to */ +let currentY = 0; + +/* + * This app creates a graphic display of a digital(numeric) clock face. + * The face shows pairs of two, 7-segment 'LED' numeric displays for; + * hour, minute, and seconds values, each separated by a 'colon' character. + * The segments are drawn as 4 x 7 matrix of dots(circles) that immitate + * common 7-segment 'LED' numeric displays. + * + * This clock face is animated to 'explode' numbers as they change. + * Any segment(dot) that is active when a number value changes, and is not + * required to display the new value, is animated with velocity away + * from its original spot in the number. + * These 'free' dots become animated, will collide with each other, + * bounce off walls, and eventually fall out of view if they roll on + * the floor off either end. + */ + +/** + * A two element array of instances of the ClockNumber class. + * Represents the graphic display of a 2 digit 'hours' number + * (using a leading zero) + */ +let hours: ClockNumber[]; + +/** + * A two element array of instances of the ClockNumber class. + * Represents the graphic display of a 2 digit 'minutes' number + * (using a leading zero) + */ +let minutes: ClockNumber[]; + +/** + * A two element array of instances of the ClockNumber class. + * Represents the graphic display of a 2 digit 'seconds' number + * (using a leading zero) + */ +let seconds: ClockNumber[]; + +/** colon locations */ +let colon1X = 0; +let colon2X = 0; + +/** Constructs and initializes a new ClockFace */ +export const buildClockFace = () => { + // init our ClockNumber array objects to empty(default) values + hours = [createNumber(0, 0), createNumber(0, 0)]; + minutes = [createNumber(0, 0), createNumber(0, 0)]; + seconds = [createNumber(0, 0), createNumber(0, 0)]; + + // initialize the dot-pool that will contain + // and animate dots that are 'freed' from this clock-face. + initializeDotPool(); + + // fill the background image all solid black + ctx.fillStyle = "black"; + ctx.fillRect(0, 0, WIDTH, HEIGHT); + ctx.lineCap = "round"; + + // draw number placeholders and colons onto the canvas + createNumbers(); +}; + +/** + * Display the current time. + * Called on each 'tick' + */ +const updateTime = (now: Date) => { + // set the current hours display + setDigits(pad2(now.getHours()), hours); + + // set the current minutes display + setDigits(pad2(now.getMinutes()), minutes); + + // set the current seconds display + setDigits(pad2(now.getSeconds()), seconds); +}; + +/** + * Sets the static and active pixels for each of the two numeric displays + * SEE: ClockNumber.setPixels() + */ +const setDigits = (digits: string, numbers: ClockNumber[]) => { + numbers[0].drawPixels(PIXELS[parseInt(digits[0])]); + numbers[1].drawPixels(PIXELS[parseInt(digits[1])]); +}; + +/** + * This is where we create our empty numeric displays + * and their two separating colons. + * + * Called only once by the constructor for initialization. + */ +const createNumbers = () => { + // first, calculate the width of a numeric display + // (16 x 4 + 16) * 6 + (16 + 16) x 2 + hSize = ((DOT_WIDTH * MatrixWidth) + + NUMBER_SPACING) * 6 + + ((DOT_WIDTH + NUMBER_SPACING) * 2) - NUMBER_SPACING; + + // Now, calculate the height of a numeric display + vSize = DOT_HEIGHT * MatrixHeight; + + // we calculate our initial 'top' value (y) + currentY = (HEIGHT - vSize) * 0.33; + + // Next, initialize the horizontal position (x) + // We will manipulate this several times as we build up the display + currentX = (WIDTH - hSize) * 0.45; + + // go build the 'hours' display + buildNumber(hours); + + // Set the position of the colon between the hours and minutes display + colon1X = currentX + 8; + + // calculate the horizontal position for the minutes display + currentX += DOT_WIDTH + (2 * NUMBER_SPACING); + + // go build the 'minutes' display + buildNumber(minutes); + + // Set the position of the colon between the minutes and seconds display + colon2X = currentX + 8; + + // calculate the horizontal position for the seconds display + currentX += DOT_WIDTH + (2 * NUMBER_SPACING); + + // finally, build the 'seconds' display + buildNumber(seconds); +}; + +/** + * Initialize the positions of the ClockNumber objects, + */ +const buildNumber = (digits: ClockNumber[]) => { + for (i = 0; i < 2; ++i) { + digits[i] = createNumber(currentX, currentY); + currentX += (DOT_WIDTH * MatrixWidth) + NUMBER_SPACING; + } +}; + +/** + * Convert a number to a string and add a + * leading zero to any number less than 10. + */ +const pad2 = (num: number) => { + return (num < 10) ? `0${num}` : num.toString(); +}; + +buildClockFace(); + +/////////////////////////\\\\\\\\\\\\\\\\\\\\\\\\ +// \\ +// Main animation loop \\ +// \\ +/////////////////////////\\\\\\\\\\\\\\\\\\\\\\\\ +/* This method produces a 'particle' effect using + * a transparent fill on the canvas. + * We expect ~ 60 frames per second. + */ +await mainloop(() => { + window.makeContextCurrent(); + + ctx.fillStyle = RGBA; + + // spray the whole canvas with the above transparent black + ctx.fillRect(0, 0, WIDTH, HEIGHT); + + ctx.fillStyle = "black"; + + // Render Colon #1 between the hours and minutes + renderDot(colon1X, currentY + (2.0 * DOT_HEIGHT)); + renderDot(colon1X, currentY + (4.0 * DOT_HEIGHT)); + + // Render Colon #2 between the minutes and seconds + renderDot(colon2X, currentY + (2.0 * DOT_HEIGHT)); + renderDot(colon2X, currentY + (4.0 * DOT_HEIGHT)); + + // display the graphical time value dots + updateTime(new Date()); + + // update all of the animated 'free' dots + delta = performance.now() - lastTime; + lastTime = performance.now(); + tickDots(delta); + + // show FPS on screen (uncomment below to show Frames Per Second) + //const text = 'FPS = ' + (1000 / delta).toFixed() + //ctx.fillStyle = "white" + //ctx.fillText(text, 10, 20) + //ctx.fillStyle = '#44f' + + wc.flush(); +}); diff --git a/examples/clock/window.ts b/examples/clock/window.ts new file mode 100644 index 0000000..fae0144 --- /dev/null +++ b/examples/clock/window.ts @@ -0,0 +1,88 @@ +// https://github.com/deno-windowing/dwm/blob/main/ext/canvas.ts +import { + Canvas, + CanvasRenderingContext2D, + createCanvas, +} from "https://deno.land/x/skia_canvas@0.5.4/mod.ts"; + +import { + createWindow, + CreateWindowOptions, + DwmWindow, + WindowClosedEvent, + WindowFramebufferSizeEvent, + WindowRefreshEvent, +} from "https://deno.land/x/dwm@0.3.3/mod.ts"; + +export class WindowCanvas { + canvas: Canvas; + window: DwmWindow; + ctx: CanvasRenderingContext2D; + + #toDraw = true; + + onContextLoss?: () => void; + onDraw?: (ctx: CanvasRenderingContext2D) => unknown; + + #resizeNextFrame?: [number, number]; + + constructor(options: CreateWindowOptions = {}) { + this.window = createWindow(Object.assign({ + glVersion: [3, 3], + }, options)); + const { width, height } = this.window.framebufferSize; + this.canvas = createCanvas(width, height, true); + this.ctx = this.canvas.getContext("2d"); + + const onFramebuffersize = (evt: WindowFramebufferSizeEvent) => { + if (!evt.match(this.window)) return; + if (evt.width === 0 || evt.height === 0) { + this.#toDraw = false; + return; + } + this.#resizeNextFrame = [evt.width, evt.height]; + }; + + const onClosed = (evt: WindowClosedEvent) => { + if (!evt.match(this.window)) return; + removeEventListener("framebuffersize", onFramebuffersize); + removeEventListener("closed", onClosed); + this.#toDraw = false; + }; + + addEventListener("framebuffersize", onFramebuffersize); + addEventListener("closed", onClosed); + } + + #resize(width: number, height: number) { + this.canvas.resize(width, height); + this.ctx = this.canvas.getContext("2d"); + this.onContextLoss?.(); + this.#toDraw = true; + } + + makeContextCurrent() { + this.window.makeContextCurrent(); + } + + flush() { + if (!this.#toDraw) return; + this.canvas.flush(); + this.window.swapBuffers(); + } + + async draw() { + if (!this.#toDraw) return; + // this.makeContextCurrent(); + if (this.#resizeNextFrame) { + const [width, height] = this.#resizeNextFrame; + this.#resizeNextFrame = undefined; + this.#resize(width, height); + } + await this.onDraw?.(this.ctx); + this.flush(); + } +} + +export * from "https://deno.land/x/skia_canvas@0.5.4/mod.ts"; +export { mainloop } from "https://deno.land/x/dwm@0.3.3/mod.ts";