Add outliner and do work on importing from .beam
This commit is contained in:
parent
d6a1ecb18c
commit
8a647c1d3d
|
|
@ -1108,6 +1108,16 @@ class Bezier {
|
||||||
return new Bezier(S, nc1, nc2, E);
|
return new Bezier(S, nc1, nc2, E);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static fromJSON(json) {
|
||||||
|
return new Bezier(...json)
|
||||||
|
}
|
||||||
|
toJSON() {
|
||||||
|
return [this.points[0].x, this.points[0].y,
|
||||||
|
this.points[1].x, this.points[1].y,
|
||||||
|
this.points[2].x, this.points[2].y,
|
||||||
|
this.points[3].x, this.points[3].y]
|
||||||
|
}
|
||||||
|
|
||||||
static getUtils() {
|
static getUtils() {
|
||||||
return utils;
|
return utils;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
476
src/main.js
476
src/main.js
|
|
@ -3,8 +3,8 @@ import * as fitCurve from '/fit-curve.js';
|
||||||
import { Bezier } from "/bezier.js";
|
import { Bezier } from "/bezier.js";
|
||||||
import { Quadtree } from './quadtree.js';
|
import { Quadtree } from './quadtree.js';
|
||||||
import { createNewFileDialog, showNewFileDialog, closeDialog } from './newfile.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, drawBorderedRect, drawCenteredText, drawHorizontallyCenteredText, deepMerge, getPointNearBox, arraysAreEqual } from './utils.js';
|
import { titleCase, getMousePositionFraction, getKeyframesSurrounding, invertPixels, lerpColor, lerp, camelToWords, generateWaveform, floodFillRegion, getShapeAtPoint, hslToRgb, drawCheckerboardBackground, hexToHsl, hsvToRgb, hexToHsv, rgbToHex, clamp, drawBorderedRect, drawCenteredText, drawHorizontallyCenteredText, deepMerge, getPointNearBox, arraysAreEqual, drawRegularPolygon, getFileExtension, createModal, deeploop } from './utils.js';
|
||||||
import { backgroundColor, darkMode, foregroundColor, frameWidth, gutterHeight, highlight, iconSize, labelColor, layerHeight, layerWidth, scrubberColor, shade, shadow } from './styles.js';
|
import { backgroundColor, darkMode, foregroundColor, frameWidth, gutterHeight, highlight, iconSize, triangleSize, labelColor, layerHeight, layerWidth, scrubberColor, shade, shadow } from './styles.js';
|
||||||
import { Icon } from './icon.js';
|
import { Icon } from './icon.js';
|
||||||
const { writeTextFile: writeTextFile, readTextFile: readTextFile, writeFile: writeFile, readFile: readFile }= window.__TAURI__.fs;
|
const { writeTextFile: writeTextFile, readTextFile: readTextFile, writeFile: writeFile, readFile: readFile }= window.__TAURI__.fs;
|
||||||
const {
|
const {
|
||||||
|
|
@ -266,7 +266,8 @@ let config = {
|
||||||
fileHeight: 600,
|
fileHeight: 600,
|
||||||
framerate: 24,
|
framerate: 24,
|
||||||
recentFiles: [],
|
recentFiles: [],
|
||||||
scrollSpeed: 1
|
scrollSpeed: 1,
|
||||||
|
debug: false
|
||||||
}
|
}
|
||||||
|
|
||||||
function getShortcut(shortcut) {
|
function getShortcut(shortcut) {
|
||||||
|
|
@ -1419,22 +1420,6 @@ function selectVertex(context, mouse) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// function moldCurve(curve, mouse, oldmouse) {
|
|
||||||
// let diff = {x: mouse.x - oldmouse.x, y: mouse.y - oldmouse.y}
|
|
||||||
// let p = curve.project(mouse)
|
|
||||||
// let min_influence = 0.1
|
|
||||||
// const CP1 = {
|
|
||||||
// x: curve.points[1].x + diff.x*(1-p.t)*2,
|
|
||||||
// y: curve.points[1].y + diff.y*(1-p.t)*2
|
|
||||||
// }
|
|
||||||
// const CP2 = {
|
|
||||||
// x: curve.points[2].x + diff.x*(p.t)*2,
|
|
||||||
// y: curve.points[2].y + diff.y*(p.t)*2
|
|
||||||
// }
|
|
||||||
// return new Bezier(curve.points[0], CP1, CP2, curve.points[3])
|
|
||||||
// // return curve
|
|
||||||
// }
|
|
||||||
|
|
||||||
function moldCurve(curve, mouse, oldMouse, epsilon = 0.01) {
|
function moldCurve(curve, mouse, oldMouse, epsilon = 0.01) {
|
||||||
// Step 1: Find the closest point on the curve to the old mouse position
|
// Step 1: Find the closest point on the curve to the old mouse position
|
||||||
const projection = curve.project(oldMouse);
|
const projection = curve.project(oldMouse);
|
||||||
|
|
@ -1462,7 +1447,7 @@ function moldCurve(curve, mouse, oldMouse, epsilon = 0.01) {
|
||||||
};
|
};
|
||||||
const derivativeP2 = {
|
const derivativeP2 = {
|
||||||
x: (offset2.x - projection.x) / epsilon,
|
x: (offset2.x - projection.x) / epsilon,
|
||||||
y: (offset2.y - projection.y) / epsilon
|
y: (offset2.y - projection.y) / epsilon
|
||||||
};
|
};
|
||||||
|
|
||||||
// Step 5: Use the derivatives to move the projected point to the mouse
|
// Step 5: Use the derivatives to move the projected point to the mouse
|
||||||
|
|
@ -1595,6 +1580,27 @@ class Frame {
|
||||||
}
|
}
|
||||||
return newFrame
|
return newFrame
|
||||||
}
|
}
|
||||||
|
static fromJSON(json) {
|
||||||
|
const frame = new Frame(json.frameType, json.idx)
|
||||||
|
frame.keys = json.keys
|
||||||
|
for (let i in json.shapes) {
|
||||||
|
const shape = json.shapes[i]
|
||||||
|
frame.shapes.push(Shape.fromJSON(shape))
|
||||||
|
}
|
||||||
|
|
||||||
|
return frame
|
||||||
|
}
|
||||||
|
toJSON() {
|
||||||
|
const json = {}
|
||||||
|
json.frameType = this.frameType
|
||||||
|
json.idx = this.idx
|
||||||
|
json.keys = this.keys
|
||||||
|
json.shapes = []
|
||||||
|
for (let shape of this.shapes) {
|
||||||
|
json.shapes.push(shape.toJSON())
|
||||||
|
}
|
||||||
|
return json
|
||||||
|
}
|
||||||
addShape(shape, sendToBack) {
|
addShape(shape, sendToBack) {
|
||||||
if (sendToBack) {
|
if (sendToBack) {
|
||||||
this.shapes.unshift(shape)
|
this.shapes.unshift(shape)
|
||||||
|
|
@ -1648,6 +1654,39 @@ class Layer {
|
||||||
this.audible = true
|
this.audible = true
|
||||||
pointerList[this.idx] = this
|
pointerList[this.idx] = this
|
||||||
}
|
}
|
||||||
|
static fromJSON(json) {
|
||||||
|
const layer = new Layer(json.idx)
|
||||||
|
for (let i in json.children) {
|
||||||
|
const child = json.children[i]
|
||||||
|
layer.children.push(GraphicsObject.fromJSON(child))
|
||||||
|
}
|
||||||
|
layer.name = json.name
|
||||||
|
layer.frames = []
|
||||||
|
for (let i in json.frames) {
|
||||||
|
const frame = json.frames[i]
|
||||||
|
layer.frames.push(Frame.fromJSON(frame))
|
||||||
|
}
|
||||||
|
layer.visible = json.visible
|
||||||
|
layer.audible = json.audible
|
||||||
|
|
||||||
|
return layer
|
||||||
|
}
|
||||||
|
toJSON() {
|
||||||
|
const json = {}
|
||||||
|
json.idx = this.idx
|
||||||
|
json.children = []
|
||||||
|
for (let child of this.children) {
|
||||||
|
json.children.push(child.toJSON())
|
||||||
|
}
|
||||||
|
json.name = this.name
|
||||||
|
json.frames = []
|
||||||
|
for (let frame of this.frames) {
|
||||||
|
json.frames.push(frame.toJSON())
|
||||||
|
}
|
||||||
|
json.visible = this.visible
|
||||||
|
json.audible = this.audible
|
||||||
|
return json
|
||||||
|
}
|
||||||
getFrame(num) {
|
getFrame(num) {
|
||||||
if (this.frames[num]) {
|
if (this.frames[num]) {
|
||||||
if (this.frames[num].frameType == "keyframe") {
|
if (this.frames[num].frameType == "keyframe") {
|
||||||
|
|
@ -1858,10 +1897,10 @@ class Layer {
|
||||||
class AudioLayer {
|
class AudioLayer {
|
||||||
constructor(uuid, name) {
|
constructor(uuid, name) {
|
||||||
this.sounds = {}
|
this.sounds = {}
|
||||||
this.track = new Tone.Part(((time, sound) => {
|
this.track = new Tone.Part(((time, sound) => {
|
||||||
console.log(this.sounds[sound])
|
console.log(this.sounds[sound])
|
||||||
this.sounds[sound].player.start(time)
|
this.sounds[sound].player.start(time)
|
||||||
}))
|
}))
|
||||||
if (!uuid) {
|
if (!uuid) {
|
||||||
this.idx = uuidv4()
|
this.idx = uuidv4()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1874,6 +1913,22 @@ class AudioLayer {
|
||||||
}
|
}
|
||||||
this.audible = true
|
this.audible = true
|
||||||
}
|
}
|
||||||
|
static fromJSON(json) {
|
||||||
|
const audioLayer = new AudioLayer(json.idx, json.name)
|
||||||
|
// TODO: load audiolayer from json
|
||||||
|
audioLayer.sounds = {}
|
||||||
|
audioLayer.audible = json.audible
|
||||||
|
return audioLayer
|
||||||
|
}
|
||||||
|
toJSON() {
|
||||||
|
const json = {}
|
||||||
|
// TODO: build json from audiolayer
|
||||||
|
json.sounds = {}
|
||||||
|
json.audible = this.audible
|
||||||
|
json.idx = this.idx
|
||||||
|
json.name = this.name
|
||||||
|
return json
|
||||||
|
}
|
||||||
copy(idx) {
|
copy(idx) {
|
||||||
let newAudioLayer = new AudioLayer(idx.slice(0,8)+this.idx.slice(8), this.name)
|
let newAudioLayer = new AudioLayer(idx.slice(0,8)+this.idx.slice(8), this.name)
|
||||||
for (let soundIdx in this.sounds) {
|
for (let soundIdx in this.sounds) {
|
||||||
|
|
@ -2017,6 +2072,63 @@ class Shape extends BaseShape {
|
||||||
this.regionIdx = 0;
|
this.regionIdx = 0;
|
||||||
this.inProgress = true
|
this.inProgress = true
|
||||||
}
|
}
|
||||||
|
static fromJSON(json) {
|
||||||
|
const shape = new Shape(json.startx, json.starty, {
|
||||||
|
fillStyle: json.fillStyle,
|
||||||
|
fillImage: json.fillImage,
|
||||||
|
strokeStyle: json.strokeStyle,
|
||||||
|
lineWidth: json.lineWidth,
|
||||||
|
fillShape: json.filled,
|
||||||
|
strokeShape: json.stroked
|
||||||
|
}, json.idx, json.shapeId)
|
||||||
|
for (let curve of json.curves) {
|
||||||
|
shape.addCurve(Bezier.fromJSON(curve))
|
||||||
|
}
|
||||||
|
for (let region of json.regions) {
|
||||||
|
const curves = []
|
||||||
|
for (let curve of region.curves) {
|
||||||
|
curves.push(Bezier.fromJSON(curve))
|
||||||
|
}
|
||||||
|
shape.regions.push({
|
||||||
|
idx: region.idx,
|
||||||
|
curves: curves,
|
||||||
|
fillStyle: region.fillStyle,
|
||||||
|
filled: region.filled
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return shape
|
||||||
|
}
|
||||||
|
toJSON() {
|
||||||
|
const json = {}
|
||||||
|
json.startx = this.startx
|
||||||
|
json.starty = this.starty
|
||||||
|
json.fillStyle = this.fillStyle
|
||||||
|
json.fillImage = this.fillImage
|
||||||
|
json.strokeStyle = this.fillStyle
|
||||||
|
json.lineWidth = this.lineWidth
|
||||||
|
json.filled = this.filled
|
||||||
|
json.stroked = this.stroked
|
||||||
|
json.idx = this.idx
|
||||||
|
json.shapeId = this.shapeId
|
||||||
|
json.curves = []
|
||||||
|
for (let curve of this.curves) {
|
||||||
|
json.curves.push(curve.toJSON())
|
||||||
|
}
|
||||||
|
json.regions = []
|
||||||
|
for (let region of this.regions) {
|
||||||
|
const curves = []
|
||||||
|
for (let curve of region.curves) {
|
||||||
|
curves.push(curve.toJSON())
|
||||||
|
}
|
||||||
|
json.regions.push({
|
||||||
|
idx: region.idx,
|
||||||
|
curves: curves,
|
||||||
|
fillStyle: region.fillStyle,
|
||||||
|
filled: region.filled
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return json
|
||||||
|
}
|
||||||
addCurve(curve) {
|
addCurve(curve) {
|
||||||
if (curve.color == undefined) {
|
if (curve.color == undefined) {
|
||||||
curve.color = context.strokeStyle
|
curve.color = context.strokeStyle
|
||||||
|
|
@ -2320,6 +2432,46 @@ class GraphicsObject {
|
||||||
|
|
||||||
this.shapes = []
|
this.shapes = []
|
||||||
}
|
}
|
||||||
|
static fromJSON(json) {
|
||||||
|
const graphicsObject = new GraphicsObject(json.idx)
|
||||||
|
graphicsObject.x = json.x
|
||||||
|
graphicsObject.y = json.y
|
||||||
|
graphicsObject.rotation = json.rotation
|
||||||
|
graphicsObject.scale_x = json.scale_x
|
||||||
|
graphicsObject.scale_y = json.scale_y
|
||||||
|
graphicsObject.name = json.name
|
||||||
|
graphicsObject.currentFrameNum = json.currentFrameNum
|
||||||
|
graphicsObject.currentLayer = json.currentLayer
|
||||||
|
graphicsObject.layers = []
|
||||||
|
for (let layer of json.layers) {
|
||||||
|
graphicsObject.layers.push(Layer.fromJSON(layer))
|
||||||
|
}
|
||||||
|
for (let audioLayer of json.audioLayers) {
|
||||||
|
graphicsObject.audioLayers.push(AudioLayer.fromJSON(audioLayer))
|
||||||
|
}
|
||||||
|
return graphicsObject
|
||||||
|
}
|
||||||
|
toJSON() {
|
||||||
|
const json = {}
|
||||||
|
json.x = this.x
|
||||||
|
json.y = this.y
|
||||||
|
json.rotation = this.rotation
|
||||||
|
json.scale_x = this.scale_x
|
||||||
|
json.scale_y = this.scale_y
|
||||||
|
json.idx = this.idx
|
||||||
|
json.name = this.name
|
||||||
|
json.currentFrameNum = this.currentFrameNum
|
||||||
|
json.currentLayer = this.currentLayer
|
||||||
|
json.layers = []
|
||||||
|
for (let layer of this.layers) {
|
||||||
|
json.layers.push(layer.toJSON())
|
||||||
|
}
|
||||||
|
json.audioLayers = []
|
||||||
|
for (let audioLayer of this.audioLayers) {
|
||||||
|
json.audioLayers.push(audioLayer.toJSON())
|
||||||
|
}
|
||||||
|
return json
|
||||||
|
}
|
||||||
get activeLayer() {
|
get activeLayer() {
|
||||||
return this.layers[this.currentLayer]
|
return this.layers[this.currentLayer]
|
||||||
}
|
}
|
||||||
|
|
@ -2815,11 +2967,7 @@ function advanceFrame() {
|
||||||
|
|
||||||
// Calculate the time remaining for the next frame
|
// Calculate the time remaining for the next frame
|
||||||
const targetTimePerFrame = 1000 / config.framerate;
|
const targetTimePerFrame = 1000 / config.framerate;
|
||||||
const timeToWait = Math.max(0, targetTimePerFrame - elapsedTime); // Ensure no negative timeout
|
const timeToWait = Math.max(0, targetTimePerFrame - elapsedTime);
|
||||||
// const timeToWait = 1000 / config.framerate
|
|
||||||
console.log(timeToWait)
|
|
||||||
|
|
||||||
// Update lastFrameTime to the current time
|
|
||||||
lastFrameTime = now + timeToWait;
|
lastFrameTime = now + timeToWait;
|
||||||
setTimeout(advanceFrame, timeToWait)
|
setTimeout(advanceFrame, timeToWait)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -2843,7 +2991,7 @@ function decrementFrame() {
|
||||||
updateUI()
|
updateUI()
|
||||||
}
|
}
|
||||||
|
|
||||||
function _newFile(width, height, fps) {
|
function _newFile(width, height, fps, context=context) {
|
||||||
root = new GraphicsObject("root");
|
root = new GraphicsObject("root");
|
||||||
context.objectStack = [root]
|
context.objectStack = [root]
|
||||||
config.fileWidth = width
|
config.fileWidth = width
|
||||||
|
|
@ -2866,15 +3014,28 @@ async function newFile() {
|
||||||
|
|
||||||
async function _save(path) {
|
async function _save(path) {
|
||||||
try {
|
try {
|
||||||
|
function replacer(key, value) {
|
||||||
|
if (key === 'parent') {
|
||||||
|
return undefined; // Avoid circular references
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
const fileData = {
|
const fileData = {
|
||||||
version: "1.4",
|
version: "1.5",
|
||||||
width: config.fileWidth,
|
width: config.fileWidth,
|
||||||
height: config.fileHeight,
|
height: config.fileHeight,
|
||||||
fps: config.framerate,
|
fps: config.framerate,
|
||||||
actions: undoStack
|
actions: undoStack,
|
||||||
|
json: root.toJSON()
|
||||||
|
}
|
||||||
|
if (config.debug) {
|
||||||
|
// Pretty print file structure when debugging
|
||||||
|
const contents = JSON.stringify(fileData, null, 2);
|
||||||
|
await writeTextFile(path, contents)
|
||||||
|
} else {
|
||||||
|
const contents = JSON.stringify(fileData);
|
||||||
|
await writeTextFile(path, contents)
|
||||||
}
|
}
|
||||||
const contents = JSON.stringify(fileData);
|
|
||||||
await writeTextFile(path, contents)
|
|
||||||
filePath = path
|
filePath = path
|
||||||
addRecentFile(path)
|
addRecentFile(path)
|
||||||
lastSaveIndex = undoStack.length;
|
lastSaveIndex = undoStack.length;
|
||||||
|
|
@ -2907,7 +3068,7 @@ async function saveAs() {
|
||||||
if (path != undefined) _save(path);
|
if (path != undefined) _save(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _open(path) {
|
async function _open(path, returnJson=false) {
|
||||||
closeDialog()
|
closeDialog()
|
||||||
try {
|
try {
|
||||||
const contents = await readTextFile(path)
|
const contents = await readTextFile(path)
|
||||||
|
|
@ -2918,26 +3079,33 @@ async function _open(path) {
|
||||||
}
|
}
|
||||||
if (file.version >= minFileVersion) {
|
if (file.version >= minFileVersion) {
|
||||||
if (file.version < maxFileVersion) {
|
if (file.version < maxFileVersion) {
|
||||||
_newFile(file.width, file.height, file.fps)
|
if (returnJson) {
|
||||||
if (file.actions == undefined) {
|
if (file.json==undefined) {
|
||||||
await messageDialog("File has no content!", {title: "Parse error", kind: 'error'})
|
await messageDialog("Could not import from this file. Re-save it with a current version of Lightningbeam.")
|
||||||
return
|
}
|
||||||
}
|
return file.json
|
||||||
for (let action of file.actions) {
|
} else {
|
||||||
if (!(action.name in actions)) {
|
_newFile(file.width, file.height, file.fps)
|
||||||
await messageDialog(`Invalid action ${action.name}. File may be corrupt.`, { title: "Error", kind: 'error'})
|
if (file.actions == undefined) {
|
||||||
|
await messageDialog("File has no content!", {title: "Parse error", kind: 'error'})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.log(action.name)
|
for (let action of file.actions) {
|
||||||
await actions[action.name].execute(action.action)
|
if (!(action.name in actions)) {
|
||||||
undoStack.push(action)
|
await messageDialog(`Invalid action ${action.name}. File may be corrupt.`, { title: "Error", kind: 'error'})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log(action.name)
|
||||||
|
await actions[action.name].execute(action.action)
|
||||||
|
undoStack.push(action)
|
||||||
|
}
|
||||||
|
lastSaveIndex = undoStack.length;
|
||||||
|
filePath = path
|
||||||
|
// Tauri thinks it is setting the title here, but it isn't getting updated
|
||||||
|
await getCurrentWindow().setTitle(await basename(filePath))
|
||||||
|
addRecentFile(path)
|
||||||
|
updateUI()
|
||||||
}
|
}
|
||||||
lastSaveIndex = undoStack.length;
|
|
||||||
filePath = path
|
|
||||||
// Tauri thinks it is setting the title here, but it isn't getting updated
|
|
||||||
await getCurrentWindow().setTitle(await basename(filePath))
|
|
||||||
addRecentFile(path)
|
|
||||||
updateUI()
|
|
||||||
} else {
|
} else {
|
||||||
await messageDialog(`File ${path} was created in a newer version of Lightningbeam and cannot be opened in this version.`, { title: 'File version mismatch', kind: 'error' });
|
await messageDialog(`File ${path} was created in a newer version of Lightningbeam and cannot be opened in this version.`, { title: 'File version mismatch', kind: 'error' });
|
||||||
}
|
}
|
||||||
|
|
@ -2948,8 +3116,10 @@ async function _open(path) {
|
||||||
console.log(e )
|
console.log(e )
|
||||||
if (e instanceof SyntaxError) {
|
if (e instanceof SyntaxError) {
|
||||||
await messageDialog(`Could not parse ${path}, ${e.message}`, { title: 'Error', kind: 'error' })
|
await messageDialog(`Could not parse ${path}, ${e.message}`, { title: 'Error', kind: 'error' })
|
||||||
} else if (e.startsWith("failed to read file as text")) {
|
} else if (e instanceof String && e.startsWith("failed to read file as text")) {
|
||||||
await messageDialog(`Could not parse ${path}, is it actually a Lightningbeam file?`, { title: 'Error', kind: 'error' })
|
await messageDialog(`Could not parse ${path}, is it actually a Lightningbeam file?`, { title: 'Error', kind: 'error' })
|
||||||
|
} else {
|
||||||
|
console.error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2991,6 +3161,10 @@ async function importFile() {
|
||||||
name: 'Audio files',
|
name: 'Audio files',
|
||||||
extensions: ['mp3'],
|
extensions: ['mp3'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Lightningbeam files',
|
||||||
|
extensions: ['beam'],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
defaultPath: await documentDir(),
|
defaultPath: await documentDir(),
|
||||||
title: "Import File"
|
title: "Import File"
|
||||||
|
|
@ -3020,11 +3194,28 @@ async function importFile() {
|
||||||
];
|
];
|
||||||
if (path) {
|
if (path) {
|
||||||
const filename = await basename(path)
|
const filename = await basename(path)
|
||||||
const {dataURL, mimeType} = await convertToDataURL(path, imageMimeTypes.concat(audioMimeTypes));
|
if (getFileExtension(filename)=="beam") {
|
||||||
if (imageMimeTypes.indexOf(mimeType) != -1) {
|
function reassignIdxs(json) {
|
||||||
actions.addImageObject.create(50, 50, dataURL, 0, context.activeObject)
|
deeploop(json, (key, item) => {
|
||||||
|
if (item.idx in pointerList) {
|
||||||
|
item.idx = uuidv4()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const json = await _open(path, true)
|
||||||
|
if (json==undefined) return;
|
||||||
|
reassignIdxs(json)
|
||||||
|
createModal(outliner, json)
|
||||||
|
updateOutliner()
|
||||||
} else {
|
} else {
|
||||||
actions.addAudio.create(dataURL, context.activeObject, filename)
|
const {dataURL, mimeType} = await convertToDataURL(path, imageMimeTypes.concat(audioMimeTypes));
|
||||||
|
if (imageMimeTypes.indexOf(mimeType) != -1) {
|
||||||
|
actions.addImageObject.create(50, 50, dataURL, 0, context.activeObject)
|
||||||
|
} else {
|
||||||
|
actions.addAudio.create(dataURL, context.activeObject, filename)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4361,6 +4552,108 @@ function infopanel() {
|
||||||
return panel
|
return panel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function outliner(object=undefined) {
|
||||||
|
let outliner = document.createElement("canvas")
|
||||||
|
outliner.className = "outliner"
|
||||||
|
if (object==undefined) {
|
||||||
|
outliner.object = root
|
||||||
|
} else {
|
||||||
|
outliner.object = object
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastResizeTime = 0;
|
||||||
|
const throttleIntervalMs = 20;
|
||||||
|
|
||||||
|
function updateTimelineCanvasSize() {
|
||||||
|
const canvasStyles = window.getComputedStyle(outliner);
|
||||||
|
|
||||||
|
outliner.width = parseInt(canvasStyles.width);
|
||||||
|
outliner.height = parseInt(canvasStyles.height);
|
||||||
|
updateOutliner()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up ResizeObserver to watch for changes in the canvas size
|
||||||
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
const currentTime = Date.now();
|
||||||
|
|
||||||
|
// Only call updateTimelineCanvasSize if enough time has passed since the last call
|
||||||
|
// This prevents error messages about a ResizeObserver loop
|
||||||
|
if (currentTime - lastResizeTime > throttleIntervalMs) {
|
||||||
|
lastResizeTime = currentTime;
|
||||||
|
updateTimelineCanvasSize();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
resizeObserver.observe(outliner);
|
||||||
|
|
||||||
|
outliner.collapsed = {}
|
||||||
|
outliner.offsetX = 0
|
||||||
|
outliner.offsetY = 0
|
||||||
|
|
||||||
|
outliner.addEventListener('click', function (e) {
|
||||||
|
const mouse = getMousePos(outliner, e)
|
||||||
|
const mouseY = mouse.y; // Get the Y position of the click
|
||||||
|
const mouseX = mouse.x; // Get the X position (not used here, but can be used to check clicked area)
|
||||||
|
|
||||||
|
// Iterate again to check which object was clicked
|
||||||
|
let currentY = 20; // Starting y position
|
||||||
|
const stack = [{ object: outliner.object, indent: 0 }];
|
||||||
|
|
||||||
|
while (stack.length > 0) {
|
||||||
|
const { object, indent } = stack.pop();
|
||||||
|
|
||||||
|
|
||||||
|
// Check if the click was on this object
|
||||||
|
if (mouseY >= currentY - 20 && mouseY <= currentY) {
|
||||||
|
if (mouseX >= 0 && mouseX <= indent + 2*triangleSize) {
|
||||||
|
// Toggle the collapsed state of the object
|
||||||
|
outliner.collapsed[object.idx] = !outliner.collapsed[object.idx];
|
||||||
|
} else {
|
||||||
|
outliner.active = object
|
||||||
|
}
|
||||||
|
updateOutliner(); // Re-render the outliner
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the Y position for the next object
|
||||||
|
currentY += 20;
|
||||||
|
|
||||||
|
|
||||||
|
// If the object is collapsed, skip it
|
||||||
|
if (outliner.collapsed[object.idx]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the object has layers, add them to the stack
|
||||||
|
if (object.layers) {
|
||||||
|
for (let i = object.layers.length - 1; i >= 0; i--) {
|
||||||
|
const layer = object.layers[i];
|
||||||
|
stack.push({ object: layer, indent: indent + 20 });
|
||||||
|
}
|
||||||
|
} else if (object.children) {
|
||||||
|
for (let i = object.children.length - 1; i >= 0; i--) {
|
||||||
|
const child = object.children[i];
|
||||||
|
stack.push({ object: child, indent: indent + 40 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
outliner.addEventListener('wheel', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const deltaY = event.deltaY * config.scrollSpeed;
|
||||||
|
|
||||||
|
outliner.offsetY = Math.max(0, outliner.offsetY + deltaY);
|
||||||
|
|
||||||
|
const currentTime = Date.now();
|
||||||
|
if (currentTime - lastResizeTime > throttleIntervalMs) {
|
||||||
|
lastResizeTime = currentTime;
|
||||||
|
updateOutliner();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return outliner
|
||||||
|
}
|
||||||
|
|
||||||
async function startup() {
|
async function startup() {
|
||||||
await loadConfig()
|
await loadConfig()
|
||||||
createNewFileDialog(_newFile, _open, config);
|
createNewFileDialog(_newFile, _open, config);
|
||||||
|
|
@ -5177,6 +5470,68 @@ function updateInfopanel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateOutliner() {
|
||||||
|
const padding = 20; // pixels
|
||||||
|
for (let outliner of document.querySelectorAll('.outliner')) {
|
||||||
|
const x = 0;
|
||||||
|
let y = padding
|
||||||
|
const ctx = outliner.getContext("2d")
|
||||||
|
ctx.fillStyle = "white"
|
||||||
|
ctx.fillRect(0,0,outliner.width, outliner.height)
|
||||||
|
const stack = [{ object: outliner.object, indent: 0 }];
|
||||||
|
|
||||||
|
ctx.save()
|
||||||
|
ctx.translate(0, -outliner.offsetY)
|
||||||
|
|
||||||
|
// Iterate as long as there are items in the stack
|
||||||
|
while (stack.length > 0) {
|
||||||
|
const { object, indent } = stack.pop();
|
||||||
|
|
||||||
|
// Determine if the object is collapsed and draw the corresponding triangle
|
||||||
|
const triangleX = x + indent + triangleSize; // X position for the triangle
|
||||||
|
const triangleY = y - padding / 2; // Y position for the triangle (centered vertically)
|
||||||
|
|
||||||
|
if (outliner.active === object) {
|
||||||
|
ctx.fillStyle = "red"
|
||||||
|
ctx.fillRect(0, y - padding, outliner.width, padding)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outliner.collapsed[object.idx]) {
|
||||||
|
drawRegularPolygon(ctx, triangleX, triangleY, triangleSize, 3, "black")
|
||||||
|
} else {
|
||||||
|
drawRegularPolygon(ctx, triangleX, triangleY, triangleSize, 3, "black", Math.PI/2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the current object (GraphicsObject or Layer)
|
||||||
|
const label = `(${object.constructor.name}) ${object.name}`
|
||||||
|
ctx.fillStyle = "black";
|
||||||
|
// ctx.fillText(label, x + indent + 2*triangleSize, y);
|
||||||
|
drawHorizontallyCenteredText(ctx, label, x+indent+2*triangleSize, y-padding/2, padding*.75)
|
||||||
|
|
||||||
|
// Update the Y position for the next line
|
||||||
|
y += padding; // Space between lines (adjust as necessary)
|
||||||
|
|
||||||
|
if (outliner.collapsed[object.idx]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the object has layers, add them to the stack
|
||||||
|
if (object.layers) {
|
||||||
|
for (let i = object.layers.length - 1; i >= 0; i--) {
|
||||||
|
const layer = object.layers[i];
|
||||||
|
stack.push({ object: layer, indent: indent + padding });
|
||||||
|
}
|
||||||
|
} else if (object.children) {
|
||||||
|
for (let i = object.children.length - 1; i >= 0; i--) {
|
||||||
|
const child = object.children[i];
|
||||||
|
stack.push({ object: child, indent: indent + 2*padding });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function updateMenu() {
|
async function updateMenu() {
|
||||||
let activeFrame;
|
let activeFrame;
|
||||||
let activeKeyframe;
|
let activeKeyframe;
|
||||||
|
|
@ -5184,6 +5539,9 @@ async function updateMenu() {
|
||||||
let newKeyframeMenuItem;
|
let newKeyframeMenuItem;
|
||||||
let deleteFrameMenuItem;
|
let deleteFrameMenuItem;
|
||||||
|
|
||||||
|
// Move this
|
||||||
|
updateOutliner()
|
||||||
|
|
||||||
let recentFilesList = []
|
let recentFilesList = []
|
||||||
config.recentFiles.forEach((file) => {
|
config.recentFiles.forEach((file) => {
|
||||||
recentFilesList.push({
|
recentFilesList.push({
|
||||||
|
|
@ -5483,6 +5841,10 @@ const panes = {
|
||||||
name: "infopanel",
|
name: "infopanel",
|
||||||
func: infopanel
|
func: infopanel
|
||||||
},
|
},
|
||||||
|
outlineer: {
|
||||||
|
name: "outliner",
|
||||||
|
func: outliner
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _arrayBufferToBase64( buffer ) {
|
function _arrayBufferToBase64( buffer ) {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,9 @@ const gutterHeight = 15
|
||||||
const scrubberColor = "#cc2222"
|
const scrubberColor = "#cc2222"
|
||||||
const labelColor = darkMode ? "white" : "black"
|
const labelColor = darkMode ? "white" : "black"
|
||||||
|
|
||||||
|
|
||||||
|
const triangleSize = 5;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
darkMode,
|
darkMode,
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
|
|
@ -25,5 +28,6 @@ export {
|
||||||
frameWidth,
|
frameWidth,
|
||||||
gutterHeight,
|
gutterHeight,
|
||||||
scrubberColor,
|
scrubberColor,
|
||||||
labelColor
|
labelColor,
|
||||||
|
triangleSize
|
||||||
}
|
}
|
||||||
119
src/utils.js
119
src/utils.js
|
|
@ -600,6 +600,23 @@ function drawHorizontallyCenteredText(ctx, text, x, y, height) {
|
||||||
ctx.fillText(text, x, centerY);
|
ctx.fillText(text, x, centerY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function drawRegularPolygon(ctx, x, y, radius, sides, color, rotate = 0) {
|
||||||
|
ctx.beginPath();
|
||||||
|
|
||||||
|
// First point, adding rotation to the angle
|
||||||
|
ctx.moveTo(x + radius * Math.cos(0 + rotate), y + radius * Math.sin(0 + rotate));
|
||||||
|
|
||||||
|
// Draw the rest of the sides, adding the rotation to each angle
|
||||||
|
for (let i = 1; i <= sides; i++) {
|
||||||
|
let angle = (i * 2 * Math.PI) / sides + rotate; // Add rotation to the angle
|
||||||
|
ctx.lineTo(x + radius * Math.cos(angle), y + radius * Math.sin(angle));
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
function deepMerge(target, source) {
|
function deepMerge(target, source) {
|
||||||
// If either target or source is not an object, return source (base case)
|
// If either target or source is not an object, return source (base case)
|
||||||
if (typeof target !== 'object' || target === null) {
|
if (typeof target !== 'object' || target === null) {
|
||||||
|
|
@ -666,6 +683,102 @@ function arraysAreEqual(arr1, arr2) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getFileExtension(filename) {
|
||||||
|
const dotIndex = filename.lastIndexOf('.'); // Find the last period in the filename
|
||||||
|
if (dotIndex === -1) return ''; // No extension found (no dot in filename)
|
||||||
|
return filename.substring(dotIndex + 1); // Extract the extension
|
||||||
|
}
|
||||||
|
|
||||||
|
function createModal(contentFunction, arg) {
|
||||||
|
// Create the modal overlay
|
||||||
|
const modalOverlay = document.createElement('div');
|
||||||
|
modalOverlay.style.position = 'fixed';
|
||||||
|
modalOverlay.style.top = 0;
|
||||||
|
modalOverlay.style.left = 0;
|
||||||
|
modalOverlay.style.width = '100%';
|
||||||
|
modalOverlay.style.height = '100%';
|
||||||
|
modalOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
|
||||||
|
modalOverlay.style.zIndex = 1000;
|
||||||
|
modalOverlay.style.display = 'flex';
|
||||||
|
modalOverlay.style.alignItems = 'center';
|
||||||
|
modalOverlay.style.justifyContent = 'center';
|
||||||
|
|
||||||
|
// Create the modal container
|
||||||
|
const modalContainer = document.createElement('div');
|
||||||
|
modalContainer.style.backgroundColor = 'white';
|
||||||
|
modalContainer.style.padding = '20px';
|
||||||
|
modalContainer.style.borderRadius = '8px';
|
||||||
|
modalContainer.style.maxWidth = '80%';
|
||||||
|
modalContainer.style.maxHeight = '80%';
|
||||||
|
modalContainer.style.overflowY = 'auto';
|
||||||
|
|
||||||
|
const modalContent = contentFunction(arg);
|
||||||
|
modalContainer.appendChild(modalContent);
|
||||||
|
|
||||||
|
// Create Ok and Cancel buttons
|
||||||
|
const buttonContainer = document.createElement('div');
|
||||||
|
buttonContainer.style.display = 'flex';
|
||||||
|
buttonContainer.style.justifyContent = 'space-between';
|
||||||
|
buttonContainer.style.marginTop = '20px';
|
||||||
|
|
||||||
|
const okButton = document.createElement('button');
|
||||||
|
okButton.innerText = 'Ok';
|
||||||
|
okButton.style.padding = '10px 20px';
|
||||||
|
okButton.style.fontSize = '16px';
|
||||||
|
okButton.style.cursor = 'pointer';
|
||||||
|
okButton.style.backgroundColor = '#4CAF50';
|
||||||
|
okButton.style.color = 'white';
|
||||||
|
okButton.style.border = 'none';
|
||||||
|
okButton.style.borderRadius = '4px';
|
||||||
|
|
||||||
|
const cancelButton = document.createElement('button');
|
||||||
|
cancelButton.innerText = 'Cancel';
|
||||||
|
cancelButton.style.padding = '10px 20px';
|
||||||
|
cancelButton.style.fontSize = '16px';
|
||||||
|
cancelButton.style.cursor = 'pointer';
|
||||||
|
cancelButton.style.backgroundColor = '#f44336';
|
||||||
|
cancelButton.style.color = 'white';
|
||||||
|
cancelButton.style.border = 'none';
|
||||||
|
cancelButton.style.borderRadius = '4px';
|
||||||
|
|
||||||
|
// Add button events
|
||||||
|
okButton.addEventListener('click', () => {
|
||||||
|
modalOverlay.remove(); // Close modal on Ok
|
||||||
|
console.log(modalContent.active)
|
||||||
|
// You can add additional action here if needed
|
||||||
|
});
|
||||||
|
|
||||||
|
cancelButton.addEventListener('click', () => {
|
||||||
|
modalOverlay.remove(); // Close modal on Cancel
|
||||||
|
});
|
||||||
|
|
||||||
|
// Append buttons to the container
|
||||||
|
buttonContainer.appendChild(okButton);
|
||||||
|
buttonContainer.appendChild(cancelButton);
|
||||||
|
|
||||||
|
// Add button container to the modal
|
||||||
|
modalContainer.appendChild(buttonContainer);
|
||||||
|
|
||||||
|
// Add the modal container to the overlay
|
||||||
|
modalOverlay.appendChild(modalContainer);
|
||||||
|
|
||||||
|
// Append the modal overlay to the body
|
||||||
|
document.body.appendChild(modalOverlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deeploop(obj, callback) {
|
||||||
|
// Loop through all the entries in the object
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
// Call the callback with the key and value
|
||||||
|
callback(key, value);
|
||||||
|
|
||||||
|
// If the value is an object, recursively call deeploop on it
|
||||||
|
if (typeof value === 'object' && value !== null) {
|
||||||
|
deeploop(value, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
titleCase,
|
titleCase,
|
||||||
getMousePositionFraction,
|
getMousePositionFraction,
|
||||||
|
|
@ -687,7 +800,11 @@ export {
|
||||||
drawBorderedRect,
|
drawBorderedRect,
|
||||||
drawCenteredText,
|
drawCenteredText,
|
||||||
drawHorizontallyCenteredText,
|
drawHorizontallyCenteredText,
|
||||||
|
drawRegularPolygon,
|
||||||
deepMerge,
|
deepMerge,
|
||||||
getPointNearBox,
|
getPointNearBox,
|
||||||
arraysAreEqual
|
arraysAreEqual,
|
||||||
|
getFileExtension,
|
||||||
|
createModal,
|
||||||
|
deeploop
|
||||||
};
|
};
|
||||||
Loading…
Reference in New Issue