scrollcraft 2.0.5 → 2.0.7

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 CHANGED
@@ -1,53 +1,69 @@
1
- # 🎞️ ScrollCraft 2.0
1
+ # ScrollCraft
2
2
 
3
- **Transform cinematic motion into interactive web experiences.**
3
+ **Transform media into interactive web experiences.**
4
4
 
5
- ScrollCraft 2.0 is a modern animation SDK built for the era of high-performance, agent-driven development. It allows you to transform standard video or image sequences into "Intelligent Assets" that precisely track subjects and depth.
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
- ## 🚀 Quick Start
9
+ ## Installation
10
10
 
11
11
  ```bash
12
- # 1. Transform your video into an intelligent asset sequence
13
- npx scft create "examples/sample-media/jabko.mp4" --cloud --depth --prompt "apple"
12
+ npm install scrollcraft
13
+ ```
14
+
15
+ ---
14
16
 
15
- # this command will output a folder named scrollcraft-project
17
+ ## Quick Start
18
+
19
+ Initialize your project:
20
+
21
+ ```bash
22
+ # 1. Transform your video into a ScrollCraft project
23
+ # This will extract frames, track the subject, and generate optimized variants and depth maps using cloud AI processing.
24
+ npx scft create "your-video.mp4" --name "my-project" --track "apple" --cloud --depth
16
25
  ```
17
26
 
18
27
  ```tsx
19
- // 2. Drop it into your React app
20
- import project from './scrollcraft-project/scrollcraft.json';
28
+ // 2. Drop it into your Next.js app
29
+ import project from './my-project/scrollcraft.json';
21
30
  import { ScrollCraftProvider, ScrollCraftCanvas, SubjectLayer } from 'scrollcraft';
22
31
 
23
32
  const App = () => (
24
- <ScrollCraftProvider project={project}>
25
- <ScrollCraftCanvas />
26
- <SubjectLayer offset={{ x: 10, y: -5 }}>
27
- <h2>Pin UI to moving objects.</h2>
28
- </SubjectLayer>
33
+ <ScrollCraftProvider
34
+ project={project}
35
+ scrub={0.1} // Smooth interpolation (0 = instant, 1 = heavy lag)
36
+ >
37
+ <div style={{ height: '400vh' }}>
38
+ <ScrollCraftCanvas />
39
+
40
+ {/* Automatically follows the 'apple' tracked in the CLI */}
41
+ <SubjectLayer id="main">
42
+ <h2>Pinned Element</h2>
43
+ </SubjectLayer>
44
+ </div>
29
45
  </ScrollCraftProvider>
30
46
  );
31
47
  ```
32
48
 
33
49
  ---
34
50
 
35
- ## 📖 Documentation & Guides
51
+ ## Documentation & Guides
36
52
 
37
53
  Choose your path based on your role:
38
54
 
39
55
  ### 👤 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.
