@gannochenko/staticstripes 0.0.22 ā 0.0.24
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/Makefile +8 -0
- package/dist/app-builder.d.ts +18 -0
- package/dist/app-builder.d.ts.map +1 -0
- package/dist/app-builder.js +94 -0
- package/dist/app-builder.js.map +1 -0
- package/dist/cli/commands/filters.d.ts +3 -0
- package/dist/cli/commands/filters.d.ts.map +1 -0
- package/dist/cli/commands/filters.js +21 -0
- package/dist/cli/commands/filters.js.map +1 -0
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +6 -1
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/instagram/instagram-upload-strategy.d.ts +5 -0
- package/dist/cli/instagram/instagram-upload-strategy.d.ts.map +1 -1
- package/dist/cli/instagram/instagram-upload-strategy.js +46 -3
- package/dist/cli/instagram/instagram-upload-strategy.js.map +1 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/ffmpeg.d.ts +32 -0
- package/dist/ffmpeg.d.ts.map +1 -1
- package/dist/ffmpeg.js +118 -0
- package/dist/ffmpeg.js.map +1 -1
- package/dist/html-project-parser.d.ts +36 -1
- package/dist/html-project-parser.d.ts.map +1 -1
- package/dist/html-project-parser.js +332 -15
- package/dist/html-project-parser.js.map +1 -1
- package/dist/project.d.ts +4 -1
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +50 -1
- package/dist/project.js.map +1 -1
- package/dist/sample-sequences.d.ts.map +1 -1
- package/dist/sample-sequences.js +293 -0
- package/dist/sample-sequences.js.map +1 -1
- package/dist/sequence.d.ts +4 -1
- package/dist/sequence.d.ts.map +1 -1
- package/dist/sequence.js +71 -21
- package/dist/sequence.js.map +1 -1
- package/dist/stream.d.ts +17 -0
- package/dist/stream.d.ts.map +1 -1
- package/dist/stream.js +28 -0
- package/dist/stream.js.map +1 -1
- package/dist/type.d.ts +29 -2
- package/dist/type.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/app-builder.ts +113 -0
- package/src/cli/commands/filters.ts +21 -0
- package/src/cli/commands/generate.ts +10 -1
- package/src/cli/instagram/instagram-upload-strategy.ts +61 -1
- package/src/cli.ts +2 -0
- package/src/ffmpeg.ts +161 -0
- package/src/html-project-parser.ts +410 -28
- package/src/project.ts +62 -0
- package/src/sample-sequences.ts +300 -0
- package/src/sequence.ts +78 -22
- package/src/stream.ts +50 -0
- package/src/type.ts +31 -2
|
@@ -1033,6 +1033,7 @@ export class HTMLProjectParser {
|
|
|
1033
1033
|
let thumbOffset: number | undefined;
|
|
1034
1034
|
let coverUrl: string | undefined;
|
|
1035
1035
|
let videoUrl: string | undefined;
|
|
1036
|
+
let locationId: string | undefined;
|
|
1036
1037
|
const localTags: string[] = [];
|
|
1037
1038
|
|
|
1038
1039
|
if ('children' in element && element.children) {
|
|
@@ -1106,6 +1107,21 @@ export class HTMLProjectParser {
|
|
|
1106
1107
|
}
|
|
1107
1108
|
break;
|
|
1108
1109
|
}
|
|
1110
|
+
case 'location': {
|
|
1111
|
+
const id = childAttrs.get('id');
|
|
1112
|
+
const city = childAttrs.get('city');
|
|
1113
|
+
const country = childAttrs.get('country');
|
|
1114
|
+
|
|
1115
|
+
if (id) {
|
|
1116
|
+
// Explicit location ID provided
|
|
1117
|
+
locationId = id;
|
|
1118
|
+
} else if (city && country) {
|
|
1119
|
+
// Store search query for later resolution
|
|
1120
|
+
// Format: "search:City, Country"
|
|
1121
|
+
locationId = `search:${city}, ${country}`;
|
|
1122
|
+
}
|
|
1123
|
+
break;
|
|
1124
|
+
}
|
|
1109
1125
|
}
|
|
1110
1126
|
}
|
|
1111
1127
|
}
|
|
@@ -1131,6 +1147,7 @@ export class HTMLProjectParser {
|
|
|
1131
1147
|
thumbOffset,
|
|
1132
1148
|
coverUrl,
|
|
1133
1149
|
videoUrl,
|
|
1150
|
+
locationId,
|
|
1134
1151
|
},
|
|
1135
1152
|
};
|
|
1136
1153
|
}
|
|
@@ -1615,26 +1632,44 @@ export class HTMLProjectParser {
|
|
|
1615
1632
|
const container = this.extractFragmentContainer(element);
|
|
1616
1633
|
const app = container ? undefined : this.extractFragmentApp(element);
|
|
1617
1634
|
|
|
1618
|
-
//
|
|
1619
|
-
const
|
|
1620
|
-
|
|
1621
|
-
//
|
|
1622
|
-
const
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
//
|
|
1634
|
-
const
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1635
|
+
// 4b. Parse data-timing attribute (short syntax) - takes precedence over CSS
|
|
1636
|
+
const dataTiming = this.parseDataTiming(attrs.get('data-timing'));
|
|
1637
|
+
|
|
1638
|
+
// 5. Parse trimLeft from data-timing or -trim-start property
|
|
1639
|
+
const trimLeft =
|
|
1640
|
+
dataTiming.trimStart !== undefined
|
|
1641
|
+
? dataTiming.trimStart
|
|
1642
|
+
: this.parseTrimStart(styles['-trim-start']);
|
|
1643
|
+
|
|
1644
|
+
// 5b. Parse trimRight from data-timing or -trim-end property
|
|
1645
|
+
const trimRight =
|
|
1646
|
+
dataTiming.trimEnd !== undefined
|
|
1647
|
+
? dataTiming.trimEnd
|
|
1648
|
+
: this.parseTrimEnd(styles['-trim-end']);
|
|
1649
|
+
|
|
1650
|
+
// 6. Parse duration from data-timing or -duration property
|
|
1651
|
+
const duration =
|
|
1652
|
+
dataTiming.duration !== undefined
|
|
1653
|
+
? dataTiming.duration
|
|
1654
|
+
: this.parseDurationProperty(
|
|
1655
|
+
styles['-duration'],
|
|
1656
|
+
assetName,
|
|
1657
|
+
assets,
|
|
1658
|
+
trimLeft,
|
|
1659
|
+
trimRight,
|
|
1660
|
+
);
|
|
1661
|
+
|
|
1662
|
+
// 7. Parse overlayLeft from data-timing or -offset-start property
|
|
1663
|
+
const overlayLeft =
|
|
1664
|
+
dataTiming.offsetStart !== undefined
|
|
1665
|
+
? dataTiming.offsetStart
|
|
1666
|
+
: this.parseOffsetStart(styles['-offset-start']);
|
|
1667
|
+
|
|
1668
|
+
// 8. Parse overlayRight from data-timing or -offset-end property
|
|
1669
|
+
const overlayRight =
|
|
1670
|
+
dataTiming.offsetEnd !== undefined
|
|
1671
|
+
? dataTiming.offsetEnd
|
|
1672
|
+
: this.parseOffsetEnd(styles['-offset-end']);
|
|
1638
1673
|
|
|
1639
1674
|
// 9. Parse -overlay-start-z-index for overlayZIndex
|
|
1640
1675
|
const overlayZIndex = this.parseZIndex(styles['-overlay-start-z-index']);
|
|
@@ -1658,10 +1693,16 @@ export class HTMLProjectParser {
|
|
|
1658
1693
|
// 14. Parse -chromakey
|
|
1659
1694
|
const chromakeyData = this.parseChromakeyProperty(styles['-chromakey']);
|
|
1660
1695
|
|
|
1696
|
+
// 14b. Parse -object-fit-ken-burns
|
|
1697
|
+
const kenBurnsData = this.parseKenBurnsProperty(styles['-object-fit-ken-burns']);
|
|
1698
|
+
|
|
1661
1699
|
// 15. Parse filter (for visual filters)
|
|
1662
1700
|
const visualFilter = this.parseVisualFilterProperty(styles['filter']);
|
|
1663
1701
|
|
|
1664
|
-
// 16.
|
|
1702
|
+
// 16. Parse sound property (on/off)
|
|
1703
|
+
const sound = this.parseSoundProperty(styles['-sound']);
|
|
1704
|
+
|
|
1705
|
+
// 17. Extract timecode label from data-timecode attribute
|
|
1665
1706
|
const timecodeLabel = attrs.get('data-timecode') || undefined;
|
|
1666
1707
|
|
|
1667
1708
|
return {
|
|
@@ -1692,6 +1733,17 @@ export class HTMLProjectParser {
|
|
|
1692
1733
|
chromakeyBlend: chromakeyData.chromakeyBlend,
|
|
1693
1734
|
chromakeySimilarity: chromakeyData.chromakeySimilarity,
|
|
1694
1735
|
chromakeyColor: chromakeyData.chromakeyColor,
|
|
1736
|
+
objectFitKenBurns: kenBurnsData.objectFitKenBurns,
|
|
1737
|
+
objectFitKenBurnsZoom: kenBurnsData.objectFitKenBurnsZoom,
|
|
1738
|
+
objectFitKenBurnsEffectDuration: kenBurnsData.objectFitKenBurnsEffectDuration,
|
|
1739
|
+
objectFitKenBurnsEasing: kenBurnsData.objectFitKenBurnsEasing,
|
|
1740
|
+
objectFitKenBurnsFocalX: kenBurnsData.objectFitKenBurnsFocalX,
|
|
1741
|
+
objectFitKenBurnsFocalY: kenBurnsData.objectFitKenBurnsFocalY,
|
|
1742
|
+
objectFitKenBurnsPanStartX: kenBurnsData.objectFitKenBurnsPanStartX,
|
|
1743
|
+
objectFitKenBurnsPanStartY: kenBurnsData.objectFitKenBurnsPanStartY,
|
|
1744
|
+
objectFitKenBurnsPanEndX: kenBurnsData.objectFitKenBurnsPanEndX,
|
|
1745
|
+
objectFitKenBurnsPanEndY: kenBurnsData.objectFitKenBurnsPanEndY,
|
|
1746
|
+
sound,
|
|
1695
1747
|
...(visualFilter && { visualFilter }), // Add visualFilter if present
|
|
1696
1748
|
...(container && { container }), // Add container if present
|
|
1697
1749
|
...(app && { app }), // Add app if present
|
|
@@ -1718,6 +1770,25 @@ export class HTMLProjectParser {
|
|
|
1718
1770
|
return trimmed || undefined;
|
|
1719
1771
|
}
|
|
1720
1772
|
|
|
1773
|
+
/**
|
|
1774
|
+
* Parses -sound property
|
|
1775
|
+
* Can be: "on" (default) or "off"
|
|
1776
|
+
* When "off", replaces audio stream with silence
|
|
1777
|
+
*/
|
|
1778
|
+
private parseSoundProperty(sound: string | undefined): 'on' | 'off' {
|
|
1779
|
+
if (!sound) {
|
|
1780
|
+
return 'on'; // Default: use audio
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
const trimmed = sound.trim().toLowerCase();
|
|
1784
|
+
|
|
1785
|
+
if (trimmed === 'off') {
|
|
1786
|
+
return 'off';
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
return 'on'; // Default for any other value
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1721
1792
|
/**
|
|
1722
1793
|
* Extracts the first <container> child from a fragment element
|
|
1723
1794
|
*/
|
|
@@ -1919,7 +1990,7 @@ export class HTMLProjectParser {
|
|
|
1919
1990
|
assets: Map<string, Asset>,
|
|
1920
1991
|
trimLeft: number,
|
|
1921
1992
|
trimRight: number,
|
|
1922
|
-
): number {
|
|
1993
|
+
): number | CompiledExpression {
|
|
1923
1994
|
if (!duration || duration.trim() === 'auto') {
|
|
1924
1995
|
// Auto: use asset duration minus trim-start and trim-end
|
|
1925
1996
|
const asset = assets.get(assetName);
|
|
@@ -1929,9 +2000,16 @@ export class HTMLProjectParser {
|
|
|
1929
2000
|
return Math.max(0, asset.duration - trimLeft - trimRight);
|
|
1930
2001
|
}
|
|
1931
2002
|
|
|
2003
|
+
const trimmed = duration.trim();
|
|
2004
|
+
|
|
2005
|
+
// Check if it's a calc() expression
|
|
2006
|
+
if (trimmed.startsWith('calc(')) {
|
|
2007
|
+
return parseValueLazy(trimmed) as CompiledExpression;
|
|
2008
|
+
}
|
|
2009
|
+
|
|
1932
2010
|
// Handle percentage (e.g., "100%", "50%")
|
|
1933
|
-
if (
|
|
1934
|
-
const percentage = parseFloat(
|
|
2011
|
+
if (trimmed.endsWith('%')) {
|
|
2012
|
+
const percentage = parseFloat(trimmed);
|
|
1935
2013
|
if (isNaN(percentage)) {
|
|
1936
2014
|
return 0;
|
|
1937
2015
|
}
|
|
@@ -1946,12 +2024,12 @@ export class HTMLProjectParser {
|
|
|
1946
2024
|
}
|
|
1947
2025
|
|
|
1948
2026
|
// Handle time value (e.g., "5000ms", "5s")
|
|
1949
|
-
return this.parseMilliseconds(
|
|
2027
|
+
return this.parseMilliseconds(trimmed);
|
|
1950
2028
|
}
|
|
1951
2029
|
|
|
1952
2030
|
/**
|
|
1953
2031
|
* Parses time value into milliseconds
|
|
1954
|
-
* Supports: "5s", "5000ms", "1.5s",
|
|
2032
|
+
* Supports: "5s", "5000ms", "1.5s", or raw numbers (defaults to milliseconds)
|
|
1955
2033
|
*/
|
|
1956
2034
|
private parseMilliseconds(value: string | undefined): number {
|
|
1957
2035
|
if (!value) {
|
|
@@ -1976,6 +2054,12 @@ export class HTMLProjectParser {
|
|
|
1976
2054
|
}
|
|
1977
2055
|
}
|
|
1978
2056
|
|
|
2057
|
+
// Handle raw numbers (default to milliseconds)
|
|
2058
|
+
const num = parseFloat(trimmed);
|
|
2059
|
+
if (!isNaN(num)) {
|
|
2060
|
+
return Math.round(num);
|
|
2061
|
+
}
|
|
2062
|
+
|
|
1979
2063
|
return 0;
|
|
1980
2064
|
}
|
|
1981
2065
|
|
|
@@ -2023,6 +2107,81 @@ export class HTMLProjectParser {
|
|
|
2023
2107
|
return this.parseMilliseconds(trimmed);
|
|
2024
2108
|
}
|
|
2025
2109
|
|
|
2110
|
+
/**
|
|
2111
|
+
* Parses data-timing attribute with short syntax
|
|
2112
|
+
* Format: "ts=3000,te=5000,d=2000,os=1000,oe=7000"
|
|
2113
|
+
* Where:
|
|
2114
|
+
* ts = -trim-start
|
|
2115
|
+
* te = -trim-end
|
|
2116
|
+
* d = -duration
|
|
2117
|
+
* os = -offset-start
|
|
2118
|
+
* oe = -offset-end
|
|
2119
|
+
* Values default to milliseconds if no unit specified
|
|
2120
|
+
* Supports calc() expressions: ts=calc(url(#id.time.start))
|
|
2121
|
+
*/
|
|
2122
|
+
private parseDataTiming(dataTiming: string | undefined): {
|
|
2123
|
+
trimStart?: number;
|
|
2124
|
+
trimEnd?: number;
|
|
2125
|
+
duration?: number | CompiledExpression;
|
|
2126
|
+
offsetStart?: number | CompiledExpression;
|
|
2127
|
+
offsetEnd?: number | CompiledExpression;
|
|
2128
|
+
} {
|
|
2129
|
+
const result: {
|
|
2130
|
+
trimStart?: number;
|
|
2131
|
+
trimEnd?: number;
|
|
2132
|
+
duration?: number | CompiledExpression;
|
|
2133
|
+
offsetStart?: number | CompiledExpression;
|
|
2134
|
+
offsetEnd?: number | CompiledExpression;
|
|
2135
|
+
} = {};
|
|
2136
|
+
|
|
2137
|
+
if (!dataTiming) {
|
|
2138
|
+
return result;
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
// Split by comma and parse each key-value pair
|
|
2142
|
+
const pairs = dataTiming.split(',').map((pair) => pair.trim());
|
|
2143
|
+
|
|
2144
|
+
for (const pair of pairs) {
|
|
2145
|
+
const [key, value] = pair.split('=').map((s) => s.trim());
|
|
2146
|
+
if (!key || !value) {
|
|
2147
|
+
continue;
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
// Parse value - check if it's a calc() expression or a simple value
|
|
2151
|
+
let parsedValue: number | CompiledExpression;
|
|
2152
|
+
|
|
2153
|
+
if (value.startsWith('calc(')) {
|
|
2154
|
+
// It's a calc() expression
|
|
2155
|
+
parsedValue = parseValueLazy(value) as CompiledExpression;
|
|
2156
|
+
} else {
|
|
2157
|
+
// It's a simple time value - parse with parseMilliseconds
|
|
2158
|
+
// which handles units like 's' and 'ms', defaulting to ms
|
|
2159
|
+
parsedValue = this.parseMilliseconds(value);
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
// Map short names to result properties
|
|
2163
|
+
switch (key) {
|
|
2164
|
+
case 'ts':
|
|
2165
|
+
result.trimStart = parsedValue as number;
|
|
2166
|
+
break;
|
|
2167
|
+
case 'te':
|
|
2168
|
+
result.trimEnd = parsedValue as number;
|
|
2169
|
+
break;
|
|
2170
|
+
case 'd':
|
|
2171
|
+
result.duration = parsedValue;
|
|
2172
|
+
break;
|
|
2173
|
+
case 'os':
|
|
2174
|
+
result.offsetStart = parsedValue;
|
|
2175
|
+
break;
|
|
2176
|
+
case 'oe':
|
|
2177
|
+
result.offsetEnd = parsedValue;
|
|
2178
|
+
break;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
return result;
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2026
2185
|
/**
|
|
2027
2186
|
* Parses z-index values (-overlay-start-z-index, -overlay-end-z-index)
|
|
2028
2187
|
*/
|
|
@@ -2073,7 +2232,7 @@ export class HTMLProjectParser {
|
|
|
2073
2232
|
* - "cover"
|
|
2074
2233
|
*/
|
|
2075
2234
|
private parseObjectFitProperty(objectFit: string | undefined): {
|
|
2076
|
-
objectFit: 'cover' | 'contain';
|
|
2235
|
+
objectFit: 'cover' | 'contain' | 'ken-burns';
|
|
2077
2236
|
objectFitContain: 'ambient' | 'pillarbox';
|
|
2078
2237
|
objectFitContainAmbientBlurStrength: number;
|
|
2079
2238
|
objectFitContainAmbientBrightness: number;
|
|
@@ -2082,7 +2241,7 @@ export class HTMLProjectParser {
|
|
|
2082
2241
|
} {
|
|
2083
2242
|
// Defaults
|
|
2084
2243
|
const defaults = {
|
|
2085
|
-
objectFit: 'cover' as 'cover' | 'contain',
|
|
2244
|
+
objectFit: 'cover' as 'cover' | 'contain' | 'ken-burns',
|
|
2086
2245
|
objectFitContain: 'ambient' as 'ambient' | 'pillarbox',
|
|
2087
2246
|
objectFitContainAmbientBlurStrength: 20,
|
|
2088
2247
|
objectFitContainAmbientBrightness: -0.3,
|
|
@@ -2108,6 +2267,11 @@ export class HTMLProjectParser {
|
|
|
2108
2267
|
return { ...defaults, objectFit: 'cover' };
|
|
2109
2268
|
}
|
|
2110
2269
|
|
|
2270
|
+
// Handle "ken-burns"
|
|
2271
|
+
if (type === 'ken-burns') {
|
|
2272
|
+
return { ...defaults, objectFit: 'ken-burns' };
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2111
2275
|
// Handle "contain" with sub-options
|
|
2112
2276
|
if (type === 'contain') {
|
|
2113
2277
|
const subType = parts[1];
|
|
@@ -2223,4 +2387,222 @@ export class HTMLProjectParser {
|
|
|
2223
2387
|
chromakeyColor: color,
|
|
2224
2388
|
};
|
|
2225
2389
|
}
|
|
2390
|
+
|
|
2391
|
+
/**
|
|
2392
|
+
* Parses -object-fit-ken-burns property
|
|
2393
|
+
* Format:
|
|
2394
|
+
* Zoom effects: "<effect> <focal-x> <focal-y> <zoom%> <duration> [easing]"
|
|
2395
|
+
* Pan effects: "<effect> <zoom-factor> [easing]"
|
|
2396
|
+
* Examples:
|
|
2397
|
+
* - "zoom-in 50% 50% 30% 1000ms ease-in-out" - zoom 30% over 1s
|
|
2398
|
+
* - "zoom-out 30% 30% 50% 2000ms ease-out" - zoom out 50% over 2s
|
|
2399
|
+
* - "pan-left 1.3 linear" - pan with 1.3x zoom
|
|
2400
|
+
* Effects: zoom-in, zoom-out (require focal points + zoom% + duration)
|
|
2401
|
+
* pan-left, pan-right, pan-top, pan-bottom (require zoom factor only)
|
|
2402
|
+
* Zoom: for zoom effects: percentage (30 = 30%), for pan: factor (1.3)
|
|
2403
|
+
* Duration: effect duration in milliseconds (zoom effects only)
|
|
2404
|
+
* Easing: linear, ease-in, ease-out, ease-in-out (default: linear)
|
|
2405
|
+
*/
|
|
2406
|
+
private parseKenBurnsProperty(kenBurns: string | undefined): {
|
|
2407
|
+
objectFitKenBurns: 'zoom-in' | 'zoom-out' | 'pan-left' | 'pan-right' | 'pan-top' | 'pan-bottom';
|
|
2408
|
+
objectFitKenBurnsZoom: number;
|
|
2409
|
+
objectFitKenBurnsEffectDuration: number;
|
|
2410
|
+
objectFitKenBurnsEasing: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out';
|
|
2411
|
+
objectFitKenBurnsFocalX: number;
|
|
2412
|
+
objectFitKenBurnsFocalY: number;
|
|
2413
|
+
objectFitKenBurnsPanStartX: number;
|
|
2414
|
+
objectFitKenBurnsPanStartY: number;
|
|
2415
|
+
objectFitKenBurnsPanEndX: number;
|
|
2416
|
+
objectFitKenBurnsPanEndY: number;
|
|
2417
|
+
} {
|
|
2418
|
+
// Defaults
|
|
2419
|
+
const defaults = {
|
|
2420
|
+
objectFitKenBurns: 'zoom-in' as 'zoom-in' | 'zoom-out' | 'pan-left' | 'pan-right' | 'pan-top' | 'pan-bottom',
|
|
2421
|
+
objectFitKenBurnsZoom: 30, // 30% zoom
|
|
2422
|
+
objectFitKenBurnsEffectDuration: 0, // 0 = use fragment duration
|
|
2423
|
+
objectFitKenBurnsEasing: 'linear' as 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out',
|
|
2424
|
+
objectFitKenBurnsFocalX: 50, // center (for zoom effects)
|
|
2425
|
+
objectFitKenBurnsFocalY: 50, // center (for zoom effects)
|
|
2426
|
+
objectFitKenBurnsPanStartX: 0, // left edge (for horizontal pan)
|
|
2427
|
+
objectFitKenBurnsPanStartY: 0, // top edge (for vertical pan)
|
|
2428
|
+
objectFitKenBurnsPanEndX: 100, // right edge (for horizontal pan)
|
|
2429
|
+
objectFitKenBurnsPanEndY: 100, // bottom edge (for vertical pan)
|
|
2430
|
+
};
|
|
2431
|
+
|
|
2432
|
+
if (!kenBurns) {
|
|
2433
|
+
return defaults;
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
const trimmed = kenBurns.trim();
|
|
2437
|
+
const parts = this.splitCssValue(trimmed);
|
|
2438
|
+
|
|
2439
|
+
if (parts.length === 0) {
|
|
2440
|
+
return defaults;
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
// Parse effect (first part)
|
|
2444
|
+
const effect = parts[0] as 'zoom-in' | 'zoom-out' | 'pan-left' | 'pan-right' | 'pan-top' | 'pan-bottom';
|
|
2445
|
+
const validEffects = ['zoom-in', 'zoom-out', 'pan-left', 'pan-right', 'pan-top', 'pan-bottom'];
|
|
2446
|
+
if (!validEffects.includes(effect)) {
|
|
2447
|
+
return defaults;
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
let focalX = defaults.objectFitKenBurnsFocalX;
|
|
2451
|
+
let focalY = defaults.objectFitKenBurnsFocalY;
|
|
2452
|
+
let zoom = defaults.objectFitKenBurnsZoom;
|
|
2453
|
+
let effectDuration = defaults.objectFitKenBurnsEffectDuration;
|
|
2454
|
+
let easing = defaults.objectFitKenBurnsEasing;
|
|
2455
|
+
let panStartX = defaults.objectFitKenBurnsPanStartX;
|
|
2456
|
+
let panStartY = defaults.objectFitKenBurnsPanStartY;
|
|
2457
|
+
let panEndX = defaults.objectFitKenBurnsPanEndX;
|
|
2458
|
+
let panEndY = defaults.objectFitKenBurnsPanEndY;
|
|
2459
|
+
|
|
2460
|
+
const isZoomEffect = effect === 'zoom-in' || effect === 'zoom-out';
|
|
2461
|
+
const isPanEffect = effect === 'pan-left' || effect === 'pan-right' || effect === 'pan-top' || effect === 'pan-bottom';
|
|
2462
|
+
|
|
2463
|
+
// Parse remaining parts based on effect type
|
|
2464
|
+
let nextIndex = 1;
|
|
2465
|
+
|
|
2466
|
+
if (isZoomEffect) {
|
|
2467
|
+
// Zoom effects: "<effect> <focal-x%> <focal-y%> <zoom%> <duration> [easing]"
|
|
2468
|
+
|
|
2469
|
+
// Parse focal points
|
|
2470
|
+
if (parts.length >= 3 && parts[1].includes('%') && parts[2].includes('%')) {
|
|
2471
|
+
const focalXStr = parts[1].split('%')[0];
|
|
2472
|
+
const focalYStr = parts[2].split('%')[0];
|
|
2473
|
+
const parsedX = parseFloat(focalXStr);
|
|
2474
|
+
const parsedY = parseFloat(focalYStr);
|
|
2475
|
+
|
|
2476
|
+
if (!isNaN(parsedX) && !isNaN(parsedY)) {
|
|
2477
|
+
focalX = Math.max(0, Math.min(100, parsedX));
|
|
2478
|
+
focalY = Math.max(0, Math.min(100, parsedY));
|
|
2479
|
+
nextIndex = 3;
|
|
2480
|
+
|
|
2481
|
+
// Check if parts[2] had a concatenated value after '%'
|
|
2482
|
+
const remainder = parts[2].split('%')[1];
|
|
2483
|
+
if (remainder && remainder.trim()) {
|
|
2484
|
+
parts.splice(3, 0, remainder.trim());
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
// Parse zoom percentage (required)
|
|
2490
|
+
if (parts.length > nextIndex && parts[nextIndex].includes('%')) {
|
|
2491
|
+
const zoomStr = parts[nextIndex].split('%')[0];
|
|
2492
|
+
const parsedZoom = parseFloat(zoomStr);
|
|
2493
|
+
if (!isNaN(parsedZoom) && parsedZoom >= 0) {
|
|
2494
|
+
zoom = parsedZoom; // Store as percentage (e.g., 30 for 30%)
|
|
2495
|
+
nextIndex++;
|
|
2496
|
+
|
|
2497
|
+
// Check for concatenated value after '%'
|
|
2498
|
+
const remainder = parts[nextIndex - 1].split('%')[1];
|
|
2499
|
+
if (remainder && remainder.trim()) {
|
|
2500
|
+
parts.splice(nextIndex, 0, remainder.trim());
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
// Parse effect duration (required)
|
|
2506
|
+
if (parts.length > nextIndex) {
|
|
2507
|
+
const durationStr = parts[nextIndex];
|
|
2508
|
+
// Simple inline parser for "1000ms" or "1s" format
|
|
2509
|
+
const match = durationStr.match(/^(\d+(?:\.\d+)?)(ms|s)$/);
|
|
2510
|
+
if (match) {
|
|
2511
|
+
const value = parseFloat(match[1]);
|
|
2512
|
+
const parsedDuration = match[2] === 's' ? value * 1000 : value;
|
|
2513
|
+
if (parsedDuration > 0) {
|
|
2514
|
+
effectDuration = parsedDuration;
|
|
2515
|
+
nextIndex++;
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
// Parse easing (optional)
|
|
2521
|
+
if (parts.length > nextIndex) {
|
|
2522
|
+
const easingStr = parts[nextIndex] as 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out';
|
|
2523
|
+
if (['linear', 'ease-in', 'ease-out', 'ease-in-out'].includes(easingStr)) {
|
|
2524
|
+
easing = easingStr;
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
} else if (isPanEffect) {
|
|
2528
|
+
// Pan effects: "<effect> <zoom%> <start%> <end%> <duration> [easing]"
|
|
2529
|
+
// Example: pan-left 50% 20% 80% 2000ms ease-in
|
|
2530
|
+
|
|
2531
|
+
// Parse zoom percentage (required)
|
|
2532
|
+
if (parts.length > nextIndex && parts[nextIndex].includes('%')) {
|
|
2533
|
+
const zoomStr = parts[nextIndex].split('%')[0];
|
|
2534
|
+
const parsedZoom = parseFloat(zoomStr);
|
|
2535
|
+
if (!isNaN(parsedZoom) && parsedZoom >= 0) {
|
|
2536
|
+
zoom = parsedZoom; // Store as percentage (e.g., 50 for 50%)
|
|
2537
|
+
nextIndex++;
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
// Parse start position percentage (required)
|
|
2542
|
+
if (parts.length > nextIndex && parts[nextIndex].includes('%')) {
|
|
2543
|
+
const startStr = parts[nextIndex].split('%')[0];
|
|
2544
|
+
const parsedStart = parseFloat(startStr);
|
|
2545
|
+
if (!isNaN(parsedStart) && parsedStart >= 0 && parsedStart <= 100) {
|
|
2546
|
+
// Store in appropriate variable based on pan direction
|
|
2547
|
+
if (effect === 'pan-left' || effect === 'pan-right') {
|
|
2548
|
+
panStartX = parsedStart;
|
|
2549
|
+
} else {
|
|
2550
|
+
panStartY = parsedStart;
|
|
2551
|
+
}
|
|
2552
|
+
nextIndex++;
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
// Parse end position percentage (required)
|
|
2557
|
+
if (parts.length > nextIndex && parts[nextIndex].includes('%')) {
|
|
2558
|
+
const endStr = parts[nextIndex].split('%')[0];
|
|
2559
|
+
const parsedEnd = parseFloat(endStr);
|
|
2560
|
+
if (!isNaN(parsedEnd) && parsedEnd >= 0 && parsedEnd <= 100) {
|
|
2561
|
+
// Store in appropriate variable based on pan direction
|
|
2562
|
+
if (effect === 'pan-left' || effect === 'pan-right') {
|
|
2563
|
+
panEndX = parsedEnd;
|
|
2564
|
+
} else {
|
|
2565
|
+
panEndY = parsedEnd;
|
|
2566
|
+
}
|
|
2567
|
+
nextIndex++;
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
// Parse effect duration (required)
|
|
2572
|
+
if (parts.length > nextIndex) {
|
|
2573
|
+
const durationStr = parts[nextIndex];
|
|
2574
|
+
// Simple inline parser for "1000ms" or "1s" format
|
|
2575
|
+
const match = durationStr.match(/^(\d+(?:\.\d+)?)(ms|s)$/);
|
|
2576
|
+
if (match) {
|
|
2577
|
+
const value = parseFloat(match[1]);
|
|
2578
|
+
const parsedDuration = match[2] === 's' ? value * 1000 : value;
|
|
2579
|
+
if (parsedDuration > 0) {
|
|
2580
|
+
effectDuration = parsedDuration;
|
|
2581
|
+
nextIndex++;
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
// Parse easing (optional)
|
|
2587
|
+
if (parts.length > nextIndex) {
|
|
2588
|
+
const easingStr = parts[nextIndex] as 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out';
|
|
2589
|
+
if (['linear', 'ease-in', 'ease-out', 'ease-in-out'].includes(easingStr)) {
|
|
2590
|
+
easing = easingStr;
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
return {
|
|
2596
|
+
objectFitKenBurns: effect,
|
|
2597
|
+
objectFitKenBurnsZoom: zoom,
|
|
2598
|
+
objectFitKenBurnsEffectDuration: effectDuration,
|
|
2599
|
+
objectFitKenBurnsEasing: easing,
|
|
2600
|
+
objectFitKenBurnsFocalX: focalX,
|
|
2601
|
+
objectFitKenBurnsFocalY: focalY,
|
|
2602
|
+
objectFitKenBurnsPanStartX: panStartX,
|
|
2603
|
+
objectFitKenBurnsPanStartY: panStartY,
|
|
2604
|
+
objectFitKenBurnsPanEndX: panEndX,
|
|
2605
|
+
objectFitKenBurnsPanEndY: panEndY,
|
|
2606
|
+
};
|
|
2607
|
+
}
|
|
2226
2608
|
}
|
package/src/project.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
FFmpegOption,
|
|
6
6
|
Upload,
|
|
7
7
|
AIProvider,
|
|
8
|
+
SequenceDebugInfo,
|
|
8
9
|
} from './type';
|
|
9
10
|
import { Label } from './ffmpeg';
|
|
10
11
|
import { AssetManager } from './asset-manager';
|
|
@@ -13,11 +14,13 @@ import { FilterBuffer } from './stream';
|
|
|
13
14
|
import { ExpressionContext, FragmentData } from './expression-parser';
|
|
14
15
|
import { renderContainers } from './container-renderer';
|
|
15
16
|
import { renderApps } from './app-renderer';
|
|
17
|
+
import { buildAppsIfNeeded } from './app-builder';
|
|
16
18
|
import { dirname } from 'path';
|
|
17
19
|
|
|
18
20
|
export class Project {
|
|
19
21
|
private assetManager: AssetManager;
|
|
20
22
|
private expressionContext: ExpressionContext;
|
|
23
|
+
private sequencesDebugInfo: SequenceDebugInfo[] = [];
|
|
21
24
|
|
|
22
25
|
constructor(
|
|
23
26
|
private sequencesDefinitions: SequenceDefinition[],
|
|
@@ -46,6 +49,8 @@ export class Project {
|
|
|
46
49
|
|
|
47
50
|
let buf = new FilterBuffer();
|
|
48
51
|
let mainSequence: Sequence | null = null;
|
|
52
|
+
this.sequencesDebugInfo = []; // Reset debug info
|
|
53
|
+
let sequenceIndex = 0;
|
|
49
54
|
|
|
50
55
|
this.sequencesDefinitions.forEach((sequenceDefinition) => {
|
|
51
56
|
const seq = new Sequence(
|
|
@@ -61,6 +66,14 @@ export class Project {
|
|
|
61
66
|
|
|
62
67
|
seq.build();
|
|
63
68
|
|
|
69
|
+
// Collect debug info
|
|
70
|
+
this.sequencesDebugInfo.push({
|
|
71
|
+
sequenceIndex,
|
|
72
|
+
totalDuration: seq.getTotalDuration(),
|
|
73
|
+
fragments: seq.getDebugInfo(),
|
|
74
|
+
});
|
|
75
|
+
sequenceIndex++;
|
|
76
|
+
|
|
64
77
|
if (!mainSequence) {
|
|
65
78
|
mainSequence = seq;
|
|
66
79
|
} else {
|
|
@@ -95,6 +108,47 @@ export class Project {
|
|
|
95
108
|
});
|
|
96
109
|
}
|
|
97
110
|
|
|
111
|
+
public printDebugInfo() {
|
|
112
|
+
console.log('\n=== Debug: Sequences & Fragments Timeline ===\n');
|
|
113
|
+
|
|
114
|
+
if (this.sequencesDebugInfo.length === 0) {
|
|
115
|
+
console.log('No sequences built yet.\n');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.sequencesDebugInfo.forEach((seqInfo) => {
|
|
120
|
+
console.log(`\nš¹ Sequence ${seqInfo.sequenceIndex}`);
|
|
121
|
+
console.log(` Total Duration: ${Math.round(seqInfo.totalDuration)}ms`);
|
|
122
|
+
console.log(` Fragments: ${seqInfo.fragments.length}\n`);
|
|
123
|
+
|
|
124
|
+
seqInfo.fragments.forEach((frag, index) => {
|
|
125
|
+
const status = frag.enabled ? 'ā' : 'ā';
|
|
126
|
+
|
|
127
|
+
// Show ID if it's meaningful (not auto-generated fragment_xxx)
|
|
128
|
+
const isAutoGeneratedId = frag.id.startsWith('fragment_');
|
|
129
|
+
const idDisplay = isAutoGeneratedId ? '' : ` id="${frag.id}"`;
|
|
130
|
+
|
|
131
|
+
console.log(` ${status} [${index + 1}]${idDisplay}`);
|
|
132
|
+
console.log(` Asset: ${frag.assetName}`);
|
|
133
|
+
console.log(` Start: ${Math.round(frag.startTime)}ms`);
|
|
134
|
+
console.log(` End: ${Math.round(frag.endTime)}ms`);
|
|
135
|
+
console.log(` Duration: ${Math.round(frag.duration)}ms`);
|
|
136
|
+
|
|
137
|
+
// Only show non-zero values
|
|
138
|
+
if (Math.round(frag.trimLeft) > 0) {
|
|
139
|
+
console.log(` Trim Left: ${Math.round(frag.trimLeft)}ms`);
|
|
140
|
+
}
|
|
141
|
+
if (Math.round(frag.overlayLeft) > 0) {
|
|
142
|
+
console.log(` Overlay: ${Math.round(frag.overlayLeft)}ms`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log('');
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
console.log('===========================================\n');
|
|
150
|
+
}
|
|
151
|
+
|
|
98
152
|
public getAssetManager(): AssetManager {
|
|
99
153
|
return this.assetManager;
|
|
100
154
|
}
|
|
@@ -222,10 +276,12 @@ export class Project {
|
|
|
222
276
|
* Renders all apps and creates virtual assets for them.
|
|
223
277
|
* Apps must dispatch "sts-render-complete" on document when ready,
|
|
224
278
|
* or rendering will fail after a 5-second timeout.
|
|
279
|
+
* @param forceAppBuild - If true, rebuilds apps even if output exists
|
|
225
280
|
*/
|
|
226
281
|
public async renderApps(
|
|
227
282
|
outputName: string,
|
|
228
283
|
activeCacheKeys?: Set<string>,
|
|
284
|
+
forceAppBuild: boolean = false,
|
|
229
285
|
): Promise<void> {
|
|
230
286
|
const output = this.getOutput(outputName);
|
|
231
287
|
if (!output) {
|
|
@@ -245,6 +301,12 @@ export class Project {
|
|
|
245
301
|
const apps = fragmentsWithApps.map((frag) => frag.app!);
|
|
246
302
|
const projectDir = dirname(this.projectPath);
|
|
247
303
|
|
|
304
|
+
// Build apps if needed (checks for dst/dist directories with package.json)
|
|
305
|
+
// Deduplicate app sources to avoid building the same app multiple times
|
|
306
|
+
const appSources = apps.map((app) => app.src);
|
|
307
|
+
const uniqueAppSources = [...new Set(appSources)];
|
|
308
|
+
await buildAppsIfNeeded(uniqueAppSources, projectDir, forceAppBuild);
|
|
309
|
+
|
|
248
310
|
const results = await renderApps(
|
|
249
311
|
apps,
|
|
250
312
|
output.resolution.width,
|