From 5679fdf8bd816df05db4b0a4a05612b3f5aebca3 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 19 Dec 2024 04:22:40 -0500 Subject: [PATCH] Draw entire timeline as canvas --- src/main.js | 183 ++++++++++++++++++++++++++++++++++++++++++++++---- src/styles.js | 25 +++++++ src/utils.js | 52 +++++++++++++- 3 files changed, 246 insertions(+), 14 deletions(-) create mode 100644 src/styles.js diff --git a/src/main.js b/src/main.js index c8604bb..a7aa2b7 100644 --- a/src/main.js +++ b/src/main.js @@ -3,7 +3,8 @@ import * as fitCurve from '/fit-curve.js'; import { Bezier } from "/bezier.js"; import { Quadtree } from './quadtree.js'; import { createNewFileDialog, showNewFileDialog, closeDialog } from './newfile.js'; -import { titleCase, getMousePositionFraction, getKeyframesSurrounding, invertPixels, lerpColor, lerp, camelToWords, generateWaveform, floodFillRegion, getShapeAtPoint, hslToRgb, drawCheckerboardBackground, hexToHsl, hsvToRgb, hexToHsv, rgbToHex, clamp } from './utils.js'; +import { titleCase, getMousePositionFraction, getKeyframesSurrounding, invertPixels, lerpColor, lerp, camelToWords, generateWaveform, floodFillRegion, getShapeAtPoint, hslToRgb, drawCheckerboardBackground, hexToHsl, hsvToRgb, hexToHsv, rgbToHex, clamp, drawBorderedRect, drawCenteredText } from './utils.js'; +import { backgroundColor, darkMode, foregroundColor, frameWidth, gutterHeight, highlight, layerHeight, layerWidth, scrubberColor, shade, shadow } from './styles.js'; const { writeTextFile: writeTextFile, readTextFile: readTextFile, writeFile: writeFile, readFile: readFile }= window.__TAURI__.fs; const { open: openFileDialog, @@ -224,6 +225,7 @@ let context = { selectionRect: undefined, selection: [], shapeselection: [], + selectedFrames: [], dragDirection: undefined, zoomLevel: 1, } @@ -3591,19 +3593,73 @@ function toolbar() { } function timeline() { - let container = document.createElement("div") - let layerspanel = document.createElement("div") - let framescontainer = document.createElement("div") - container.classList.add("horizontal-grid") - container.classList.add("layers-container") - layerspanel.className = "layers" - framescontainer.className = "frames-container" - container.appendChild(layerspanel) - container.appendChild(framescontainer) - layoutElements.push(container) - container.setAttribute("lb-percent", 20) + // let container = document.createElement("div") + // let layerspanel = document.createElement("div") + // let framescontainer = document.createElement("div") + // container.classList.add("horizontal-grid") + // container.classList.add("layers-container") + // layerspanel.className = "layers" + // framescontainer.className = "frames-container" + // container.appendChild(layerspanel) + // container.appendChild(framescontainer) + // layoutElements.push(container) + // container.setAttribute("lb-percent", 20) - return container + // return container + + let timeline_cvs = document.createElement("canvas") + timeline_cvs.className = "timeline" + + function updateTimelineCanvasSize() { + const canvasStyles = window.getComputedStyle(timeline_cvs); + + timeline_cvs.width = parseInt(canvasStyles.width); + timeline_cvs.height = parseInt(canvasStyles.height); + updateLayers() + } + + // Set up ResizeObserver to watch for changes in the canvas size + const resizeObserver = new ResizeObserver(updateTimelineCanvasSize); + resizeObserver.observe(timeline_cvs); + + let scrollSpeed = 1; + timeline_cvs.addEventListener('wheel', (event) => { + event.preventDefault(); + const deltaX = event.deltaX * scrollSpeed; + const deltaY = event.deltaY * scrollSpeed; + + timeline_cvs.offsetX = Math.max(0, timeline_cvs.offsetX + deltaX); + timeline_cvs.offsetY = Math.max(0, timeline_cvs.offsetY + deltaY); + + updateLayers() + }); + timeline_cvs.addEventListener("mousedown", (e) => { + let mouse = getMousePos(timeline_cvs, e) + mouse.y += timeline_cvs.offsetY + if (mouse.x > layerWidth) { + mouse.x += timeline_cvs.offsetX - layerWidth + timeline_cvs.clicked_frame = Math.floor(mouse.x / frameWidth) + context.activeObject.setFrameNum(timeline_cvs.clicked_frame) + updateLayers() + } + console.log(mouse) + }) + timeline_cvs.addEventListener("mouseup", (e) => { + let mouse = getMousePos(timeline_cvs, e) + mouse.y += timeline_cvs.offsetY + if (mouse.x > layerWidth) { + mouse.x += timeline_cvs.offsetX - layerWidth + + updateLayers() + updateMenu() + } + console.log(mouse) + }) + + timeline_cvs.offsetX = 0; + timeline_cvs.offsetY = 0; + updateTimelineCanvasSize(); + return timeline_cvs } function infopanel() { @@ -3886,6 +3942,107 @@ function updateUI() { } function updateLayers() { + + + for (let canvas of document.querySelectorAll(".timeline")) { + const width = canvas.width + const height = canvas.height + const ctx = canvas.getContext("2d") + const offsetX = canvas.offsetX + const offsetY = canvas.offsetY + const frameCount = (width + offsetX - layerWidth) / frameWidth + ctx.fillStyle = backgroundColor + ctx.fillRect(0,0,width,height) + ctx.lineWidth = 1; + + // Draw timeline top + ctx.save() + ctx.save() + ctx.beginPath() + ctx.rect(layerWidth,0,width-layerWidth,height) + ctx.clip() + ctx.translate(layerWidth - offsetX, 0) + for (let j=Math.floor(offsetX / (5 * frameWidth)) * 5; j { + switch (frame.frameType) { + case "keyframe": + ctx.fillStyle = foregroundColor + drawBorderedRect(ctx, j*frameWidth, 0, frameWidth, layerHeight, highlight, shadow, backgroundColor, backgroundColor) + ctx.fillStyle = "#111" + ctx.beginPath() + ctx.arc((j+0.5)*frameWidth, layerHeight*0.75, frameWidth*0.25, 0, 2*Math.PI) + ctx.fill() + break; + case "normal": + ctx.fillStyle = foregroundColor + drawBorderedRect(ctx, j*frameWidth, 0, frameWidth, layerHeight, highlight, shadow, backgroundColor, backgroundColor) + break; + case "motion": + ctx.fillStyle = "#7a00b3" + ctx.fillRect(j*frameWidth, 0, frameWidth, layerHeight) + break; + case "shape": + ctx.fillStyle = "#9bff9b" + ctx.fillRect(j*frameWidth, 0, frameWidth, layerHeight) + break; + } + }) + // Draw highlighted frame + // if (context.activeObject.currentFrameNum) + ctx.restore() + i++; + } + ctx.restore() + + + // Draw scrubber bar + ctx.save() + ctx.beginPath() + ctx.rect(layerWidth, -gutterHeight, width, height) + ctx.clip() + ctx.translate(layerWidth - offsetX, 0) + let frameNum = context.activeObject.currentFrameNum + ctx.strokeStyle = scrubberColor + ctx.beginPath() + ctx.moveTo((frameNum + 0.5) * frameWidth, 0) + ctx.lineTo((frameNum + 0.5) * frameWidth, height) + ctx.stroke() + ctx.beginPath() + ctx.fillStyle = scrubberColor + ctx.fillRect(frameNum * frameWidth, -gutterHeight, frameWidth, gutterHeight) + drawCenteredText(ctx, (frameNum+1).toString(), (frameNum + 0.5) * frameWidth, -gutterHeight/2, gutterHeight) + ctx.restore() + ctx.restore() + } for (let container of document.querySelectorAll(".layers-container")) { let layerspanel = container.querySelectorAll(".layers")[0] let framescontainer = container.querySelectorAll(".frames-container")[0] diff --git a/src/styles.js b/src/styles.js new file mode 100644 index 0000000..27df2c7 --- /dev/null +++ b/src/styles.js @@ -0,0 +1,25 @@ +const darkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; +const backgroundColor = darkMode ? "#333" : "#ccc" +const foregroundColor = darkMode ? "#888" : "#ddd" +const highlight = darkMode ? "#4f4f4f" : "#ddd" +const shadow = darkMode ? "#111" : "#999" +const shade = darkMode ? "#222" : "#aaa" +const layerHeight = 50 +const layerWidth = 300 +const frameWidth = 25 +const gutterHeight = 15 +const scrubberColor = "#cc2222" + +export { + darkMode, + backgroundColor, + foregroundColor, + highlight, + shadow, + shade, + layerHeight, + layerWidth, + frameWidth, + gutterHeight, + scrubberColor +} \ No newline at end of file diff --git a/src/utils.js b/src/utils.js index a4ae897..56d9f07 100644 --- a/src/utils.js +++ b/src/utils.js @@ -547,6 +547,54 @@ function clamp(n) { return Math.min(Math.max(n,0),1) } +function drawBorderedRect(ctx, x, y, width, height, top, bottom, left, right) { + ctx.fillRect(x, y, width, height) + if (top) { + ctx.strokeStyle = top + ctx.beginPath() + ctx.moveTo(x, y) + ctx.lineTo(x+width, y) + ctx.stroke() + } + if (bottom) { + ctx.strokeStyle = bottom + ctx.beginPath() + ctx.moveTo(x, y+height) + ctx.lineTo(x+width, y+height) + ctx.stroke() + } + if (left) { + ctx.strokeStyle = left + ctx.beginPath() + ctx.moveTo(x, y) + ctx.lineTo(x, y+height) + ctx.stroke() + } + if (right) { + ctx.strokeStyle = right + ctx.beginPath() + ctx.moveTo(x+width, y) + ctx.lineTo(x+width, y+height) + ctx.stroke() + } +} + +function drawCenteredText(ctx, text, x, y, height) { + // Set the font size and family based on the 'height' parameter + ctx.font = `${height}px Arial`; // You can customize the font style if needed + + // Calculate the width of the text + const textWidth = ctx.measureText(text).width; + + // Calculate the position to center the text + const centerX = x - textWidth / 2; + const centerY = y + height / 4; // Adjust for vertical centering + + // Draw the text centered at (x, y) with the specified font size + ctx.fillStyle = 'black'; // Set the color for the text + ctx.fillText(text, centerX, centerY); +} + export { titleCase, getMousePositionFraction, @@ -564,5 +612,7 @@ export { hexToHsv, rgbToHex, drawCheckerboardBackground, - clamp + clamp, + drawBorderedRect, + drawCenteredText }; \ No newline at end of file