56
+ - [**Core Architecture**](https://github.com/aleskozelsky/scrollcraft/blob/main/packages/docs/app/docs/architecture/page.md): Understand the state-snapshot engine.
57
+ - [**Asset Pipeline**](https://github.com/aleskozelsky/scrollcraft/blob/main/packages/docs/app/docs/asset-pipeline/page.md): Learn how to use the CLI and AI tracking.
58
+ - [**React Hooks**](https://github.com/aleskozelsky/scrollcraft/blob/main/packages/docs/app/docs/react-integration/page.md): Build custom interactive components.
43
59
 
44
60
  ### 🤖 For AI Agents
45
61
  - [**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.
62
+ - [**AI Integration Protocol**](https://github.com/aleskozelsky/scrollcraft/blob/main/packages/docs/app/docs/ai-integration/page.md): How to prompt agents to build scenes for you.
47
63
 
48
64
  ---
49
65
 
50
- ## 🛠️ Performance & Tech
66
+ ## Performance & Tech
51
67
  - **WebGL Accelerated**: High-FPS rendering even for 4K sequences.
52
68
  - **AI Subject Tracking**: Automatic (x,y) pinning via SAM 3.
53
69
  - **Mouse-Interactive Parallax**: Automatic 3D depth map generation and rendering.
@@ -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`);
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- export {};
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 fal_service_1 = require("./fal-service");
47
- const processor_1 = require("./processor");
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('<input>', 'Path to input video or directory of images')
75
- .option('-o, --output <dir>', 'Output directory', './scrollcraft-project')
76
- .option('-p, --prompt <text>', 'Text prompt for subject tracking', 'main subject')
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 (input, opts) => {
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,76 @@ 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 outDir = path.resolve(opts.output);
91
- const tempDir = path.join(outDir, '.temp-frames');
92
- const step = parseInt(opts.step) || 1;
93
- try {
94
- await fs.ensureDir(outDir);
95
- await fs.ensureDir(tempDir);
96
- // 1. FRAME EXTRACTION
97
- if (fs.statSync(input).isFile()) {
98
- console.log(chalk_1.default.yellow(`📦 Extracting frames from video: ${input}`));
99
- // Extract 30 frames per second (matching our default)
100
- // Using the robust path discovered in pre-flight
101
- (0, child_process_1.execSync)(`"${ffmpegPath}" -i "${input}" -vf "fps=30" "${tempDir}/frame_%04d.png"`, { stdio: 'inherit' });
102
- }
103
- else {
104
- console.log(chalk_1.default.yellow(`📂 Using images from: ${input}`));
105
- if (opts.cloud || opts.depth) {
106
- console.error(chalk_1.default.red('\n❌ AI Cloud features (tracking/depth) currently require a video file as input.'));
107
- console.log(chalk_1.default.yellow('To use a directory of images, please use local mode (disable --cloud and --depth).'));
108
- process.exit(1);
109
- }
110
- const files = (await fs.readdir(input))
111
- .filter(f => /\.(png|jpg|jpeg|webp)$/i.test(f))
112
- .sort((a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }));
113
- if (files.length === 0) {
114
- throw new Error(`No compatible images (png, jpg, webp) found in ${input}`);
115
- }
116
- console.log(chalk_1.default.dim(`📦 Standardizing ${files.length} images...`));
117
- for (let i = 0; i < files.length; i++) {
118
- const ext = path.extname(files[i]);
119
- const frameName = `frame_${(i + 1).toString().padStart(4, '0')}${ext}`;
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
- // 3. VARIANT GENERATION (Mobile/Desktop)
150
- const processor = new processor_1.AssetProcessor(outDir);
151
- const variants = await processor.processVariants(tempDir, trackingData, { step, hasDepth });
152
- // 4. CLEANUP & SAVE
153
- await processor.saveConfig(variants);
154
- await fs.remove(tempDir);
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: ${outDir}`));
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
+ // In a real implementation, we might offer to upgrade or handle incompatibilities
236
+ }
237
+ console.log(chalk_1.default.dim('Skeletal update implemented. Continuing in next iteration...'));
164
238
  });
165
239
  program.parse(process.argv);
@@ -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
  /**
@@ -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
- // We grayscale and save as webp
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.id === 'mobile' ? '9:16' : '16:9',
128
+ aspectRatio: config.aspectRatio,
121
129
  frameCount: framesToProcess.length,
122
130
  hasDepthMap: options.hasDepth,
123
- subjectTracking: variantTracking
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: "2.0.1",
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 2.0 CORE ENGINE
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
- constructor(config: ProjectConfiguration);
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
- * ADAPTIVE RENDERING
27
- * Selects the best image folder based on current browser media queries.
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
- * Maps global scroll progress (0-1) to local scene frames.
48
+ * Sets the target scroll progress. Actual rendering interpolates to this value.
35
49
  */
36
- update(progress: number): {
37
- frame: number;
38
- subjectCoords: {
39
- x: number;
40
- y: number;
41
- };
42
- } | undefined;
43
- private getSubjectCoords;
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.
@@ -1 +1,4 @@
1
1
  export * from './types';
