diff --git a/package.json b/package.json index 2348626..1ce0f8d 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,10 @@ "@tauri-apps/cli": "^2" }, "dependencies": { + "@ffmpeg/ffmpeg": "^0.12.10", "@tauri-apps/plugin-dialog": "~2", - "@tauri-apps/plugin-fs": "~2" + "@tauri-apps/plugin-fs": "~2", + "ffmpeg": "^0.0.4", + "ffmpeg.js": "^4.2.9003" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9af2e76..f168c04 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,12 +8,21 @@ importers: .: dependencies: + '@ffmpeg/ffmpeg': + specifier: ^0.12.10 + version: 0.12.10 '@tauri-apps/plugin-dialog': specifier: ~2 version: 2.0.1 '@tauri-apps/plugin-fs': specifier: ~2 version: 2.0.2 + ffmpeg: + specifier: ^0.0.4 + version: 0.0.4 + ffmpeg.js: + specifier: ^4.2.9003 + version: 4.2.9003 devDependencies: '@tauri-apps/cli': specifier: ^2 @@ -21,6 +30,14 @@ importers: packages: + '@ffmpeg/ffmpeg@0.12.10': + resolution: {integrity: sha512-lVtk8PW8e+NUzGZhPTWj2P1J4/NyuCrbDD3O9IGpSeLYtUZKBqZO8CNj1WYGghep/MXoM8e1qVY1GztTkf8YYQ==} + engines: {node: '>=18.x'} + + '@ffmpeg/types@0.12.2': + resolution: {integrity: sha512-NJtxwPoLb60/z1Klv0ueshguWQ/7mNm106qdHkB4HL49LXszjhjCCiL+ldHJGQ9ai2Igx0s4F24ghigy//ERdA==} + engines: {node: '>=16.x'} + '@tauri-apps/api@2.1.1': resolution: {integrity: sha512-fzUfFFKo4lknXGJq8qrCidkUcKcH2UHhfaaCNt4GzgzGaW2iS26uFOg4tS3H4P8D6ZEeUxtiD5z0nwFF0UN30A==} @@ -95,8 +112,23 @@ packages: '@tauri-apps/plugin-fs@2.0.2': resolution: {integrity: sha512-4YZaX2j7ta81M5/DL8aN10kTnpUkEpkPo1FTYPT8Dd0ImHe3azM8i8MrtjrDGoyBYLPO3zFv7df/mSCYF8oA0Q==} + ffmpeg.js@4.2.9003: + resolution: {integrity: sha512-l1JBr8HwnnJEaSwg5p8K3Ifbom8O2IDHsZp7UVyr6MzQ7gc32tt/2apoOuQAr/j76c+uDOjla799VSsBnRvSTg==} + + ffmpeg@0.0.4: + resolution: {integrity: sha512-3TgWUJJlZGQn+crJFyhsO/oNeRRnGTy6GhgS98oUCIfZrOW5haPPV7DUfOm3xJcHr5q3TJpjk2GudPutrNisRA==} + + when@3.7.8: + resolution: {integrity: sha512-5cZ7mecD3eYcMiCH4wtRPA5iFJZ50BJYDfckI5RRpQiktMiYTcn0ccLTZOvcbBume+1304fQztxeNzNS9Gvrnw==} + snapshots: + '@ffmpeg/ffmpeg@0.12.10': + dependencies: + '@ffmpeg/types': 0.12.2 + + '@ffmpeg/types@0.12.2': {} + '@tauri-apps/api@2.1.1': {} '@tauri-apps/cli-darwin-arm64@2.0.4': @@ -149,3 +181,11 @@ snapshots: '@tauri-apps/plugin-fs@2.0.2': dependencies: '@tauri-apps/api': 2.1.1 + + ffmpeg.js@4.2.9003: {} + + ffmpeg@0.0.4: + dependencies: + when: 3.7.8 + + when@3.7.8: {} diff --git a/src/main.js b/src/main.js index 36ad38e..d714276 100644 --- a/src/main.js +++ b/src/main.js @@ -3,7 +3,7 @@ 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 } from './utils.js'; +import { titleCase, getMousePositionFraction, getKeyframesSurrounding, invertPixels, lerpColor, lerp } from './utils.js'; const { writeTextFile: writeTextFile, readTextFile: readTextFile, writeFile: writeFile }= window.__TAURI__.fs; const { open: openFileDialog, @@ -276,28 +276,29 @@ let actions = { }); } img = await loadImage(action.src) + console.log(img.crossOrigin) // img.onload = function() { - let ct = { - ...context, - fillImage: img, - strokeShape: false, - } - let imageShape = new Shape(0, 0, ct, action.shapeUuid) - imageShape.addLine(img.width, 0) - imageShape.addLine(img.width, img.height) - imageShape.addLine(0, img.height) - imageShape.addLine(0, 0) - imageShape.update() - imageShape.fillImage = img - imageShape.filled = true - imageObject.addShape(imageShape) - let parent = pointerList[action.parent] - parent.addObject( - imageObject, - action.x-img.width/2 + (20*action.ix), - action.y-img.height/2 + (20*action.ix) - ) - updateUI(); + let ct = { + ...context, + fillImage: img, + strokeShape: false, + } + let imageShape = new Shape(0, 0, ct, action.shapeUuid) + imageShape.addLine(img.width, 0) + imageShape.addLine(img.width, img.height) + imageShape.addLine(0, img.height) + imageShape.addLine(0, 0) + imageShape.update() + imageShape.fillImage = img + imageShape.filled = true + imageObject.addShape(imageShape) + let parent = pointerList[action.parent] + parent.addObject( + imageObject, + action.x-img.width/2 + (20*action.ix), + action.y-img.height/2 + (20*action.ix) + ) + updateUI(); // } // img.src = action.src }, @@ -959,6 +960,7 @@ class BaseShape { ctx.fill() } if (this.stroked) { + console.log(this.curves) for (let curve of this.curves) { ctx.strokeStyle = curve.color ctx.beginPath() @@ -968,9 +970,11 @@ class BaseShape { curve.points[3].x, curve.points[3].y) ctx.stroke() - // Debug, show curve endpoints + // // Debug, show curve control points // ctx.beginPath() - // ctx.arc(curve.points[3].x,curve.points[3].y, 3, 0, 2*Math.PI) + // ctx.arc(curve.points[1].x,curve.points[1].y, 5, 0, 2*Math.PI) + // ctx.arc(curve.points[2].x,curve.points[2].y, 5, 0, 2*Math.PI) + // ctx.arc(curve.points[3].x,curve.points[3].y, 5, 0, 2*Math.PI) // ctx.fill() } } @@ -981,12 +985,14 @@ class BaseShape { } class TempShape extends BaseShape { - constructor(startx, starty, curves, lineWidth, stroked, filled) { + constructor(startx, starty, curves, lineWidth, stroked, filled, strokeStyle, fillStyle) { super(startx, starty) this.curves = curves this.lineWidth = lineWidth this.stroked = stroked this.filled = filled + this.strokeStyle = strokeStyle + this.fillStyle = fillStyle this.inProgress = false } } @@ -1380,8 +1386,6 @@ class GraphicsObject { }) } } - console.log(path1) - console.log(path2) const interpolator = d3.interpolatePathCommands(path1, path2) let current = interpolator(t) let curves = [] @@ -1392,9 +1396,16 @@ class GraphicsObject { x = curve.x y = curve.y } - console.log(curves) - // TODO: lerp lineWidth - shapes.push(new TempShape(start.x, start.y, curves, shape1.lineWidth, shape1.stroked)) + let lineWidth = lerp(shape1.lineWidth, shape2.lineWidth, t) + let strokeStyle = lerpColor(shape1.strokeStyle, shape2.strokeStyle, t) + let fillStyle; + if (!shape1.fillImage) { + fillStyle = lerpColor(shape1.fillStyle, shape2.fillStyle, t) + } + shapes.push(new TempShape( + start.x, start.y, curves, shape1.lineWidth, + shape1.stroked, shape1.filled, strokeStyle, fillStyle + )) } } let frame = new Frame("shape", "temp") @@ -1899,7 +1910,7 @@ async function render() { exportContext.ctx.fillStyle = "white" exportContext.ctx.rect(0,0,fileWidth, fileHeight) exportContext.ctx.fill() - root.draw(exportContext) + await root.draw(exportContext) // Convert the canvas content to a PNG image (this is the "frame" we add to the APNG) const imageData = exportContext.ctx.getImageData(0, 0, canvas.width, canvas.height); @@ -1983,7 +1994,7 @@ function stage() { e.preventDefault() let mouse = getMousePos(stage, e) const imageTypes = ['image/png', 'image/gif', 'image/avif', 'image/jpeg', - 'image/svg+xml', 'image/webp' + 'image/webp', //'image/svg+xml' // Disabling SVG until we can export them nicely ]; if (e.dataTransfer.items) { let i = 0 @@ -2029,6 +2040,7 @@ function stage() { let mouse = getMousePos(stage, e) switch (mode) { case "rectangle": + case "ellipse": case "draw": context.mouseDown = true context.activeShape = new Shape(mouse.x, mouse.y, context, true, true) @@ -2141,6 +2153,7 @@ function stage() { } break; case "rectangle": + case "ellipse": actions.addShape.create(context.activeObject, context.activeShape) context.activeShape = undefined break; @@ -2205,6 +2218,41 @@ function stage() { context.activeShape.update() } break; + case "ellipse": + context.activeCurve = undefined + if (context.activeShape) { + let midX = (mouse.x + context.activeShape.startx) / 2 + let midY = (mouse.y + context.activeShape.starty) / 2 + let xDiff = (mouse.x - context.activeShape.startx) / 2 + let yDiff = (mouse.y - context.activeShape.starty) / 2 + let ellipseConst = 0.552284749831 + context.activeShape.clear() + context.activeShape.addCurve(new Bezier( + midX, context.activeShape.starty, + midX + ellipseConst * xDiff, context.activeShape.starty, + mouse.x, midY - ellipseConst * yDiff, + mouse.x, midY + )) + context.activeShape.addCurve(new Bezier( + mouse.x, midY, + mouse.x, midY + ellipseConst * yDiff, + midX + ellipseConst * xDiff, mouse.y, + midX, mouse.y + )) + context.activeShape.addCurve(new Bezier( + midX, mouse.y, + midX - ellipseConst * xDiff, mouse.y, + context.activeShape.startx, midY + ellipseConst * yDiff, + context.activeShape.startx, midY + )) + context.activeShape.addCurve(new Bezier( + context.activeShape.startx, midY, + context.activeShape.startx, midY - ellipseConst * yDiff, + midX - ellipseConst * xDiff, context.activeShape.starty, + midX, context.activeShape.starty + )) + break; + } case "select": if (context.dragging) { if (context.activeVertex) { diff --git a/src/utils.js b/src/utils.js index 974df6d..d6bf25b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -91,4 +91,35 @@ function invertPixels(ctx, width, height) { ctx.globalCompositeOperation = "source-over" } -export { titleCase, getMousePositionFraction, getKeyframesSurrounding, invertPixels }; \ No newline at end of file +function lerp(a, b, t) { + return a + (b - a) * t; +} + +function lerpColor(color1, color2, t) { + // Convert hex color to RGB + const hexToRgb = (hex) => { + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + return { r, g, b }; + }; + + // Convert RGB to hex color + const rgbToHex = (r, g, b) => { + return `#${(1 << 24 | (r << 16) | (g << 8) | b).toString(16).slice(1).toUpperCase()}`; + }; + + // Get RGB values of both colors + const start = hexToRgb(color1); + const end = hexToRgb(color2); + + // Calculate the interpolated RGB values + const r = Math.round(start.r + (end.r - start.r) * t); + const g = Math.round(start.g + (end.g - start.g) * t); + const b = Math.round(start.b + (end.b - start.b) * t); + + // Convert the interpolated RGB back to hex + return rgbToHex(r, g, b); +} + +export { titleCase, getMousePositionFraction, getKeyframesSurrounding, invertPixels, lerp, lerpColor }; \ No newline at end of file