scrollcraft 2.0.5 → 2.0.8
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/README.md +61 -21
- package/dist/cli/fal-service.js +1 -1
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +148 -72
- package/dist/cli/processor.d.ts +1 -0
- package/dist/cli/processor.js +13 -6
- package/dist/core/CoreEngine.d.ts +31 -13
- package/dist/core/index.d.ts +3 -0
- package/dist/core/scrollcraft.umd.min.js +1 -1
- package/dist/core/types.d.ts +6 -76
- package/dist/core/types.js +1 -1
- package/dist/pipeline/browser-driver.d.ts +31 -0
- package/dist/pipeline/browser-driver.js +176 -0
- package/dist/pipeline/fal-service.d.ts +15 -0
- package/dist/pipeline/fal-service.js +101 -0
- package/dist/pipeline/index.d.ts +21 -0
- package/dist/pipeline/index.js +223 -0
- package/dist/pipeline/node-driver.d.ts +18 -0
- package/dist/pipeline/node-driver.js +108 -0
- package/dist/pipeline/types.d.ts +43 -0
- package/dist/pipeline/types.js +2 -0
- package/dist/react/ScrollCraftProvider.d.ts +9 -8
- package/dist/react/index.js +1 -1
- package/package.json +20 -19
package/README.md
CHANGED
|
@@ -1,53 +1,93 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ScrollCraft
|
|
2
2
|
|
|
3
|
-
**Transform
|
|
3
|
+
**Transform media into interactive web experiences.**
|
|
4
4
|
|
|
5
|
-
ScrollCraft
|
|
5
|
+
ScrollCraft is a modern animation SDK built for the era of high-performance, agent-driven development. It allows you to transform standard video or images into web assets that precisely track subjects and depth.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Installation
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
npm install scrollcraft
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## The Universal Asset Pipeline
|
|
18
|
+
|
|
19
|
+
ScrollCraft features a **Universal Asset Pipeline** that runs identical logic in both the **CLI** (Node.js) and the **Browser** (ideal for CMS integrations like WordPress).
|
|
20
|
+
|
|
21
|
+
### 1. CLI Usage (Node.js)
|
|
22
|
+
Transform your video into a ScrollCraft project from your terminal:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# This will extract frames, track the subject, and generate optimized variants and depth maps.
|
|
26
|
+
npx scft create "your-video.mp4" --name "my-project" --track "apple" --cloud --depth
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 2. Programmatic Usage (Browser/Node)
|
|
30
|
+
You can also import the pipeline into your own React apps or dashboard:
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
import { AssetPipeline } from 'scrollcraft/pipeline';
|
|
14
34
|
|
|
15
|
-
|
|
35
|
+
const pipeline = new AssetPipeline({
|
|
36
|
+
apiKey: process.env.FAL_KEY,
|
|
37
|
+
onProgress: (p) => console.log(`${p.step}: ${p.percent}%`)
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Returns the project configuration or a ZIP blob
|
|
41
|
+
const project = await pipeline.create({
|
|
42
|
+
input: videoFile, // Can be a File object or Path
|
|
43
|
+
name: "my-project",
|
|
44
|
+
variants: [720, 1080],
|
|
45
|
+
outputZip: true // Perfect for CMS uploads
|
|
46
|
+
});
|
|
16
47
|
```
|
|
17
48
|
|
|
49
|
+
---
|
|
50
|
+
|
|
18
51
|
```tsx
|
|
19
|
-
// 2. Drop it into your
|
|
20
|
-
import project from './
|
|
21
|
-
import { ScrollCraftProvider, ScrollCraftCanvas, SubjectLayer } from 'scrollcraft';
|
|
52
|
+
// 2. Drop it into your Next.js app
|
|
53
|
+
import project from './my-project/scrollcraft.json';
|
|
54
|
+
import { ScrollCraftProvider, ScrollCraftCanvas, SubjectLayer } from 'scrollcraft/react';
|
|
22
55
|
|
|
23
56
|
const App = () => (
|
|
24
|
-
<ScrollCraftProvider
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
57
|
+
<ScrollCraftProvider
|
|
58
|
+
project={project}
|
|
59
|
+
scrub={0.1} // Smooth interpolation (0 = instant, 1 = heavy lag)
|
|
60
|
+
>
|
|
61
|
+
<div style={{ height: '400vh' }}>
|
|
62
|
+
<ScrollCraftCanvas />
|
|
63
|
+
|
|
64
|
+
{/* Automatically follows the 'apple' tracked in the CLI */}
|
|
65
|
+
<SubjectLayer id="main">
|
|
66
|
+
<h2>Pinned Element</h2>
|
|
67
|
+
</SubjectLayer>
|
|
68
|
+
</div>
|
|
29
69
|
</ScrollCraftProvider>
|
|
30
70
|
);
|
|
31
71
|
```
|
|
32
72
|
|
|
33
73
|
---
|
|
34
74
|
|
|
35
|
-
##
|
|
75
|
+
## Documentation & Guides
|
|
36
76
|
|
|
37
77
|
Choose your path based on your role:
|
|
38
78
|
|
|
39
79
|
### 👤 For Humans
|
|
40
|
-
- [**Core Architecture**](https://github.com/aleskozelsky/scrollcraft/blob/main/packages/docs/architecture.md): Understand the state-snapshot engine.
|
|
41
|
-
- [**Asset Pipeline**](https://github.com/aleskozelsky/scrollcraft/blob/main/packages/docs/asset-pipeline.md): Learn how to use the CLI and AI tracking.
|
|
42
|
-
- [**React Hooks**](https://github.com/aleskozelsky/scrollcraft/blob/main/packages/docs/react-integration.md): Build custom interactive components.
|
|
80
|
+
- [**Core Architecture**](https://github.com/aleskozelsky/scrollcraft/blob/main/packages/docs/app/architecture/page.md): Understand the state-snapshot engine.
|
|
81
|
+
- [**Asset Pipeline**](https://github.com/aleskozelsky/scrollcraft/blob/main/packages/docs/app/asset-pipeline/page.md): Learn how to use the CLI and AI tracking.
|
|
82
|
+
- [**React Hooks**](https://github.com/aleskozelsky/scrollcraft/blob/main/packages/docs/app/react-integration/page.md): Build custom interactive components.
|
|
43
83
|
|
|
44
84
|
### 🤖 For AI Agents
|
|
45
85
|
- [**AGENTS.md**](https://github.com/aleskozelsky/scrollcraft/blob/main/AGENTS.md): Technical standard operating procedures for the repository.
|
|
46
|
-
- [**AI Integration Protocol**](https://github.com/aleskozelsky/scrollcraft/blob/main/packages/docs/ai-integration.md): How to prompt agents to build scenes for you.
|
|
86
|
+
- [**AI Integration Protocol**](https://github.com/aleskozelsky/scrollcraft/blob/main/packages/docs/app/ai-integration/page.md): How to prompt agents to build scenes for you.
|
|
47
87
|
|
|
48
88
|
---
|
|
49
89
|
|
|
50
|
-
##
|
|
90
|
+
## Performance & Tech
|
|
51
91
|
- **WebGL Accelerated**: High-FPS rendering even for 4K sequences.
|
|
52
92
|
- **AI Subject Tracking**: Automatic (x,y) pinning via SAM 3.
|
|
53
93
|
- **Mouse-Interactive Parallax**: Automatic 3D depth map generation and rendering.
|
package/dist/cli/fal-service.js
CHANGED
|
@@ -117,7 +117,7 @@ class FalService {
|
|
|
117
117
|
}
|
|
118
118
|
});
|
|
119
119
|
// Debug output to see what Fal is actually returning
|
|
120
|
-
await fs.writeFile('debug_fal.json', JSON.stringify(result, null, 2));
|
|
120
|
+
//await fs.writeFile('debug_fal.json', JSON.stringify(result, null, 2));
|
|
121
121
|
const payload = result.data || result;
|
|
122
122
|
if (!payload.video || !payload.video.url) {
|
|
123
123
|
throw new Error(`AI Depth Map generation failed. No video URL returned. Saved response to debug_fal.json`);
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
import 'dotenv/config';
|
package/dist/cli/index.js
CHANGED
|
@@ -43,9 +43,69 @@ const fs = __importStar(require("fs-extra"));
|
|
|
43
43
|
const path = __importStar(require("path"));
|
|
44
44
|
const child_process_1 = require("child_process");
|
|
45
45
|
const ffmpeg_static_1 = __importDefault(require("ffmpeg-static"));
|
|
46
|
-
const
|
|
47
|
-
const
|
|
46
|
+
const pipeline_1 = require("../pipeline");
|
|
47
|
+
const readline = __importStar(require("readline"));
|
|
48
|
+
require("dotenv/config");
|
|
48
49
|
const pkg = require('../../package.json');
|
|
50
|
+
/**
|
|
51
|
+
* MEDIA QUERY BUILDER
|
|
52
|
+
*/
|
|
53
|
+
function buildVariantsFromIds(input) {
|
|
54
|
+
const result = [];
|
|
55
|
+
const orientations = ['portrait', 'landscape'];
|
|
56
|
+
// 1. Process each "Target" resolution
|
|
57
|
+
input.forEach(item => {
|
|
58
|
+
let res = 0;
|
|
59
|
+
if (typeof item === 'number')
|
|
60
|
+
res = item;
|
|
61
|
+
else if (typeof item === 'string')
|
|
62
|
+
res = parseInt(item);
|
|
63
|
+
else if (item.height)
|
|
64
|
+
res = item.height; // Assume height is the defining metric
|
|
65
|
+
if (!res || isNaN(res))
|
|
66
|
+
return;
|
|
67
|
+
orientations.forEach(orient => {
|
|
68
|
+
const isPortrait = orient === 'portrait';
|
|
69
|
+
const width = isPortrait ? res : Math.round(res * (16 / 9));
|
|
70
|
+
const height = isPortrait ? Math.round(res * (16 / 9)) : res;
|
|
71
|
+
result.push({
|
|
72
|
+
id: `${res}p_${orient.substring(0, 1)}`,
|
|
73
|
+
width,
|
|
74
|
+
height,
|
|
75
|
+
orientation: orient,
|
|
76
|
+
aspectRatio: isPortrait ? '9:16' : '16:9',
|
|
77
|
+
media: `(orientation: ${orient})` // Minimal fallback
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
// 2. Sort by height (ascending) so the engine finds the first one that fits
|
|
82
|
+
return result.sort((a, b) => a.height - b.height);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* CONFIG LOADER
|
|
86
|
+
* Looks for scrollcraft.config.js/ts in the current working directory.
|
|
87
|
+
*/
|
|
88
|
+
async function loadProjectConfig() {
|
|
89
|
+
const possiblePaths = [
|
|
90
|
+
path.join(process.cwd(), 'scrollcraft.cli.config.js'),
|
|
91
|
+
path.join(process.cwd(), 'scrollcraft.cli.config.cjs'),
|
|
92
|
+
path.join(process.cwd(), 'scrollcraft.cli.config.ts')
|
|
93
|
+
];
|
|
94
|
+
for (const p of possiblePaths) {
|
|
95
|
+
if (fs.existsSync(p)) {
|
|
96
|
+
try {
|
|
97
|
+
// For simplicity in CLI we handle commonjs/esm basics
|
|
98
|
+
// If it's TS, it might need jiti or ts-node, but let's assume JS for now
|
|
99
|
+
// or use a simple dynamic import if supported.
|
|
100
|
+
return require(p);
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
console.warn(chalk_1.default.yellow(`⚠️ Found config at ${p} but failed to load it. Skipping...`));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
49
109
|
/**
|
|
50
110
|
* Robust FFmpeg Detection
|
|
51
111
|
* Prioritizes bundled static binary, then system PATH.
|
|
@@ -68,16 +128,33 @@ program
|
|
|
68
128
|
.name('scft')
|
|
69
129
|
.description('ScrollCraft CLI - Immersive Web SDK')
|
|
70
130
|
.version(pkg.version);
|
|
131
|
+
/**
|
|
132
|
+
* Interactive Helper
|
|
133
|
+
*/
|
|
134
|
+
async function prompt(question, defaultValue) {
|
|
135
|
+
const rl = readline.createInterface({
|
|
136
|
+
input: process.stdin,
|
|
137
|
+
output: process.stdout
|
|
138
|
+
});
|
|
139
|
+
return new Promise(resolve => {
|
|
140
|
+
rl.question(`${chalk_1.default.cyan('?')} ${question}${defaultValue ? ` (${defaultValue})` : ''}: `, (answer) => {
|
|
141
|
+
rl.close();
|
|
142
|
+
resolve(answer.trim() || defaultValue || '');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
71
146
|
program
|
|
72
147
|
.command('create')
|
|
73
148
|
.description('ONE-STEP: Transform video/images into a responsive ScrollCraft')
|
|
74
|
-
.argument('
|
|
75
|
-
.option('-o, --output <dir>', 'Output directory
|
|
76
|
-
.option('-p, --
|
|
149
|
+
.argument('[input]', 'Path to input video or directory of images')
|
|
150
|
+
.option('-o, --output <dir>', 'Output directory (deprecated, use --name)')
|
|
151
|
+
.option('-p, --track <text>', 'Text prompt for subject tracking', 'main subject')
|
|
152
|
+
.option('-n, --name <string>', 'Name of the project')
|
|
153
|
+
.option('-v, --variants <string>', 'Comma-separated target resolutions (e.g. 720,1080)')
|
|
77
154
|
.option('-s, --step <number>', 'Process every Nth frame (default: 1)', '1')
|
|
78
155
|
.option('--cloud', 'Use Fal.ai for tracking and refinement', false)
|
|
79
156
|
.option('--depth', 'Generate a 3D depth map for the displacement effect (Requires --cloud)', false)
|
|
80
|
-
.action(async (
|
|
157
|
+
.action(async (inputArg, opts) => {
|
|
81
158
|
console.log(chalk_1.default.bold.blue('\n🎞️ ScrollCraft Asset Pipeline\n'));
|
|
82
159
|
// 0. PRE-FLIGHT CHECK
|
|
83
160
|
const ffmpegPath = getFFmpegPath();
|
|
@@ -87,79 +164,78 @@ program
|
|
|
87
164
|
console.log('Please install it manually or ensure regular npm install was successful.');
|
|
88
165
|
process.exit(1);
|
|
89
166
|
}
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
await fs.copy(path.join(input, files[i]), path.join(tempDir, frameName));
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
// 2. SUBJECT TRACKING & DEPTH MAP
|
|
124
|
-
let trackingData = [];
|
|
125
|
-
let hasDepth = false;
|
|
126
|
-
if (opts.cloud) {
|
|
127
|
-
const fal = new fal_service_1.FalService();
|
|
128
|
-
// Tracking
|
|
129
|
-
trackingData = await fal.trackSubject(input, opts.prompt);
|
|
130
|
-
// Depth Map
|
|
131
|
-
if (opts.depth) {
|
|
132
|
-
console.log(chalk_1.default.yellow(`\n🕳️ Generating Depth Map via AI...`));
|
|
133
|
-
const depthUrl = await fal.generateDepthMap(input);
|
|
134
|
-
console.log(chalk_1.default.yellow(`📥 Downloading Depth Map Video...`));
|
|
135
|
-
const res = await fetch(depthUrl);
|
|
136
|
-
const arrayBuffer = await res.arrayBuffer();
|
|
137
|
-
const depthVideoPath = path.join(tempDir, 'depth_video.mp4');
|
|
138
|
-
await fs.writeFile(depthVideoPath, Buffer.from(arrayBuffer));
|
|
139
|
-
console.log(chalk_1.default.yellow(`📦 Extracting depth frames...`));
|
|
140
|
-
(0, child_process_1.execSync)(`"${ffmpegPath}" -i "${depthVideoPath}" -vf "fps=30" "${tempDir}/depth_%04d.png"`, { stdio: 'inherit' });
|
|
141
|
-
hasDepth = true;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
console.log(chalk_1.default.dim('ℹ️ Local tracking not yet implemented. Using center-pinned defaults.'));
|
|
146
|
-
const frames = (await fs.readdir(tempDir)).filter(f => f.startsWith('frame_'));
|
|
147
|
-
trackingData = frames.map((_, i) => ({ frame: i, x: 0.5, y: 0.5, scale: 0 }));
|
|
167
|
+
const projectConfig = await loadProjectConfig();
|
|
168
|
+
let input = inputArg;
|
|
169
|
+
let track = opts.track;
|
|
170
|
+
let projectName = opts.name;
|
|
171
|
+
let useTracking = opts.cloud; // Default to cloud if flag set
|
|
172
|
+
let useDepth = opts.depth;
|
|
173
|
+
let customVariants = projectConfig?.variants || (opts.variants ? buildVariantsFromIds(opts.variants.split(',')) : null);
|
|
174
|
+
// 1. INPUT VALIDATION (Immediate)
|
|
175
|
+
if (!input) {
|
|
176
|
+
input = await prompt('Path to input video or directory of images');
|
|
177
|
+
}
|
|
178
|
+
if (!input || !fs.existsSync(input)) {
|
|
179
|
+
console.error(chalk_1.default.red(`\n❌ Error: Input path "${input || ''}" does not exist.`));
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
// 2. PROJECT NAME & SETTINGS
|
|
183
|
+
if (!projectName) {
|
|
184
|
+
projectName = await prompt('Project name', 'scrollcraft-project');
|
|
185
|
+
}
|
|
186
|
+
let step = parseInt(opts.step) || 1;
|
|
187
|
+
if (!inputArg) {
|
|
188
|
+
const stepInput = await prompt('Process every Nth frame (Step size)', '1');
|
|
189
|
+
step = parseInt(stepInput) || 1;
|
|
190
|
+
}
|
|
191
|
+
// AI Tracking logic preserved in CLI wrapper...
|
|
192
|
+
// ...
|
|
193
|
+
const pipeline = new pipeline_1.AssetPipeline({
|
|
194
|
+
apiKey: process.env.FAL_KEY,
|
|
195
|
+
onProgress: (p) => {
|
|
196
|
+
// You could add a progress bar here
|
|
148
197
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
198
|
+
});
|
|
199
|
+
try {
|
|
200
|
+
await pipeline.create({
|
|
201
|
+
input: input,
|
|
202
|
+
name: projectName,
|
|
203
|
+
track: useTracking ? track : undefined,
|
|
204
|
+
hasDepth: useDepth,
|
|
205
|
+
variants: customVariants || [720, 1080],
|
|
206
|
+
step: step
|
|
207
|
+
});
|
|
155
208
|
console.log(chalk_1.default.bold.green(`\n✅ Project Created Successfully!`));
|
|
156
|
-
console.log(chalk_1.default.white(`📍 Output: ${
|
|
209
|
+
console.log(chalk_1.default.white(`📍 Output: ${projectName}`));
|
|
157
210
|
console.log(chalk_1.default.white(`📜 Config: scrollcraft.json`));
|
|
158
211
|
console.log(chalk_1.default.cyan(`\nNext: Import the .json into your <ScrollCraftProvider />\n`));
|
|
159
212
|
}
|
|
160
213
|
catch (err) {
|
|
161
|
-
console.error(chalk_1.default.red(`\n❌ Error: ${err.message}`));
|
|
214
|
+
console.error(chalk_1.default.red(`\n❌ Error during pipeline: ${err.message}`));
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
// NEW UPDATE COMMAND
|
|
219
|
+
program
|
|
220
|
+
.command('update')
|
|
221
|
+
.description('Rerun extraction and tracking on an existing project')
|
|
222
|
+
.argument('<dir>', 'Project directory')
|
|
223
|
+
.option('-p, --track <text>', 'Additional subject to track')
|
|
224
|
+
.action(async (dir, opts) => {
|
|
225
|
+
console.log(chalk_1.default.bold.yellow('\n♻️ ScrollCraft Update Pipeline\n'));
|
|
226
|
+
const projectPath = path.resolve(dir);
|
|
227
|
+
const configPath = path.join(projectPath, 'scrollcraft.json');
|
|
228
|
+
if (!fs.existsSync(configPath)) {
|
|
229
|
+
console.error(chalk_1.default.red('❌ Not a valid ScrollCraft project directory (missing scrollcraft.json).'));
|
|
162
230
|
process.exit(1);
|
|
163
231
|
}
|
|
232
|
+
const config = await fs.readJson(configPath);
|
|
233
|
+
if (config.version !== pkg.version) {
|
|
234
|
+
console.warn(chalk_1.default.yellow(`⚠️ Version Mismatch: Project is ${config.version}, CLI is ${pkg.version}`));
|
|
235
|
+
}
|
|
236
|
+
console.log(chalk_1.default.red('⚠️ UNDER CONSTRUCTION'));
|
|
237
|
+
console.log(chalk_1.default.yellow('The "update" command is currently being refactored for the Universal Pipeline.'));
|
|
238
|
+
console.log(chalk_1.default.dim('Please use "scft create" to regenerate your project for now.\n'));
|
|
239
|
+
process.exit(0);
|
|
164
240
|
});
|
|
165
241
|
program.parse(process.argv);
|
package/dist/cli/processor.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export declare class AssetProcessor {
|
|
|
15
15
|
processVariants(sourceFramesDir: string, trackingData: SubjectFrameData[], options?: {
|
|
16
16
|
step?: number;
|
|
17
17
|
hasDepth?: boolean;
|
|
18
|
+
variants?: any[];
|
|
18
19
|
}): Promise<AssetVariant[]>;
|
|
19
20
|
private subjectToSharpPosition;
|
|
20
21
|
/**
|
package/dist/cli/processor.js
CHANGED
|
@@ -40,6 +40,7 @@ exports.AssetProcessor = void 0;
|
|
|
40
40
|
const fs = __importStar(require("fs-extra"));
|
|
41
41
|
const path = __importStar(require("path"));
|
|
42
42
|
const sharp_1 = __importDefault(require("sharp"));
|
|
43
|
+
const pkg = require('../../package.json');
|
|
43
44
|
/**
|
|
44
45
|
* LOCAL ASSET PROCESSOR
|
|
45
46
|
*
|
|
@@ -65,7 +66,7 @@ class AssetProcessor {
|
|
|
65
66
|
const framesToProcess = allFrames.filter((_, i) => i % step === 0);
|
|
66
67
|
const variants = [];
|
|
67
68
|
// Define our target variants
|
|
68
|
-
const configs = [
|
|
69
|
+
const configs = options.variants || [
|
|
69
70
|
{ id: 'mobile', width: 720, height: 1280, media: '(max-width: 600px)' },
|
|
70
71
|
{ id: 'desktop', width: 1920, height: 1080, media: '(min-width: 601px)' }
|
|
71
72
|
];
|
|
@@ -101,8 +102,9 @@ class AssetProcessor {
|
|
|
101
102
|
fit: 'cover',
|
|
102
103
|
position: this.subjectToSharpPosition(subject)
|
|
103
104
|
})
|
|
104
|
-
//
|
|
105
|
+
// Grayscale, then blur slightly to prevent "staircase" effects in displacement
|
|
105
106
|
.grayscale()
|
|
107
|
+
.blur(2)
|
|
106
108
|
.webp({ quality: 80 })
|
|
107
109
|
.toFile(depthTargetPath);
|
|
108
110
|
}
|
|
@@ -113,14 +115,20 @@ class AssetProcessor {
|
|
|
113
115
|
frame: i
|
|
114
116
|
});
|
|
115
117
|
}
|
|
118
|
+
// Extract tracking data into its own file
|
|
119
|
+
const trackingPath = path.join(variantDir, '000_tracking-main.json');
|
|
120
|
+
await fs.writeJson(trackingPath, variantTracking, { spaces: 2 });
|
|
116
121
|
variants.push({
|
|
117
122
|
id: config.id,
|
|
118
123
|
media: config.media,
|
|
124
|
+
width: config.width,
|
|
125
|
+
height: config.height,
|
|
126
|
+
orientation: config.orientation,
|
|
119
127
|
path: `./${config.id}`, // Relative path in the final output
|
|
120
|
-
aspectRatio: config.
|
|
128
|
+
aspectRatio: config.aspectRatio,
|
|
121
129
|
frameCount: framesToProcess.length,
|
|
122
130
|
hasDepthMap: options.hasDepth,
|
|
123
|
-
|
|
131
|
+
subjects: ['main']
|
|
124
132
|
});
|
|
125
133
|
}
|
|
126
134
|
return variants;
|
|
@@ -138,9 +146,8 @@ class AssetProcessor {
|
|
|
138
146
|
*/
|
|
139
147
|
async saveConfig(variants) {
|
|
140
148
|
const config = {
|
|
141
|
-
version:
|
|
149
|
+
version: pkg.version,
|
|
142
150
|
settings: {
|
|
143
|
-
fps: 30,
|
|
144
151
|
baseResolution: { width: 1920, height: 1080 },
|
|
145
152
|
scrollMode: 'vh'
|
|
146
153
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ProjectConfiguration } from './types';
|
|
2
2
|
/**
|
|
3
|
-
* SCROLLCRAFT
|
|
3
|
+
* SCROLLCRAFT CORE ENGINE
|
|
4
4
|
*
|
|
5
5
|
* A declarative, performant engine that maps scroll progress
|
|
6
6
|
* to high-performance image sequence rendering.
|
|
@@ -12,10 +12,23 @@ export declare class CoreEngine {
|
|
|
12
12
|
private canvas;
|
|
13
13
|
private ctx;
|
|
14
14
|
private renderer;
|
|
15
|
+
basePath: string;
|
|
16
|
+
scrub: number;
|
|
17
|
+
private targetProgress;
|
|
18
|
+
private currentProgress;
|
|
19
|
+
private rafId;
|
|
20
|
+
private destroyed;
|
|
15
21
|
private imageCache;
|
|
16
22
|
private depthCache;
|
|
17
23
|
private scrollTimeout;
|
|
18
|
-
|
|
24
|
+
private trackingDataCache;
|
|
25
|
+
onFrameChange?: (frame: number, progress: number) => void;
|
|
26
|
+
private boundResize;
|
|
27
|
+
constructor(config: ProjectConfiguration, options?: {
|
|
28
|
+
scrub?: number;
|
|
29
|
+
});
|
|
30
|
+
destroy(): void;
|
|
31
|
+
static init(container: HTMLElement, configUrl: string): Promise<CoreEngine>;
|
|
19
32
|
/**
|
|
20
33
|
* ATTACH CANVAS
|
|
21
34
|
* Connects the engine to a DOM element for rendering.
|
|
@@ -23,24 +36,29 @@ export declare class CoreEngine {
|
|
|
23
36
|
attachCanvas(canvas: HTMLCanvasElement): void;
|
|
24
37
|
private resizeCanvas;
|
|
25
38
|
/**
|
|
26
|
-
*
|
|
27
|
-
* Selects the best image
|
|
39
|
+
* SMART VARIANT SELECTION
|
|
40
|
+
* Selects the best image variant based on physical pixel requirements
|
|
41
|
+
* and the dimensions of the parent container.
|
|
28
42
|
*/
|
|
29
43
|
private detectBestVariant;
|
|
30
44
|
private clearCache;
|
|
31
45
|
private preloadInitial;
|
|
32
46
|
/**
|
|
33
47
|
* THE PLAYER ENGINE
|
|
34
|
-
*
|
|
48
|
+
* Sets the target scroll progress. Actual rendering interpolates to this value.
|
|
35
49
|
*/
|
|
36
|
-
update(progress: number):
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
update(progress: number): void;
|
|
51
|
+
private updateLoop;
|
|
52
|
+
private calculateFrame;
|
|
53
|
+
/**
|
|
54
|
+
* LOAD SUBJECT TRACKING (On-Demand)
|
|
55
|
+
*/
|
|
56
|
+
loadTrackingData(subjectId: string): Promise<void>;
|
|
57
|
+
getTrackedCoords(subjectId: string, frame: number): {
|
|
58
|
+
x: number;
|
|
59
|
+
y: number;
|
|
60
|
+
scale?: number;
|
|
61
|
+
};
|
|
44
62
|
/**
|
|
45
63
|
* RENDER LOOP
|
|
46
64
|
* Draws the image to the canvas with object-fit: cover logic.
|
package/dist/core/index.d.ts
CHANGED
|
@@ -11,4 +11,4 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.ScrollCraft=t():e.ScrollCraft=t()}(this,()=>(()=>{"use strict";var e={};return e=e.default})());
|
|
14
|
+
!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ScrollCraft=e():t.ScrollCraft=e()}(this,()=>(()=>{"use strict";var t={d:(e,i)=>{for(var s in i)t.o(i,s)&&!t.o(e,s)&&Object.defineProperty(e,s,{enumerable:!0,get:i[s]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e)},e={};t.d(e,{default:()=>a});class i{constructor(t){if(this.targetMouse={x:0,y:0},this.currentMouse={x:0,y:0},this.animationFrameId=0,this.animate=()=>{if(this.currentMouse.x+=.1*(this.targetMouse.x-this.currentMouse.x),this.currentMouse.y+=.1*(this.targetMouse.y-this.currentMouse.y),this.gl&&this.program){this.gl.useProgram(this.program);const t=this.gl.getUniformLocation(this.program,"u_mouse");this.gl.uniform2f(t,this.currentMouse.x,this.currentMouse.y),this.draw()}this.animationFrameId=requestAnimationFrame(this.animate)},this.gl=t.getContext("webgl",{alpha:!1,antialias:!1}),!this.gl)throw new Error("WebGL not supported");this.program=this.createProgram("\n attribute vec2 a_position;\n varying vec2 v_texCoord;\n void main() {\n gl_Position = vec4(a_position, 0.0, 1.0);\n // Convert -1 -> 1 to 0 -> 1 for UVs\n v_texCoord = a_position * 0.5 + 0.5;\n v_texCoord.y = 1.0 - v_texCoord.y;\n }\n ","\n precision mediump float;\n uniform sampler2D u_image;\n uniform sampler2D u_depthMap;\n uniform vec2 u_resolution;\n uniform vec2 u_imageResolution;\n uniform vec2 u_mouse;\n uniform bool u_hasDepth;\n varying vec2 v_texCoord;\n\n void main() {\n // object-fit: cover math\n vec2 ratio = vec2(\n min((u_resolution.x / u_resolution.y) / (u_imageResolution.x / u_imageResolution.y), 1.0),\n min((u_resolution.y / u_resolution.x) / (u_imageResolution.y / u_imageResolution.x), 1.0)\n );\n vec2 uv = vec2(\n v_texCoord.x * ratio.x + (1.0 - ratio.x) * 0.5,\n v_texCoord.y * ratio.y + (1.0 - ratio.y) * 0.5\n );\n\n if (u_hasDepth) {\n float depth = texture2D(u_depthMap, uv).r;\n // White is close (1), Black is far (0). We move close objects more.\n vec2 parallax = u_mouse * depth * 0.04;\n uv += parallax;\n }\n \n gl_FragColor = texture2D(u_image, uv);\n }\n "),this.gl.useProgram(this.program),this.positionBuffer=this.gl.createBuffer(),this.gl.bindBuffer(this.gl.ARRAY_BUFFER,this.positionBuffer),this.gl.bufferData(this.gl.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,-1,1,1,-1,1,1]),this.gl.STATIC_DRAW),this.texture=this.gl.createTexture(),this.depthTexture=this.gl.createTexture(),window.addEventListener("mousemove",t=>{this.targetMouse.x=t.clientX/window.innerWidth*2-1,this.targetMouse.y=-t.clientY/window.innerHeight*2+1}),this.animate()}createProgram(t,e){const i=this.gl.createShader(this.gl.VERTEX_SHADER);this.gl.shaderSource(i,t),this.gl.compileShader(i);const s=this.gl.createShader(this.gl.FRAGMENT_SHADER);this.gl.shaderSource(s,e),this.gl.compileShader(s);const r=this.gl.createProgram();return this.gl.attachShader(r,i),this.gl.attachShader(r,s),this.gl.linkProgram(r),r}render(t,e,i,s){this.gl.useProgram(this.program),this.gl.activeTexture(this.gl.TEXTURE0),this.gl.bindTexture(this.gl.TEXTURE_2D,this.texture),this.gl.texImage2D(this.gl.TEXTURE_2D,0,this.gl.RGBA,this.gl.RGBA,this.gl.UNSIGNED_BYTE,t),this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_WRAP_S,this.gl.CLAMP_TO_EDGE),this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_WRAP_T,this.gl.CLAMP_TO_EDGE),this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_MIN_FILTER,this.gl.LINEAR),this.gl.activeTexture(this.gl.TEXTURE1),this.gl.bindTexture(this.gl.TEXTURE_2D,this.depthTexture),e&&(this.gl.texImage2D(this.gl.TEXTURE_2D,0,this.gl.RGBA,this.gl.RGBA,this.gl.UNSIGNED_BYTE,e),this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_WRAP_S,this.gl.CLAMP_TO_EDGE),this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_WRAP_T,this.gl.CLAMP_TO_EDGE),this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_MIN_FILTER,this.gl.LINEAR)),this.gl.uniform1i(this.gl.getUniformLocation(this.program,"u_image"),0),this.gl.uniform1i(this.gl.getUniformLocation(this.program,"u_depthMap"),1),this.gl.uniform1i(this.gl.getUniformLocation(this.program,"u_hasDepth"),e?1:0),this.gl.uniform2f(this.gl.getUniformLocation(this.program,"u_resolution"),i,s),this.gl.uniform2f(this.gl.getUniformLocation(this.program,"u_imageResolution"),t.naturalWidth,t.naturalHeight);const r=this.gl.getAttribLocation(this.program,"a_position");this.gl.enableVertexAttribArray(r),this.gl.bindBuffer(this.gl.ARRAY_BUFFER,this.positionBuffer),this.gl.vertexAttribPointer(r,2,this.gl.FLOAT,!1,0,0),this.gl.viewport(0,0,i,s),this.draw()}draw(){this.gl.drawArrays(this.gl.TRIANGLES,0,6)}destroy(){cancelAnimationFrame(this.animationFrameId)}}var s=function(t,e,i,s){return new(i||(i=Promise))(function(r,a){function n(t){try{h(s.next(t))}catch(t){a(t)}}function o(t){try{h(s.throw(t))}catch(t){a(t)}}function h(t){var e;t.done?r(t.value):(e=t.value,e instanceof i?e:new i(function(t){t(e)})).then(n,o)}h((s=s.apply(t,e||[])).next())})};class r{constructor(t,e={}){this.currentFrame=-1,this.activeVariant=null,this.canvas=null,this.ctx=null,this.renderer=null,this.basePath="",this.scrub=0,this.targetProgress=0,this.currentProgress=0,this.rafId=0,this.destroyed=!1,this.imageCache=new Map,this.depthCache=new Map,this.scrollTimeout=null,this.trackingDataCache=new Map,this.config=t,this.basePath=t.settings.basePath||"",this.scrub=e.scrub||0,this.detectBestVariant(),this.boundResize=()=>{this.detectBestVariant(),this.resizeCanvas(),this.render()},window.addEventListener("resize",this.boundResize),this.updateLoop=this.updateLoop.bind(this),this.rafId=requestAnimationFrame(this.updateLoop)}destroy(){this.destroyed=!0,this.rafId&&cancelAnimationFrame(this.rafId),window.removeEventListener("resize",this.boundResize),this.scrollTimeout&&clearTimeout(this.scrollTimeout),this.clearCache(),this.trackingDataCache.clear(),this.canvas=null,this.ctx=null,this.renderer=null,this.onFrameChange=void 0}static init(t,e){return s(this,void 0,void 0,function*(){const i=yield fetch(e);if(!i.ok)throw new Error(`Failed to load config: ${i.statusText}`);const s=yield i.json(),a=e.substring(0,e.lastIndexOf("/"));s.settings||(s.settings={baseResolution:{width:1920,height:1080},scrollMode:"vh"}),s.settings.basePath=s.settings.basePath||a;const n=new r(s,{scrub:"object"==typeof s.settings?s.settings.scrub:0});let o=t.querySelector("canvas");return o||(o=document.createElement("canvas"),o.style.width="100%",o.style.height="100%",o.style.display="block",o.style.objectFit="cover",t.appendChild(o)),n.attachCanvas(o),n})}attachCanvas(t){this.canvas=t;try{this.renderer=new i(t)}catch(e){console.warn("WebGL failed, falling back to 2D",e),this.ctx=t.getContext("2d",{alpha:!1})}this.resizeCanvas(),this.render()}resizeCanvas(){if(!this.canvas)return;const t=window.innerWidth,e=window.innerHeight,i=window.devicePixelRatio||1;this.canvas.width=t*i,this.canvas.height=e*i,this.ctx&&this.ctx.scale(i,i)}detectBestVariant(){var t;const e=this.config.assets[0];if(!e)return;const i=this.canvas?this.canvas.getBoundingClientRect():{width:window.innerWidth,height:window.innerHeight},s=i.height>i.width,r=i.width*(window.devicePixelRatio||1),a=e.variants.filter(t=>{const e="portrait"===t.orientation||parseInt(t.aspectRatio.split(":")[1])>parseInt(t.aspectRatio.split(":")[0]);return s===e});a.sort((t,e)=>t.frameCount-e.frameCount);const n=a.find(t=>t.width>=r)||a[a.length-1];n?(null===(t=this.activeVariant)||void 0===t?void 0:t.id)!==n.id&&(console.log(`🎯 Variant Switched: ${n.id} (${s?"Portrait":"Landscape"})`),this.activeVariant=n,this.clearCache(),this.preloadInitial()):console.warn("[CoreEngine] No suitable variant found")}clearCache(){this.imageCache.clear(),this.depthCache.clear()}preloadInitial(){for(let t=0;t<15;t++)this.getImage(t)}update(t){this.targetProgress=Math.max(0,Math.min(1,t))}updateLoop(){if(this.destroyed)return;this.rafId=requestAnimationFrame(this.updateLoop);const t=this.scrub;if(t>0){const e=Math.max(.01,1-t);this.currentProgress+=(this.targetProgress-this.currentProgress)*e}else this.currentProgress=this.targetProgress;Math.abs(this.targetProgress-this.currentProgress)<1e-4&&(this.currentProgress=this.targetProgress),this.calculateFrame(this.currentProgress)}calculateFrame(t){const e=this.config.timeline.scenes[0];if(!e)return;const i=e.assetRange[1]-e.assetRange[0],s=Math.floor(e.assetRange[0]+t*i),r=Math.max(0,Math.min(s,e.assetRange[1]));r!==this.currentFrame&&(this.currentFrame=r,this.render(),this.getImage(this.currentFrame+5),this.getImage(this.currentFrame+10),this.scrollTimeout&&clearTimeout(this.scrollTimeout),this.scrollTimeout=setTimeout(()=>{this.loadDepthMap(this.currentFrame)},100),this.onFrameChange&&this.onFrameChange(this.currentFrame,t))}loadTrackingData(t){return s(this,void 0,void 0,function*(){var e;if(!this.activeVariant)return;if(!(null===(e=this.activeVariant.subjects)||void 0===e?void 0:e.includes(t)))return void console.warn(`[CoreEngine] Subject ${t} not found in active variant ${this.activeVariant.id}`);const i=`${this.activeVariant.id}_${t}`;if(!this.trackingDataCache.has(i))try{const e=`${this.basePath?`${this.basePath}/`:""}${this.activeVariant.path}/000_tracking-${t}.json`;console.log(`[CoreEngine] Fetching tracking data: ${e}`);const s=yield fetch(e);if(!s.ok)throw new Error(s.statusText);const r=yield s.json();this.trackingDataCache.set(i,r)}catch(e){console.error(`[CoreEngine] Failed to load tracking data for ${t}`,e)}})}getTrackedCoords(t,e){if(!this.activeVariant)return{x:.5,y:.5};const i=`${this.activeVariant.id}_${t}`,s=this.trackingDataCache.get(i);if(!s)return{x:.5,y:.5};const r=s.find(t=>t.frame===e);return r?{x:r.x,y:r.y,scale:r.scale}:{x:.5,y:.5}}render(){var t;if(!this.canvas||-1===this.currentFrame)return;const e=this.getImage(this.currentFrame);if(!e||!e.complete)return;const i=window.innerWidth,s=window.innerHeight;let r=null;if((null===(t=this.activeVariant)||void 0===t?void 0:t.hasDepthMap)&&(r=this.getDepthImage(this.currentFrame),r&&!r.complete&&(r=null)),this.renderer)this.renderer.render(e,r,i*(window.devicePixelRatio||1),s*(window.devicePixelRatio||1));else if(this.ctx){const t=e.naturalWidth/e.naturalHeight;let r,a,n,o;t>i/s?(a=s,r=s*t,n=(i-r)/2,o=0):(r=i,a=i/t,n=0,o=(s-a)/2),this.ctx.clearRect(0,0,i,s),this.ctx.drawImage(e,n,o,r,a)}}getImage(t){if(!this.activeVariant)return null;if(t<0||t>=this.activeVariant.frameCount)return null;const e=`${this.activeVariant.id}_${t}`;if(this.imageCache.has(e))return this.imageCache.get(e);const i=this.basePath?`${this.basePath}/`:"",s=new Image;return s.crossOrigin="anonymous",s.src=`${i}${this.activeVariant.path}/index_${t}.webp`,s.onload=()=>{this.currentFrame===t&&this.render()},this.imageCache.set(e,s),s}loadDepthMap(t){var e;if(!(null===(e=this.activeVariant)||void 0===e?void 0:e.hasDepthMap))return void console.log("[CoreEngine] activeVariant does not define hasDepthMap=true");console.log(`[CoreEngine] Lazy requesting depth map for frame: ${t}`);this.getDepthImage(t)}getDepthImage(t){var e;if(!(null===(e=this.activeVariant)||void 0===e?void 0:e.hasDepthMap))return null;if(t<0||t>=this.activeVariant.frameCount)return null;const i=`${this.activeVariant.id}_depth_${t}`;if(this.depthCache.has(i))return this.depthCache.get(i);const s=this.basePath?`${this.basePath}/`:"";console.log(`[CoreEngine] Downloading: ${s}${this.activeVariant.path}/index_${t}_depth.webp`);const r=new Image;return r.crossOrigin="anonymous",r.src=`${s}${this.activeVariant.path}/index_${t}_depth.webp`,r.onload=()=>{console.log(`[CoreEngine] Depth map loaded for frame: ${t}`),this.currentFrame===t&&this.render()},r.onerror=e=>{console.error(`[CoreEngine] Depth map failed to load for frame: ${t}`,e)},this.depthCache.set(i,r),r}}const a=r;return e=e.default})());
|