2
+ export { CoreEngine } from './CoreEngine';
3
+ import { CoreEngine } from './CoreEngine';
4
+ export default CoreEngine;
@@ -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})());
@@ -1,5 +1,5 @@
1
1
  /**
2
- * SCROLLCRAFT 2.0 - DECLARATIVE SCHEMA
2
+ * SCROLLCRAFT - DECLARATIVE SCHEMA
3
3
  *
4
4
  * This file defines the core data structures that allow an AI Agent
5
5
  * to describe a scroll experience in one step.
@@ -11,12 +11,12 @@ export interface ProjectConfiguration {
11
11
  timeline: TimelineDefinition;
12
12
  }
13
13
  export interface ProjectSettings {
14
- fps: number;
15
14
  baseResolution: {
16
15
  width: number;
17
16
  height: number;
18
17
  };
19
18
  scrollMode: 'vh' | 'px';
19
+ basePath?: string;
20
20
  }
21
21
  /**
22
22
  * ASSET SYSTEM
@@ -32,8 +32,11 @@ export interface AssetVariant {
32
32
  path: string;
33
33
  aspectRatio: string;
34
34
  frameCount: number;
35
+ width: number;
36
+ height: number;
37
+ orientation: 'portrait' | 'landscape';
35
38
  hasDepthMap?: boolean;
36
- subjectTracking?: SubjectFrameData[];
39
+ subjects?: string[];
37
40
  }
38
41
  export interface SubjectFrameData {
39
42
  frame: number;
@@ -97,76 +100,3 @@ export interface LayerAnimation {
97
100
  end: number;
98
101
  easing?: string;
99
102
  }
100
- /**
101
- * LEGACY TYPES (V1 Compatibility)
102
- * These are required for existing modules to compile.
103
- */
104
- export interface BlockInstanceInterface {
105
- destroy(): void;
106
- resize(params: {
107
- wiWidth: number;
108
- wiHeight: number;
109
- }): void;
110
- }
111
- export type MediaGroupPositionAndSize = {
112
- bgSize?: 'contain' | 'cover' | 'custom';
113
- bgPosition?: {
114
- x: number;
115
- y: number;
116
- };
117
- };
118
- export type ImagesUrlListArray = Array<string | {
119
- i: string;
120
- dur?: number;
121
- }>;
122
- export type ImageGroupUrlList = MediaGroupPositionAndSize & {
123
- type: 'urlList';
124
- prefix?: string;
125
- suffix?: string;
126
- images: ImagesUrlListArray;
127
- duration?: number;
128
- };
129
- export type ImageGroupGap = {
130
- type: 'gap';
131
- duration: number;
132
- };
133
- export type VideoGroupUrl = MediaGroupPositionAndSize & {
134
- type: 'urlVideo';
135
- video?: string;
136
- duration?: number;
137
- };
138
- export type ImageGroupWp = MediaGroupPositionAndSize & {
139
- type: 'wpMedia';
140
- images: number[];
141
- duration?: number;
142
- };
143
- export type VideoGroupWp = MediaGroupPositionAndSize & {
144
- type: 'wpVideo';
145
- video?: number;
146
- duration?: number;
147
- };
148
- export type MediaGroup = ImageGroupGap | ImageGroupUrlList | VideoGroupUrl | ImageGroupWp | VideoGroupWp;
149
- export type MediaGroups = MediaGroup[];
150
- export type AttributesCanvas2d = {
151
- mediaGroups: MediaGroups;
152
- markers: boolean;
153
- scrub: number;
154
- triggerStart: number;
155
- triggerEnd: number;
156
- pin: boolean;
157
- pinSpacing: boolean;
158
- };
159
- export type AttributesScene = {
160
- scenePosition: 'flow' | 'sticky' | 'fixed';
161
- stickyType: 'css' | 'js';
162
- height: number;
163
- heightUnit: string;
164
- duration: number;
165
- durationUnit: string;
166
- top: number;
167
- topUnit: string;
168
- markers: boolean;
169
- scrub: number;
170
- triggerStart: number;
171
- triggerEnd: number;
172
- };