@cntrl-site/sdk-nextjs 0.25.0 → 0.26.1

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.
Files changed (32) hide show
  1. package/cntrl-site-sdk-nextjs-0.25.1.tgz +0 -0
  2. package/lib/components/Article.js +4 -4
  3. package/lib/components/ArticleWrapper.js +2 -2
  4. package/lib/components/Item.js +8 -7
  5. package/lib/components/LayoutStyle.js +2 -2
  6. package/lib/components/LinkWrapper.js +1 -1
  7. package/lib/components/Page.js +1 -1
  8. package/lib/components/ScrollPlaybackVideo.js +46 -0
  9. package/lib/components/Section.js +4 -4
  10. package/lib/components/items/CustomItem.js +2 -2
  11. package/lib/components/items/GroupItem.js +3 -3
  12. package/lib/components/items/ImageItem.js +3 -3
  13. package/lib/components/items/RectangleItem.js +3 -3
  14. package/lib/components/items/RichTextItem.js +3 -3
  15. package/lib/components/items/VideoItem.js +16 -44
  16. package/lib/components/items/VimeoEmbed.js +4 -4
  17. package/lib/components/items/YoutubeEmbed.js +3 -3
  18. package/lib/components/useItemPosition.js +5 -1
  19. package/lib/provider/CntrlProvider.js +1 -1
  20. package/lib/utils/PlaybackVideoConverter/ScrollPlaybackVideoManager.js +221 -0
  21. package/lib/utils/PlaybackVideoConverter/VideoDecoder.js +175 -0
  22. package/lib/utils/RichTextConverter/RichTextConverter.js +4 -4
  23. package/lib/utils/Youtube/YoutubeIframeApi.js +1 -1
  24. package/package.json +6 -3
  25. package/src/components/Item.tsx +6 -4
  26. package/src/components/ScrollPlaybackVideo.tsx +56 -0
  27. package/src/components/items/VideoItem.tsx +37 -55
  28. package/src/components/useItemPosition.ts +6 -2
  29. package/src/utils/PlaybackVideoConverter/ScrollPlaybackVideoManager.ts +211 -0
  30. package/src/utils/PlaybackVideoConverter/VideoDecoder.ts +174 -0
  31. package/.idea/codeStyles/codeStyleConfig.xml +0 -5
  32. package/.idea/inspectionProfiles/Project_Default.xml +0 -15
