@editframe/elements 0.15.0-beta.8 → 0.16.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/EF_FRAMEGEN.d.ts +14 -10
- package/dist/EF_FRAMEGEN.js +17 -28
- package/dist/elements/EFCaptions.js +0 -7
- package/dist/elements/EFImage.js +0 -4
- package/dist/elements/EFMedia.d.ts +13 -7
- package/dist/elements/EFMedia.js +217 -111
- package/dist/elements/EFSourceMixin.js +2 -1
- package/dist/elements/EFTemporal.browsertest.d.ts +4 -3
- package/dist/elements/EFTemporal.d.ts +14 -11
- package/dist/elements/EFTemporal.js +63 -87
- package/dist/elements/EFTimegroup.d.ts +2 -4
- package/dist/elements/EFTimegroup.js +15 -103
- package/dist/elements/EFVideo.js +3 -1
- package/dist/elements/EFWaveform.d.ts +3 -2
- package/dist/elements/EFWaveform.js +39 -26
- package/dist/elements/durationConverter.d.ts +8 -8
- package/dist/elements/durationConverter.js +2 -2
- package/dist/elements/updateAnimations.d.ts +9 -0
- package/dist/elements/updateAnimations.js +62 -0
- package/dist/getRenderInfo.d.ts +51 -0
- package/dist/getRenderInfo.js +72 -0
- package/dist/gui/EFFilmstrip.js +7 -16
- package/dist/gui/EFFitScale.d.ts +27 -0
- package/dist/gui/EFFitScale.js +138 -0
- package/dist/gui/EFWorkbench.d.ts +2 -5
- package/dist/gui/EFWorkbench.js +13 -56
- package/dist/gui/TWMixin.css.js +1 -1
- package/dist/gui/TWMixin.js +14 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -1
- package/dist/style.css +6 -3
- package/package.json +9 -4
- package/src/elements/EFCaptions.browsertest.ts +2 -2
- package/src/elements/EFCaptions.ts +0 -7
- package/src/elements/EFImage.browsertest.ts +2 -2
- package/src/elements/EFImage.ts +0 -4
- package/src/elements/EFMedia.browsertest.ts +14 -14
- package/src/elements/EFMedia.ts +291 -136
- package/src/elements/EFSourceMixin.ts +4 -4
- package/src/elements/EFTemporal.browsertest.ts +64 -31
- package/src/elements/EFTemporal.ts +99 -119
- package/src/elements/EFTimegroup.ts +15 -133
- package/src/elements/EFVideo.ts +3 -1
- package/src/elements/EFWaveform.ts +54 -39
- package/src/elements/durationConverter.ts +9 -4
- package/src/elements/updateAnimations.ts +88 -0
- package/src/gui/ContextMixin.ts +0 -3
- package/src/gui/EFFilmstrip.ts +7 -16
- package/src/gui/EFFitScale.ts +152 -0
- package/src/gui/EFWorkbench.ts +18 -65
- package/src/gui/TWMixin.ts +19 -2
- package/types.json +1 -1
|
@@ -45,8 +45,15 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
|
|
|
45
45
|
type: String,
|
|
46
46
|
attribute: "mode",
|
|
47
47
|
})
|
|
48
|
-
mode:
|
|
49
|
-
"
|
|
48
|
+
mode:
|
|
49
|
+
| "roundBars"
|
|
50
|
+
| "bars"
|
|
51
|
+
| "bricks"
|
|
52
|
+
| "line"
|
|
53
|
+
| "curve"
|
|
54
|
+
| "pixel"
|
|
55
|
+
| "wave"
|
|
56
|
+
| "spikes" = "bars";
|
|
50
57
|
|
|
51
58
|
@property({ type: String })
|
|
52
59
|
color = "currentColor";
|
|
@@ -54,6 +61,9 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
|
|
|
54
61
|
@property({ type: String, reflect: true })
|
|
55
62
|
target = "";
|
|
56
63
|
|
|
64
|
+
@property({ type: Number, attribute: "bar-spacing" })
|
|
65
|
+
barSpacing = 0.5;
|
|
66
|
+
|
|
57
67
|
@state()
|
|
58
68
|
targetElement: EFAudio | EFVideo | null = null;
|
|
59
69
|
|
|
@@ -147,7 +157,7 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
|
|
|
147
157
|
const waveHeight = canvas.height;
|
|
148
158
|
|
|
149
159
|
const totalBars = frequencyData.length;
|
|
150
|
-
const paddingInner =
|
|
160
|
+
const paddingInner = this.barSpacing;
|
|
151
161
|
const paddingOuter = 0.01;
|
|
152
162
|
const availableWidth = waveWidth;
|
|
153
163
|
const barWidth =
|
|
@@ -157,7 +167,7 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
|
|
|
157
167
|
const path = new Path2D();
|
|
158
168
|
|
|
159
169
|
frequencyData.forEach((value, i) => {
|
|
160
|
-
const normalizedValue =
|
|
170
|
+
const normalizedValue = value / 255;
|
|
161
171
|
const barHeight = normalizedValue * waveHeight;
|
|
162
172
|
const y = (waveHeight - barHeight) / 2;
|
|
163
173
|
const x = waveWidth * paddingOuter + i * (barWidth * (1 + paddingInner));
|
|
@@ -183,7 +193,7 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
|
|
|
183
193
|
const maxBricks = Math.floor(waveHeight / (boxSize + verticalGap)); // Account for gaps in height calculation
|
|
184
194
|
|
|
185
195
|
frequencyData.forEach((value, i) => {
|
|
186
|
-
const normalizedValue =
|
|
196
|
+
const normalizedValue = value / 255;
|
|
187
197
|
const brickCount = Math.floor(normalizedValue * maxBricks);
|
|
188
198
|
|
|
189
199
|
for (let j = 0; j < brickCount; j++) {
|
|
@@ -206,7 +216,7 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
|
|
|
206
216
|
|
|
207
217
|
// Similar padding calculation as drawBars
|
|
208
218
|
const totalBars = frequencyData.length;
|
|
209
|
-
const paddingInner =
|
|
219
|
+
const paddingInner = this.barSpacing;
|
|
210
220
|
const paddingOuter = 0.01;
|
|
211
221
|
const availableWidth = waveWidth;
|
|
212
222
|
const barWidth =
|
|
@@ -218,7 +228,7 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
|
|
|
218
228
|
const path = new Path2D();
|
|
219
229
|
|
|
220
230
|
frequencyData.forEach((value, i) => {
|
|
221
|
-
const normalizedValue =
|
|
231
|
+
const normalizedValue = value / 255;
|
|
222
232
|
const height = normalizedValue * waveHeight; // Use full wave height like in drawBars
|
|
223
233
|
const x = waveWidth * paddingOuter + i * (barWidth * (1 + paddingInner));
|
|
224
234
|
const y = (waveHeight - height) / 2; // Center vertically
|
|
@@ -231,52 +241,49 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
|
|
|
231
241
|
ctx.fill(path);
|
|
232
242
|
}
|
|
233
243
|
|
|
234
|
-
protected
|
|
235
|
-
ctx: CanvasRenderingContext2D,
|
|
236
|
-
frequencyData: Uint8Array,
|
|
237
|
-
) {
|
|
244
|
+
protected drawLine(ctx: CanvasRenderingContext2D, frequencyData: Uint8Array) {
|
|
238
245
|
const canvas = ctx.canvas;
|
|
239
246
|
const waveWidth = canvas.width;
|
|
240
247
|
const waveHeight = canvas.height;
|
|
241
|
-
const baseline = waveHeight / 2;
|
|
242
|
-
const barWidth = (waveWidth / frequencyData.length) * 0.8;
|
|
243
248
|
|
|
244
249
|
ctx.clearRect(0, 0, waveWidth, waveHeight);
|
|
250
|
+
const path = new Path2D();
|
|
245
251
|
|
|
246
|
-
//
|
|
247
|
-
const
|
|
248
|
-
const barsPath = new Path2D();
|
|
249
|
-
|
|
250
|
-
// Draw baseline
|
|
251
|
-
baselinePath.moveTo(0, baseline);
|
|
252
|
-
baselinePath.lineTo(waveWidth, baseline);
|
|
252
|
+
// Sample fewer points to make sharp angles more visible
|
|
253
|
+
const sampleRate = 1; // Only use every 4th point
|
|
253
254
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
const x = i * (waveWidth / frequencyData.length);
|
|
258
|
-
const y = baseline - height;
|
|
259
|
-
barsPath.rect(x, y, barWidth, Math.max(height * 2, 1));
|
|
260
|
-
});
|
|
255
|
+
for (let i = 0; i < frequencyData.length; i += sampleRate) {
|
|
256
|
+
const x = (i / frequencyData.length) * waveWidth;
|
|
257
|
+
const y = (1 - (frequencyData[i] ?? 0) / 255) * waveHeight;
|
|
261
258
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
259
|
+
if (i === 0) {
|
|
260
|
+
path.moveTo(x, y);
|
|
261
|
+
} else {
|
|
262
|
+
path.lineTo(x, y);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// Ensure we draw to the end
|
|
266
|
+
const lastX = waveWidth;
|
|
267
|
+
const lastY =
|
|
268
|
+
(1 - (frequencyData[frequencyData.length - 1] ?? 0) / 255) * waveHeight;
|
|
269
|
+
path.lineTo(lastX, lastY);
|
|
265
270
|
|
|
266
|
-
|
|
267
|
-
ctx.
|
|
271
|
+
ctx.lineWidth = this.lineWidth;
|
|
272
|
+
ctx.stroke(path);
|
|
268
273
|
}
|
|
269
274
|
|
|
270
|
-
protected
|
|
275
|
+
protected drawCurve(
|
|
276
|
+
ctx: CanvasRenderingContext2D,
|
|
277
|
+
frequencyData: Uint8Array,
|
|
278
|
+
) {
|
|
271
279
|
const canvas = ctx.canvas;
|
|
272
280
|
const waveWidth = canvas.width;
|
|
273
281
|
const waveHeight = canvas.height;
|
|
274
282
|
|
|
275
283
|
ctx.clearRect(0, 0, waveWidth, waveHeight);
|
|
276
|
-
|
|
277
|
-
// Create a single Path2D object for the curve
|
|
278
284
|
const path = new Path2D();
|
|
279
285
|
|
|
286
|
+
// Draw smooth curves between points using quadratic curves
|
|
280
287
|
frequencyData.forEach((value, i) => {
|
|
281
288
|
const x = (i / frequencyData.length) * waveWidth;
|
|
282
289
|
const y = (1 - value / 255) * waveHeight;
|
|
@@ -284,7 +291,11 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
|
|
|
284
291
|
if (i === 0) {
|
|
285
292
|
path.moveTo(x, y);
|
|
286
293
|
} else {
|
|
287
|
-
|
|
294
|
+
const prevX = ((i - 1) / frequencyData.length) * waveWidth;
|
|
295
|
+
const prevY = (1 - (frequencyData[i - 1] ?? 0) / 255) * waveHeight;
|
|
296
|
+
const xc = (prevX + x) / 2;
|
|
297
|
+
const yc = (prevY + y) / 2;
|
|
298
|
+
path.quadraticCurveTo(prevX, prevY, xc, yc);
|
|
288
299
|
}
|
|
289
300
|
});
|
|
290
301
|
|
|
@@ -306,7 +317,7 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
|
|
|
306
317
|
const path = new Path2D();
|
|
307
318
|
|
|
308
319
|
frequencyData.forEach((value, i) => {
|
|
309
|
-
const normalizedValue =
|
|
320
|
+
const normalizedValue = value / 255;
|
|
310
321
|
const x = i * (waveWidth / frequencyData.length);
|
|
311
322
|
const barHeight = normalizedValue * (waveHeight / 2); // Half height since we extend both ways
|
|
312
323
|
const y = baseline - barHeight;
|
|
@@ -469,7 +480,8 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
|
|
|
469
480
|
if (!ctx) return;
|
|
470
481
|
|
|
471
482
|
const frequencyData = this.targetElement.frequencyDataTask.value;
|
|
472
|
-
|
|
483
|
+
const byteTimeData = this.targetElement.byteTimeDomainTask.value;
|
|
484
|
+
if (!frequencyData || !byteTimeData) return;
|
|
473
485
|
|
|
474
486
|
ctx.save();
|
|
475
487
|
if (this.color === "currentColor") {
|
|
@@ -490,7 +502,10 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
|
|
|
490
502
|
this.drawBricks(ctx, frequencyData);
|
|
491
503
|
break;
|
|
492
504
|
case "line":
|
|
493
|
-
this.drawLine(ctx,
|
|
505
|
+
this.drawLine(ctx, byteTimeData);
|
|
506
|
+
break;
|
|
507
|
+
case "curve":
|
|
508
|
+
this.drawCurve(ctx, byteTimeData);
|
|
494
509
|
break;
|
|
495
510
|
case "pixel":
|
|
496
511
|
this.drawPixel(ctx, frequencyData);
|
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
import { parseTimeToMs } from "./parseTimeToMs.js";
|
|
2
2
|
|
|
3
3
|
export const durationConverter = {
|
|
4
|
-
fromAttribute: (value: string)
|
|
5
|
-
|
|
4
|
+
fromAttribute: (value: string | null) =>
|
|
5
|
+
value === null ? null : parseTimeToMs(value),
|
|
6
|
+
toAttribute: (value: number | null) => (value === null ? null : `${value}s`),
|
|
6
7
|
};
|
|
7
8
|
|
|
8
9
|
const positiveDurationConverter = (error: string) => {
|
|
9
10
|
return {
|
|
10
|
-
fromAttribute: (value: string): number => {
|
|
11
|
+
fromAttribute: (value: string | null): number | null => {
|
|
12
|
+
if (value === null) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
11
15
|
if (value.startsWith("-")) {
|
|
12
16
|
throw new Error(error);
|
|
13
17
|
}
|
|
14
18
|
return parseTimeToMs(value);
|
|
15
19
|
},
|
|
16
|
-
toAttribute: (value: number) =>
|
|
20
|
+
toAttribute: (value: number | null) =>
|
|
21
|
+
value === null ? null : `${value}s`,
|
|
17
22
|
};
|
|
18
23
|
};
|
|
19
24
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { isEFTemporal } from "./EFTemporal.ts";
|
|
2
|
+
import type { EFTimegroup } from "./EFTimegroup.ts";
|
|
3
|
+
|
|
4
|
+
export const updateAnimations = (
|
|
5
|
+
element: HTMLElement & {
|
|
6
|
+
currentTimeMs: number;
|
|
7
|
+
durationMs: number;
|
|
8
|
+
rootTimegroup?: EFTimegroup;
|
|
9
|
+
parentTimegroup?: EFTimegroup;
|
|
10
|
+
startTimeMs: number;
|
|
11
|
+
endTimeMs: number;
|
|
12
|
+
},
|
|
13
|
+
) => {
|
|
14
|
+
element.style.setProperty(
|
|
15
|
+
"--ef-progress",
|
|
16
|
+
`${Math.max(0, Math.min(1, element.currentTimeMs / element.durationMs)) * 100}%`,
|
|
17
|
+
);
|
|
18
|
+
const timelineTimeMs = (element.rootTimegroup ?? element).currentTimeMs;
|
|
19
|
+
if (
|
|
20
|
+
element.startTimeMs > timelineTimeMs ||
|
|
21
|
+
element.endTimeMs < timelineTimeMs
|
|
22
|
+
) {
|
|
23
|
+
element.style.display = "none";
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
element.style.display = "";
|
|
27
|
+
const animations = element.getAnimations({ subtree: true });
|
|
28
|
+
element.style.setProperty("--ef-duration", `${element.durationMs}ms`);
|
|
29
|
+
element.style.setProperty(
|
|
30
|
+
"--ef-transition-duration",
|
|
31
|
+
`${element.parentTimegroup?.overlapMs ?? 0}ms`,
|
|
32
|
+
);
|
|
33
|
+
element.style.setProperty(
|
|
34
|
+
"--ef-transition-out-start",
|
|
35
|
+
`${element.durationMs - (element.parentTimegroup?.overlapMs ?? 0)}ms`,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
for (const animation of animations) {
|
|
39
|
+
if (animation.playState === "running") {
|
|
40
|
+
animation.pause();
|
|
41
|
+
}
|
|
42
|
+
const effect = animation.effect;
|
|
43
|
+
if (!(effect && effect instanceof KeyframeEffect)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const target = effect.target;
|
|
47
|
+
// TODO: better generalize work avoidance for temporal elements
|
|
48
|
+
if (!target) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (target.closest("ef-timegroup") !== element) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const timing = effect.getTiming();
|
|
56
|
+
const duration = Number(timing.duration) ?? 0;
|
|
57
|
+
const delay = Number(timing.delay) ?? 0;
|
|
58
|
+
const iterations = Number(timing.iterations) ?? 1;
|
|
59
|
+
|
|
60
|
+
const timeTarget = isEFTemporal(target)
|
|
61
|
+
? target
|
|
62
|
+
: target.closest("ef-timegroup");
|
|
63
|
+
if (!timeTarget) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const currentTime = timeTarget.ownCurrentTimeMs;
|
|
68
|
+
|
|
69
|
+
// Handle delay - don't start animation until delay is complete
|
|
70
|
+
if (currentTime < delay) {
|
|
71
|
+
animation.currentTime = 0;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const currentIteration = Math.floor((currentTime - delay) / duration);
|
|
76
|
+
const currentIterationTime = (currentTime - delay) % duration;
|
|
77
|
+
|
|
78
|
+
if (currentIteration >= iterations) {
|
|
79
|
+
// Stop just before the end to prevent DOM removal
|
|
80
|
+
animation.currentTime = duration - 0.01;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Ensure we never reach exactly duration
|
|
85
|
+
animation.currentTime =
|
|
86
|
+
Math.min(currentIterationTime, duration - 0.01) + delay;
|
|
87
|
+
}
|
|
88
|
+
};
|
package/src/gui/ContextMixin.ts
CHANGED
|
@@ -129,9 +129,6 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
|
|
|
129
129
|
@property({ type: Boolean, reflect: true })
|
|
130
130
|
loop = false;
|
|
131
131
|
|
|
132
|
-
// @state()
|
|
133
|
-
// private stageScale = 1;
|
|
134
|
-
|
|
135
132
|
@property({ type: Boolean })
|
|
136
133
|
rendering = false;
|
|
137
134
|
|
package/src/gui/EFFilmstrip.ts
CHANGED
|
@@ -87,31 +87,22 @@ class FilmstripItem extends TWMixin(LitElement) {
|
|
|
87
87
|
@property({ type: Number })
|
|
88
88
|
pixelsPerMs = 0.04;
|
|
89
89
|
|
|
90
|
+
// Gutter styles represent the entire source media.
|
|
91
|
+
// If there is no trim, then the gutter and trim portion are the same.
|
|
90
92
|
get gutterStyles() {
|
|
91
|
-
if (this.element.sourceInMs || this.element.sourceOutMs) {
|
|
92
|
-
return {
|
|
93
|
-
position: "relative",
|
|
94
|
-
left: `${this.pixelsPerMs * (this.element.startTimeWithinParentMs - this.element.trimStartMs - this.element.sourceInMs)}px`,
|
|
95
|
-
width: `${this.pixelsPerMs * (this.element.durationMs + this.element.trimStartMs + this.element.trimEndMs + this.element.sourceOutMs + this.element.sourceInMs)}px`,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
93
|
return {
|
|
99
94
|
position: "relative",
|
|
100
|
-
left: `${this.pixelsPerMs * (this.element.startTimeWithinParentMs - this.element.
|
|
101
|
-
width: `${this.pixelsPerMs * (this.element.
|
|
95
|
+
left: `${this.pixelsPerMs * (this.element.startTimeWithinParentMs - this.element.sourceStartMs)}px`,
|
|
96
|
+
width: `${this.pixelsPerMs * (this.element.intrinsicDurationMs ?? this.element.durationMs)}px`,
|
|
102
97
|
};
|
|
103
98
|
}
|
|
104
99
|
|
|
100
|
+
// Trim portion is the section of source that will be placed in the timeline
|
|
101
|
+
// If there is no trim, then the gutter and trim portion are the same.
|
|
105
102
|
get trimPortionStyles() {
|
|
106
|
-
if (this.element.sourceInMs || this.element.sourceOutMs) {
|
|
107
|
-
return {
|
|
108
|
-
width: `${this.pixelsPerMs * this.element.durationMs}px`,
|
|
109
|
-
left: `${this.pixelsPerMs * (this.element.trimStartMs + this.element.sourceInMs)}px`,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
103
|
return {
|
|
113
104
|
width: `${this.pixelsPerMs * this.element.durationMs}px`,
|
|
114
|
-
left: `${this.pixelsPerMs * this.element.
|
|
105
|
+
left: `${this.pixelsPerMs * this.element.sourceStartMs}px`,
|
|
115
106
|
};
|
|
116
107
|
}
|
|
117
108
|
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { LitElement } from "lit";
|
|
2
|
+
import { customElement, state } from "lit/decorators.js";
|
|
3
|
+
import { createRef } from "lit/directives/ref.js";
|
|
4
|
+
|
|
5
|
+
@customElement("ef-fit-scale")
|
|
6
|
+
export class EFFitScale extends LitElement {
|
|
7
|
+
containerRef = createRef<HTMLDivElement>();
|
|
8
|
+
contentRef = createRef<HTMLSlotElement>();
|
|
9
|
+
|
|
10
|
+
createRenderRoot() {
|
|
11
|
+
Object.assign(this.style, {
|
|
12
|
+
display: "grid",
|
|
13
|
+
width: "100%",
|
|
14
|
+
height: "100%",
|
|
15
|
+
gridTemplateColumns: "100%",
|
|
16
|
+
gridTemplateRows: "100%",
|
|
17
|
+
overflow: "hidden",
|
|
18
|
+
boxSizing: "border-box",
|
|
19
|
+
contain: "strict",
|
|
20
|
+
position: "relative",
|
|
21
|
+
});
|
|
22
|
+
this.id = `${this.uniqueId}`;
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
uniqueId = Math.random().toString(36).substring(2, 15);
|
|
27
|
+
|
|
28
|
+
@state()
|
|
29
|
+
private scale = 1;
|
|
30
|
+
|
|
31
|
+
private animationFrameId?: number;
|
|
32
|
+
|
|
33
|
+
get contentChild() {
|
|
34
|
+
const firstElement = this.children[0];
|
|
35
|
+
if (!firstElement) return null;
|
|
36
|
+
|
|
37
|
+
let current: Element = firstElement;
|
|
38
|
+
while (current) {
|
|
39
|
+
if (current instanceof HTMLSlotElement) {
|
|
40
|
+
const assigned = current.assignedElements()[0];
|
|
41
|
+
if (!assigned) break;
|
|
42
|
+
current = assigned;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const display = window.getComputedStyle(current).display;
|
|
47
|
+
if (display !== "contents" && display !== "none") {
|
|
48
|
+
return current as HTMLElement;
|
|
49
|
+
}
|
|
50
|
+
const firstChild = current.children[0];
|
|
51
|
+
if (!firstChild) break;
|
|
52
|
+
current = firstChild;
|
|
53
|
+
}
|
|
54
|
+
return firstElement as HTMLElement; // Fallback to first element if no non-contents found
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get scaleInfo() {
|
|
58
|
+
if (!this.contentChild) {
|
|
59
|
+
return {
|
|
60
|
+
scale: 1,
|
|
61
|
+
containerWidth: 0,
|
|
62
|
+
containerHeight: 0,
|
|
63
|
+
contentWidth: 0,
|
|
64
|
+
contentHeight: 0,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const containerWidth = this.clientWidth;
|
|
69
|
+
const containerHeight = this.clientHeight;
|
|
70
|
+
const contentWidth = this.contentChild.clientWidth;
|
|
71
|
+
const contentHeight = this.contentChild.clientHeight;
|
|
72
|
+
|
|
73
|
+
const containerRatio = containerWidth / containerHeight;
|
|
74
|
+
const contentRatio = contentWidth / contentHeight;
|
|
75
|
+
|
|
76
|
+
const scale =
|
|
77
|
+
containerRatio > contentRatio
|
|
78
|
+
? containerHeight / contentHeight
|
|
79
|
+
: containerWidth / contentWidth;
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
scale,
|
|
83
|
+
containerWidth,
|
|
84
|
+
containerHeight,
|
|
85
|
+
contentWidth,
|
|
86
|
+
contentHeight,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
scaleLastSetOn: HTMLElement | null = null;
|
|
91
|
+
|
|
92
|
+
setScale = () => {
|
|
93
|
+
if (this.isConnected) {
|
|
94
|
+
const { scale } = this.scaleInfo;
|
|
95
|
+
if (this.contentChild) {
|
|
96
|
+
const containerRect = this.getBoundingClientRect();
|
|
97
|
+
const contentRect = this.contentChild.getBoundingClientRect();
|
|
98
|
+
|
|
99
|
+
const unscaledWidth = contentRect.width / this.scale;
|
|
100
|
+
const unscaledHeight = contentRect.height / this.scale;
|
|
101
|
+
const scaledWidth = unscaledWidth * scale;
|
|
102
|
+
const scaledHeight = unscaledHeight * scale;
|
|
103
|
+
const translateX = (containerRect.width - scaledWidth) / 2;
|
|
104
|
+
const translateY = (containerRect.height - scaledHeight) / 2;
|
|
105
|
+
|
|
106
|
+
// In the rare event that the content child is changed, we need to remove the scale
|
|
107
|
+
// because we don't want to have a scale on the old content child that is somewhere else in the DOM
|
|
108
|
+
if (this.scaleLastSetOn !== this.contentChild) {
|
|
109
|
+
this.removeScale();
|
|
110
|
+
}
|
|
111
|
+
// Use toFixed to avoid floating point precision issues
|
|
112
|
+
// this will update every frame with sub-pixel changes if we don't pin it down
|
|
113
|
+
Object.assign(this.contentChild.style, {
|
|
114
|
+
transform: `translate(${translateX.toFixed(4)}px, ${translateY.toFixed(4)}px) scale(${scale.toFixed(4)})`,
|
|
115
|
+
transformOrigin: "top left",
|
|
116
|
+
});
|
|
117
|
+
this.scale = scale;
|
|
118
|
+
this.scaleLastSetOn = this.contentChild;
|
|
119
|
+
}
|
|
120
|
+
this.animationFrameId = requestAnimationFrame(this.setScale);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
removeScale = () => {
|
|
125
|
+
if (this.scaleLastSetOn) {
|
|
126
|
+
Object.assign(this.scaleLastSetOn.style, {
|
|
127
|
+
transform: "",
|
|
128
|
+
transformOrigin: "",
|
|
129
|
+
});
|
|
130
|
+
this.scaleLastSetOn = null;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
connectedCallback(): void {
|
|
135
|
+
super.connectedCallback();
|
|
136
|
+
this.animationFrameId = requestAnimationFrame(this.setScale);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
disconnectedCallback(): void {
|
|
140
|
+
super.disconnectedCallback();
|
|
141
|
+
this.removeScale();
|
|
142
|
+
if (this.animationFrameId) {
|
|
143
|
+
cancelAnimationFrame(this.animationFrameId);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
declare global {
|
|
149
|
+
interface HTMLElementTagNameMap {
|
|
150
|
+
"ef-fit-scale": EFFitScale;
|
|
151
|
+
}
|
|
152
|
+
}
|
package/src/gui/EFWorkbench.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { LitElement, type PropertyValueMap, css, html } from "lit";
|
|
2
|
-
import { customElement, eventOptions,
|
|
2
|
+
import { customElement, eventOptions, property } from "lit/decorators.js";
|
|
3
3
|
import { createRef, ref } from "lit/directives/ref.js";
|
|
4
4
|
|
|
5
5
|
import { ContextMixin } from "./ContextMixin.js";
|
|
@@ -17,69 +17,22 @@ export class EFWorkbench extends ContextMixin(TWMixin(LitElement)) {
|
|
|
17
17
|
`,
|
|
18
18
|
];
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
@property({ type: Boolean })
|
|
21
|
+
rendering = false;
|
|
22
|
+
|
|
23
|
+
focusOverlay = createRef<HTMLDivElement>();
|
|
22
24
|
|
|
23
25
|
@eventOptions({ passive: false, capture: true })
|
|
24
26
|
handleStageWheel(event: WheelEvent) {
|
|
25
27
|
event.preventDefault();
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
focusOverlay = createRef<HTMLDivElement>();
|
|
29
|
-
|
|
30
|
-
@state()
|
|
31
|
-
private stageScale = 1;
|
|
32
|
-
|
|
33
|
-
setStageScale = () => {
|
|
34
|
-
if (this.isConnected && !this.rendering) {
|
|
35
|
-
const canvasElement = this.canvasRef.value;
|
|
36
|
-
const stageElement = this.stageRef.value;
|
|
37
|
-
const canvasChild = canvasElement?.assignedElements()[0];
|
|
38
|
-
if (stageElement && canvasElement && canvasChild) {
|
|
39
|
-
// Determine the appropriate scale factor to make the canvas fit into
|
|
40
|
-
canvasElement.style.width = `${canvasChild.clientWidth}px`;
|
|
41
|
-
canvasElement.style.height = `${canvasChild.clientHeight}px`;
|
|
42
|
-
const stageWidth = stageElement.clientWidth;
|
|
43
|
-
const stageHeight = stageElement.clientHeight;
|
|
44
|
-
const canvasWidth = canvasElement.clientWidth;
|
|
45
|
-
const canvasHeight = canvasElement.clientHeight;
|
|
46
|
-
const stageRatio = stageWidth / stageHeight;
|
|
47
|
-
const canvasRatio = canvasWidth / canvasHeight;
|
|
48
|
-
|
|
49
|
-
if (stageRatio > canvasRatio) {
|
|
50
|
-
const scale = stageHeight / canvasHeight;
|
|
51
|
-
if (this.stageScale !== scale) {
|
|
52
|
-
canvasElement.style.transform = `scale(${scale})`;
|
|
53
|
-
canvasElement.style.transformOrigin = "top center";
|
|
54
|
-
}
|
|
55
|
-
this.stageScale = scale;
|
|
56
|
-
} else {
|
|
57
|
-
const scale = stageWidth / canvasWidth;
|
|
58
|
-
if (this.stageScale !== scale) {
|
|
59
|
-
canvasElement.style.transform = `scale(${scale})`;
|
|
60
|
-
canvasElement.style.transformOrigin = "center";
|
|
61
|
-
}
|
|
62
|
-
this.stageScale = scale;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
if (this.isConnected) {
|
|
67
|
-
requestAnimationFrame(this.setStageScale);
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
|
|
71
30
|
connectedCallback(): void {
|
|
72
|
-
// Set the body and html to 100% width and height to avoid scaling issues
|
|
73
|
-
// this is a hack to avoid scaling issues when the stage is scaled and during output rendering
|
|
74
|
-
// What I've discovered recently is that having the workbench be so smart as to determine
|
|
75
|
-
// how it should be displayed at render time causes problems. Much of that has been extracted
|
|
76
|
-
// but this is a quick fix to avoid scaling issues.
|
|
77
31
|
document.body.style.width = "100%";
|
|
78
32
|
document.body.style.height = "100%";
|
|
79
33
|
document.documentElement.style.width = "100%";
|
|
80
34
|
document.documentElement.style.height = "100%";
|
|
81
35
|
super.connectedCallback();
|
|
82
|
-
requestAnimationFrame(this.setStageScale);
|
|
83
36
|
}
|
|
84
37
|
|
|
85
38
|
disconnectedCallback(): void {
|
|
@@ -121,30 +74,30 @@ export class EFWorkbench extends ContextMixin(TWMixin(LitElement)) {
|
|
|
121
74
|
};
|
|
122
75
|
|
|
123
76
|
render() {
|
|
124
|
-
|
|
77
|
+
// TODO: this.rendering is not correctly set when using the framegen bridge
|
|
78
|
+
// so to hack we're checking for the existence of EF_RENDERING on the window
|
|
79
|
+
if (
|
|
80
|
+
this.rendering ||
|
|
81
|
+
(typeof window !== "undefined" && window.EF_RENDERING?.() === true)
|
|
82
|
+
) {
|
|
83
|
+
console.log("WORKBENCH RENDERING“");
|
|
125
84
|
return html`
|
|
126
|
-
<slot
|
|
127
|
-
${ref(this.canvasRef)}
|
|
128
|
-
class="fixed inset-0 h-full w-full"
|
|
129
|
-
name="canvas"
|
|
130
|
-
></slot>
|
|
85
|
+
<slot class="fixed inset-0 h-full w-full" name="canvas"></slot>
|
|
131
86
|
`;
|
|
132
87
|
}
|
|
88
|
+
console.log("WORKBENCH NOT RENDERING");
|
|
133
89
|
return html`
|
|
134
90
|
<div
|
|
135
91
|
class="grid h-full w-full bg-slate-800"
|
|
136
92
|
style="grid-template-rows: 1fr 300px; grid-template-columns: 100%;"
|
|
137
93
|
>
|
|
138
94
|
<div
|
|
139
|
-
|
|
140
|
-
class="relative grid h-full w-full justify-center overflow-hidden"
|
|
95
|
+
class="relative h-full w-full overflow-hidden"
|
|
141
96
|
@wheel=${this.handleStageWheel}
|
|
142
97
|
>
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
class="inline-block"
|
|
147
|
-
></slot>
|
|
98
|
+
<ef-fit-scale class="h-full grid place-content-center">
|
|
99
|
+
<slot name="canvas" class="contents"></slot>
|
|
100
|
+
</ef-fit-scale>
|
|
148
101
|
<div
|
|
149
102
|
class="border border-blue-500 bg-blue-200 bg-opacity-20 absolute"
|
|
150
103
|
${ref(this.focusOverlay)}
|
package/src/gui/TWMixin.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LitElement } from "lit";
|
|
1
|
+
import type { CSSResult, LitElement } from "lit";
|
|
2
2
|
// @ts-expect-error cannot figure out how to declare this module as a string
|
|
3
3
|
import twStyle from "./TWMixin.css?inline";
|
|
4
4
|
|
|
@@ -21,13 +21,30 @@ export function TWMixin<T extends new (...args: any[]) => LitElement>(Base: T) {
|
|
|
21
21
|
"twSheet not found. Probable cause: CSSStyleSheet not supported in this environment",
|
|
22
22
|
);
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
const constructorStylesheets: CSSStyleSheet[] = [];
|
|
26
|
+
const constructorStyles = (("styles" in this.constructor &&
|
|
27
|
+
this.constructor.styles) ||
|
|
28
|
+
[]) as CSSResult | CSSResult[];
|
|
29
|
+
|
|
30
|
+
if (Array.isArray(constructorStyles)) {
|
|
31
|
+
for (const item of constructorStyles) {
|
|
32
|
+
if (item.styleSheet) {
|
|
33
|
+
constructorStylesheets.push(item.styleSheet);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} else if (constructorStyles.styleSheet) {
|
|
37
|
+
constructorStylesheets.push(constructorStyles.styleSheet);
|
|
38
|
+
}
|
|
39
|
+
|
|
24
40
|
if (renderRoot?.adoptedStyleSheets) {
|
|
25
41
|
renderRoot.adoptedStyleSheets = [
|
|
26
42
|
twSheet,
|
|
27
43
|
...renderRoot.adoptedStyleSheets,
|
|
44
|
+
...constructorStylesheets,
|
|
28
45
|
];
|
|
29
46
|
} else {
|
|
30
|
-
renderRoot.adoptedStyleSheets = [twSheet];
|
|
47
|
+
renderRoot.adoptedStyleSheets = [twSheet, ...constructorStylesheets];
|
|
31
48
|
}
|
|
32
49
|
return renderRoot;
|
|
33
50
|
}
|