@@ -0,0 +1,221 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ScrollPlaybackVideoManager = void 0;
7
+ const ua_parser_js_1 = __importDefault(require("ua-parser-js"));
8
+ const VideoDecoder_1 = __importDefault(require("./VideoDecoder"));
9
+ class ScrollPlaybackVideoManager {
10
+ constructor(options) {
11
+ this.currentTime = 0;
12
+ this.targetTime = 0;
13
+ this.canvas = null;
14
+ this.context = null;
15
+ this.frames = [];
16
+ this.frameRate = 0;
17
+ this.transitioning = false;
18
+ this.debug = false;
19
+ this.frameThreshold = 0;
20
+ this.transitionSpeed = 10;
21
+ this.useWebCodecs = true;
22
+ this.resize = () => {
23
+ if (this.debug)
24
+ console.info('ScrollVideo resizing...');
25
+ if (this.canvas) {
26
+ this.setCoverStyle(this.canvas);
27
+ }
28
+ else if (this.video) {
29
+ this.setCoverStyle(this.video);
30
+ }
31
+ this.paintCanvasFrame(Math.floor(this.currentTime * this.frameRate));
32
+ };
33
+ this.decodeVideo = () => {
34
+ if (!this.video)
35
+ return;
36
+ if (this.useWebCodecs && this.video.src) {
37
+ (0, VideoDecoder_1.default)(this.video.src, (frame) => {
38
+ this.frames.push(frame);
39
+ }, this.debug).then(() => {
40
+ if (!this.video || !this.container)
41
+ return;
42
+ if (this.frames.length === 0) {
43
+ if (this.debug)
44
+ console.error('No frames were received from webCodecs');
45
+ return;
46
+ }
47
+ this.frameRate = this.frames.length / this.video.duration;
48
+ if (this.debug)
49
+ console.info('Received', this.frames.length, 'frames');
50
+ this.canvas = document.createElement('canvas');
51
+ this.context = this.canvas.getContext('2d');
52
+ this.video.style.display = 'none';
53
+ this.container.appendChild(this.canvas);
54
+ this.paintCanvasFrame(Math.floor(this.currentTime * this.frameRate));
55
+ }).catch(() => {
56
+ if (this.debug)
57
+ console.error('Error encountered while decoding video');
58
+ this.frames = [];
59
+ this.video.load();
60
+ });
61
+ }
62
+ };
63
+ this.resizeObserver = new ResizeObserver(() => {
64
+ this.resize();
65
+ });
66
+ const { src, videoContainer } = options;
67
+ if (typeof document !== 'object') {
68
+ console.error('ScrollVideo must be initiated in a DOM context');
69
+ return;
70
+ }
71
+ if (!videoContainer) {
72
+ console.error('scrollVideoContainer must be a valid DOM object');
73
+ return;
74
+ }
75
+ if (!src) {
76
+ console.error('Must provide valid video src to ScrollVideo');
77
+ return;
78
+ }
79
+ this.container = typeof videoContainer === 'string' ? document.getElementById(videoContainer) : videoContainer;
80
+ this.resizeObserver.observe(this.container);
81
+ this.video = document.createElement('video');
82
+ this.video.src = src;
83
+ this.video.preload = 'auto';
84
+ this.video.tabIndex = 0;
85
+ this.video.playsInline = true;
86
+ this.video.muted = true;
87
+ this.video.pause();
88
+ this.video.load();
89
+ this.container.appendChild(this.video);
90
+ const browserEngine = new ua_parser_js_1.default().getEngine();
91
+ this.isSafari = browserEngine.name === 'WebKit';
92
+ if (this.debug && this.isSafari)
93
+ console.info('Safari browser detected');
94
+ this.video.addEventListener('loadedmetadata', () => this.setTargetTimePercent(0, true), { once: true });
95
+ this.video.addEventListener('progress', this.resize);
96
+ this.decodeVideo();
97
+ }
98
+ setCoverStyle(el) {
99
+ if (el && this.container) {
100
+ el.style.position = 'absolute';
101
+ el.style.top = '50%';
102
+ el.style.left = '50%';
103
+ el.style.transform = 'translate(-50%, -50%)';
104
+ const { width: containerWidth, height: containerHeight } = this.container.getBoundingClientRect();
105
+ const width = el.videoWidth || el.width;
106
+ const height = el.videoHeight || el.height;
107
+ if (containerWidth / containerHeight > width / height) {
108
+ el.style.width = '100%';
109
+ el.style.height = 'auto';
110
+ }
111
+ else {
112
+ el.style.height = '100%';
113
+ el.style.width = 'auto';
114
+ }
115
+ }
116
+ }
117
+ paintCanvasFrame(frameNum) {
118
+ if (this.canvas) {
119
+ const frameIdx = Math.min(frameNum, this.frames.length - 1);
120
+ const currFrame = this.frames[frameIdx];
121
+ if (currFrame && this.container) {
122
+ if (this.debug)
123
+ console.info('Painting frame', frameIdx);
124
+ this.canvas.width = currFrame.width;
125
+ this.canvas.height = currFrame.height;
126
+ const { width, height } = this.container.getBoundingClientRect();
127
+ this.resetCanvasDimensions(width, height, currFrame.width, currFrame.height);
128
+ this.context.drawImage(currFrame, 0, 0, currFrame.width, currFrame.height);
129
+ }
130
+ }
131
+ }
132
+ transitionToTargetTime(jump) {
133
+ if (!this.video)
134
+ return;
135
+ if (this.debug)
136
+ console.info('Transitioning targetTime:', this.targetTime, 'currentTime:', this.currentTime);
137
+ if (isNaN(this.targetTime) || Math.abs(this.currentTime - this.targetTime) < this.frameThreshold) {
138
+ this.video.pause();
139
+ this.transitioning = false;
140
+ return;
141
+ }
142
+ // Make sure we don't go out of time bounds
143
+ if (this.targetTime > this.video.duration) {
144
+ this.targetTime = this.video.duration;
145
+ }
146
+ if (this.targetTime < 0) {
147
+ this.targetTime = 0;
148
+ }
149
+ // How far forward we need to transition
150
+ const transitionForward = this.targetTime - this.currentTime;
151
+ if (this.canvas) {
152
+ // Update currentTime and paint the closest frame
153
+ this.currentTime += transitionForward / (256 / this.transitionSpeed);
154
+ // If jump, we go directly to the frame
155
+ if (jump) {
156
+ this.currentTime = this.targetTime;
157
+ }
158
+ this.paintCanvasFrame(Math.floor(this.currentTime * this.frameRate));
159
+ }
160
+ else if (jump || this.isSafari || this.targetTime - this.currentTime < 0) {
161
+ this.video.pause();
162
+ this.currentTime += transitionForward / (64 / this.transitionSpeed);
163
+ // If jump, we go directly to the frame
164
+ if (jump) {
165
+ this.currentTime = this.targetTime;
166
+ }
167
+ this.video.currentTime = this.currentTime;
168
+ }
169
+ else {
170
+ // Otherwise, we play the video and adjust the playbackRate to get a smoother
171
+ // animation effect.
172
+ const playbackRate = Math.max(Math.min(transitionForward * 4, this.transitionSpeed, 16), 1);
173
+ if (this.debug)
174
+ console.info('ScrollVideo playbackRate:', playbackRate);
175
+ if (!isNaN(playbackRate)) {
176
+ this.video.playbackRate = playbackRate;
177
+ this.video.play();
178
+ }
179
+ this.currentTime = this.video.currentTime;
180
+ }
181
+ if (typeof requestAnimationFrame === 'function') {
182
+ requestAnimationFrame(() => this.transitionToTargetTime(jump));
183
+ }
184
+ }
185
+ resetCanvasDimensions(w, h, frameW, frameH) {
186
+ if (!this.canvas)
187
+ return;
188
+ if (w / h > frameW / frameH) {
189
+ this.canvas.style.width = '100%';
190
+ this.canvas.style.height = 'auto';
191
+ }
192
+ else {
193
+ this.canvas.style.height = '100%';
194
+ this.canvas.style.width = 'auto';
195
+ }
196
+ }
197
+ setTargetTimePercent(setPercentage, jump = true) {
198
+ if (!this.video)
199
+ return;
200
+ this.targetTime = Math.max(Math.min(setPercentage, 1), 0)
201
+ * (this.frames.length && this.frameRate ? this.frames.length / this.frameRate : this.video.duration);
202
+ if (!jump && Math.abs(this.currentTime - this.targetTime) < this.frameThreshold)
203
+ return;
204
+ if (!jump && this.transitioning)
205
+ return;
206
+ if (!this.canvas && !this.video.paused)
207
+ this.video.play();
208
+ this.transitioning = true;
209
+ this.transitionToTargetTime(jump);
210
+ }
211
+ destroy() {
212
+ var _a;
213
+ this.resizeObserver.unobserve(this.container);
214
+ (_a = this.video) === null || _a === void 0 ? void 0 : _a.removeEventListener('progress', this.resize);
215
+ if (this.debug)
216
+ console.info('Destroying ScrollVideo');
217
+ if (this.container)
218
+ this.container.innerHTML = '';
219
+ }
220
+ }
221
+ exports.ScrollPlaybackVideoManager = ScrollPlaybackVideoManager;
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.Writer = void 0;
27
+ // @ts-ignore
28
+ const MP4Box = __importStar(require("mp4box"));
29
+ class Writer {
30
+ constructor(size) {
31
+ this.data = new Uint8Array(size);
32
+ this.idx = 0;
33
+ this.size = size;
34
+ }
35
+ getData() {
36
+ if (this.idx !== this.size)
37
+ throw new Error('Mismatch between size reserved and sized used');
38
+ return this.data.slice(0, this.idx);
39
+ }
40
+ writeUint8(value) {
41
+ this.data.set([value], this.idx);
42
+ this.idx += 1;
43
+ }
44
+ writeUint16(value) {
45
+ const arr = new Uint16Array(1);
46
+ arr[0] = value;
47
+ const buffer = new Uint8Array(arr.buffer);
48
+ this.data.set([buffer[1], buffer[0]], this.idx);
49
+ this.idx += 2;
50
+ }
51
+ writeUint8Array(value) {
52
+ this.data.set(value, this.idx);
53
+ this.idx += value.length;
54
+ }
55
+ }
56
+ exports.Writer = Writer;
57
+ const getExtradata = (avccBox) => {
58
+ let i;
59
+ let size = 7;
60
+ for (i = 0; i < avccBox.SPS.length; i += 1) {
61
+ // nalu length is encoded as a uint16.
62
+ size += 2 + avccBox.SPS[i].length;
63
+ }
64
+ for (i = 0; i < avccBox.PPS.length; i += 1) {
65
+ // nalu length is encoded as a uint16.
66
+ size += 2 + avccBox.PPS[i].length;
67
+ }
68
+ const writer = new Writer(size);
69
+ writer.writeUint8(avccBox.configurationVersion);
70
+ writer.writeUint8(avccBox.AVCProfileIndication);
71
+ writer.writeUint8(avccBox.profile_compatibility);
72
+ writer.writeUint8(avccBox.AVCLevelIndication);
73
+ writer.writeUint8(avccBox.lengthSizeMinusOne + (63 << 2));
74
+ writer.writeUint8(avccBox.nb_SPS_nalus + (7 << 5));
75
+ for (i = 0; i < avccBox.SPS.length; i += 1) {
76
+ writer.writeUint16(avccBox.SPS[i].length);
77
+ writer.writeUint8Array(avccBox.SPS[i].nalu);
78
+ }
79
+ writer.writeUint8(avccBox.nb_PPS_nalus);
80
+ for (i = 0; i < avccBox.PPS.length; i += 1) {
81
+ writer.writeUint16(avccBox.PPS[i].length);
82
+ writer.writeUint8Array(avccBox.PPS[i].nalu);
83
+ }
84
+ return writer.getData();
85
+ };
86
+ const decodeVideo = (src, emitFrame, { VideoDecoder, EncodedVideoChunk, debug }) => new Promise((resolve, reject) => {
87
+ if (debug)
88
+ console.info('Decoding video from', src);
89
+ try {
90
+ const mp4boxfile = MP4Box.createFile();
91
+ let codec;
92
+ const decoder = new VideoDecoder({
93
+ output: (frame) => {
94
+ console.log(frame);
95
+ createImageBitmap(frame, { resizeQuality: 'low' }).then((bitmap) => {
96
+ emitFrame(bitmap);
97
+ frame.close();
98
+ if (decoder.decodeQueueSize <= 0) {
99
+ setTimeout(() => {
100
+ if (decoder.state !== 'closed') {
101
+ decoder.close();
102
+ resolve();
103
+ }
104
+ }, 500);
105
+ }
106
+ });
107
+ },
108
+ error: (e) => {
109
+ console.error(e);
110
+ reject(e);
111
+ },
112
+ });
113
+ mp4boxfile.onReady = (info) => {
114
+ if (info && info.videoTracks && info.videoTracks[0]) {
115
+ [{ codec }] = info.videoTracks;
116
+ if (debug)
117
+ console.info('Video with codec:', codec);
118
+ const avccBox = mp4boxfile.moov.traks[0].mdia.minf.stbl.stsd.entries[0].avcC;
119
+ const extradata = getExtradata(avccBox);
120
+ decoder.configure({ codec, description: extradata });
121
+ mp4boxfile.setExtractionOptions(info.videoTracks[0].id);
122
+ mp4boxfile.start();
123
+ }
124
+ else
125
+ reject(new Error('URL provided is not a valid mp4 video file.'));
126
+ };
127
+ mp4boxfile.onSamples = (track_id, ref, samples) => {
128
+ for (let i = 0; i < samples.length; i += 1) {
129
+ const sample = samples[i];
130
+ const type = sample.is_sync ? 'key' : 'delta';
131
+ const chunk = new EncodedVideoChunk({
132
+ type,
133
+ timestamp: sample.cts,
134
+ duration: sample.duration,
135
+ data: sample.data,
136
+ });
137
+ decoder.decode(chunk);
138
+ }
139
+ };
140
+ fetch(src).then((res) => {
141
+ const reader = res.body.getReader();
142
+ let offset = 0;
143
+ //@ts-ignore
144
+ function appendBuffers({ done, value }) {
145
+ if (done) {
146
+ mp4boxfile.flush();
147
+ return null;
148
+ }
149
+ const buf = value.buffer;
150
+ buf.fileStart = offset;
151
+ offset += buf.byteLength;
152
+ mp4boxfile.appendBuffer(buf);
153
+ return reader.read().then(appendBuffers);
154
+ }
155
+ return reader.read().then(appendBuffers);
156
+ });
157
+ }
158
+ catch (e) {
159
+ reject(e);
160
+ }
161
+ });
162
+ exports.default = (src, emitFrame, debug) => {
163
+ if (typeof VideoDecoder === 'function' && typeof EncodedVideoChunk === 'function') {
164
+ if (debug)
165
+ console.info('WebCodecs is natively supported, using native version...');
166
+ return decodeVideo(src, emitFrame, {
167
+ VideoDecoder,
168
+ EncodedVideoChunk,
169
+ debug,
170
+ });
171
+ }
172
+ if (debug)
173
+ console.info('WebCodecs is not available in this browser.');
174
+ return Promise.resolve();
175
+ };
@@ -31,7 +31,7 @@ class RichTextConverter {
31
31
  const content = text.slice(block.start, block.end);
32
32
  const entities = (_a = block.entities.sort((a, b) => a.start - b.start)) !== null && _a !== void 0 ? _a : [];
33
33
  if (content.length === 0) {
34
- root.push((0, jsx_runtime_1.jsx)("div", Object.assign({ className: `rt_${richText.id}_br_${blockIndex}` }, { children: (0, jsx_runtime_1.jsx)("br", {}) })));
34
+ root.push((0, jsx_runtime_1.jsx)("div", { className: `rt_${richText.id}_br_${blockIndex}`, children: (0, jsx_runtime_1.jsx)("br", {}) }));
35
35
  layouts.forEach(l => {
36
36
  const lhForLayout = currentLineHeight[l.id];
37
37
  if (lhForLayout === undefined)
@@ -82,7 +82,7 @@ class RichTextConverter {
82
82
  if (offset < style.start) {
83
83
  entityKids.push(sliceSymbols(content, offset, style.start));
84
84
  }
85
- entityKids.push((0, jsx_runtime_1.jsx)("span", Object.assign({ className: `s-${style.start}-${style.end}` }, { children: sliceSymbols(content, style.start, style.end) }), style.start));
85
+ entityKids.push((0, jsx_runtime_1.jsx)("span", { className: `s-${style.start}-${style.end}`, children: sliceSymbols(content, style.start, style.end) }, style.start));
86
86
  offset = style.end;
87
87
  }
88
88
  if (offset < entity.end) {
@@ -90,7 +90,7 @@ class RichTextConverter {
90
90
  offset = entity.end;
91
91
  }
92
92
  if (entity.link) {
93
- kids.push((0, jsx_runtime_1.jsx)(LinkWrapper_1.LinkWrapper, Object.assign({ url: entity.link, target: entity.target }, { children: entityKids }), entity.start));
93
+ kids.push((0, jsx_runtime_1.jsx)(LinkWrapper_1.LinkWrapper, { url: entity.link, target: entity.target, children: entityKids }, entity.start));
94
94
  continue;
95
95
  }
96
96
  kids.push(...entityKids);
@@ -127,7 +127,7 @@ class RichTextConverter {
127
127
  }
128
128
  }
129
129
  }
130
- root.push((0, jsx_runtime_1.jsx)("div", Object.assign({ className: blockClass }, { children: kids }), blockClass));
130
+ root.push((0, jsx_runtime_1.jsx)("div", { className: blockClass, children: kids }, blockClass));
131
131
  }
132
132
  }
133
133
  const styles = layouts.map(l => `
@@ -9,4 +9,4 @@ var YTPlayerState;
9
9
  YTPlayerState[YTPlayerState["Paused"] = 2] = "Paused";
10
10
  YTPlayerState[YTPlayerState["Buffering"] = 3] = "Buffering";
11
11
  YTPlayerState[YTPlayerState["VideoCued"] = 5] = "VideoCued";
12
- })(YTPlayerState = exports.YTPlayerState || (exports.YTPlayerState = {}));
12
+ })(YTPlayerState || (exports.YTPlayerState = YTPlayerState = {}));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cntrl-site/sdk-nextjs",
3
- "version": "0.25.0",
3
+ "version": "0.26.1",
4
4
  "description": "SDK for Next.js",
5
5
  "main": "lib/index.js",
6
6
  "types": "src/index.ts",
@@ -24,11 +24,14 @@
24
24
  "@cntrl-site/color": "^1.0.0",
25
25
  "@cntrl-site/effects": "^1.0.5",
26
26
  "@cntrl-site/sdk": "^1.8.0",
27
+ "@types/ua-parser-js": "^0.7.39",
27
28
  "@types/vimeo__player": "^2.18.0",
28
29
  "@vimeo/player": "^2.20.1",
29
30
  "html-react-parser": "^3.0.1",
31
+ "mp4box": "^0.5.2",
30
32
  "resize-observer-polyfill": "^1.5.1",
31
- "styled-jsx": "^5.0.2"
33
+ "styled-jsx": "^5.0.2",
34
+ "ua-parser-js": "^1.0.37"
32
35
  },
33
36
  "peerDependencies": {
34
37
  "@types/lodash.isequal": "^4.5.6",
@@ -45,7 +48,7 @@
45
48
  "@types/react": "^18.0.15",
46
49
  "jest": "^29.3.1",
47
50
  "ts-jest": "^29.0.3",
48
- "typescript": "^4.9.5"
51
+ "typescript": "^5.2.2"
49
52
  },
50
53
  "directories": {
51
54
  "lib": "lib"
@@ -1,6 +1,7 @@
1
1
  import { ComponentType, FC, PropsWithChildren, useContext, useEffect, useId, useRef, useState } from 'react';
2
2
  import JSXStyle from 'styled-jsx/style';
3
3
  import {
4
+ AnchorSide,
4
5
  ArticleItemType,
5
6
  getLayoutStyles,
6
7
  Item as TItem,
@@ -76,7 +77,7 @@ export const Item: FC<ItemWrapperProps> = ({ item, sectionId, articleHeight }) =
76
77
  const [wrapperHeight, setWrapperHeight] = useState<undefined | number>(undefined);
77
78
  const [itemHeight, setItemHeight] = useState<undefined | number>(undefined);
78
79
  const { scale, scaleAnchor } = useItemScale(item, sectionId);
79
- const { top, left } = useItemPosition(item, sectionId);
80
+ const { top, left, bottom } = useItemPosition(item, sectionId);
80
81
  const sectionHeight = useSectionHeightData(sectionId);
81
82
  const stickyTop = useStickyItemTop(item, sectionHeight, sectionId);
82
83
  const { width, height } = useItemDimensions(item, sectionId);
@@ -128,7 +129,7 @@ export const Item: FC<ItemWrapperProps> = ({ item, sectionId, articleHeight }) =
128
129
  <div
129
130
  className={`item-wrapper-${item.id}`}
130
131
  ref={itemWrapperRef}
131
- style={isInitialRef.current ? {} : { top, left, ...(wrapperHeight !== undefined ? { height: `${wrapperHeight * 100}vw` } : {}) }}
132
+ style={isInitialRef.current ? {} : { top, left, bottom, ...(wrapperHeight !== undefined ? { height: `${wrapperHeight * 100}vw` } : {}) }}
132
133
  >
133
134
  <div
134
135
  suppressHydrationWarning={true}
@@ -156,12 +157,12 @@ export const Item: FC<ItemWrapperProps> = ({ item, sectionId, articleHeight }) =
156
157
  <JSXStyle id={id}>{`
157
158
  ${getLayoutStyles(layouts, layoutValues, ([area, hidden, hoverParams, sticky, sectionHeight, layoutParams]) => {
158
159
  const sizingAxis = parseSizing(layoutParams.sizing);
160
+ const isScreenBasedBottom = area.positionType === PositionType.ScreenBased && area.anchorSide === AnchorSide.Bottom;
159
161
  return (`
160
162
  .item-${item.id} {
161
163
  position: ${sticky ? 'sticky' : 'absolute'};
162
164
  top: ${sticky ? `${getAnchoredItemTop(area.top - sticky.from, sectionHeight, area.anchorSide)}` : 0};
163
165
  pointer-events: auto;
164
- cursor: ${hoverParams ? 'pointer' : 'default'};
165
166
  display: ${hidden ? 'none' : 'block'};
166
167
  height: fit-content;
167
168
  }
@@ -179,7 +180,8 @@ export const Item: FC<ItemWrapperProps> = ({ item, sectionId, articleHeight }) =
179
180
  -webkit-transform: translate3d(0, 0, 0);
180
181
  transform: translate3d(0, 0, 0);
181
182
  pointer-events: none;
182
- top: ${getItemTopStyle(area.top, area.anchorSide)};
183
+ bottom: ${isScreenBasedBottom ? `${-area.top * 100}vw` : 'unset'};
184
+ top: ${isScreenBasedBottom ? 'unset' : getItemTopStyle(area.top, area.anchorSide)};
183
185
  left: ${area.left * 100}vw;
184
186
  transition: ${getTransitions(['left', 'top'], hoverParams)};
185
187
  }
@@ -0,0 +1,56 @@
1
+ import React, { FC, useContext, useEffect, useMemo, useState } from 'react';
2
+ import { ScrollPlaybackVideoManager } from '../utils/PlaybackVideoConverter/ScrollPlaybackVideoManager';
3
+ import { rangeMap } from '../utils/rangeMap';
4
+ import { ArticleRectContext } from '../provider/ArticleRectContext';
5
+
6
+ type PlaybackParams = { from: number, to: number };
7
+
8
+ interface Props {
9
+ sectionId: string;
10
+ src: string;
11
+ playbackParams: PlaybackParams | null;
12
+ style?: React.CSSProperties;
13
+ className: string;
14
+ }
15
+
16
+ export const ScrollPlaybackVideo: FC<Props> = ({ sectionId, src, playbackParams, style, className}) => {
17
+ const [containerElement, setContainerElement] = useState<HTMLDivElement | null>(null);
18
+ const [time, setTime] = useState(0);
19
+ const articleRectObserver = useContext(ArticleRectContext);
20
+
21
+ useEffect(() => {
22
+ if (!playbackParams || !articleRectObserver) return;
23
+ return articleRectObserver.on('scroll', () => {
24
+ const scrollPos = articleRectObserver.getSectionScroll(sectionId);
25
+ const time = rangeMap(scrollPos, playbackParams.from, playbackParams.to, 0, 1, true);
26
+ setTime(toFixed(time));
27
+ })
28
+ }, [playbackParams?.from, playbackParams?.to, time]);
29
+
30
+ const scrollVideoManager = useMemo<ScrollPlaybackVideoManager | null>(() => {
31
+ if (!containerElement) return null;
32
+ const manager = new ScrollPlaybackVideoManager({
33
+ src,
34
+ videoContainer: containerElement
35
+ });
36
+ return manager;
37
+ }, [containerElement, src]);
38
+
39
+ useEffect(() => {
40
+ return () => {
41
+ scrollVideoManager?.destroy();
42
+ };
43
+ }, [scrollVideoManager]);
44
+
45
+ useEffect(() => {
46
+ if (scrollVideoManager && time >= 0 && time <= 1) {
47
+ scrollVideoManager.setTargetTimePercent(time);
48
+ }
49
+ }, [time, scrollVideoManager]);
50
+
51
+ return <div className={className} style={style} ref={setContainerElement} />;
52
+ };
53
+
54
+ function toFixed(num: number) {
55
+ return Number(num.toFixed(3));
56
+ }