@cntrl-site/sdk-nextjs 1.2.3-alpha.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/eslint.config.mjs +16 -0
- package/lib/components/items/FileItem/ImageItem.js +5 -10
- package/lib/components/items/FileItem/VideoItem.js +52 -10
- package/lib/components/items/FileItem/shaderBaseVars.js +11 -0
- package/lib/utils/effects/useImageFx.js +11 -6
- package/lib/utils/effects/useVideoFx.js +90 -0
- package/package.json +3 -3
- package/src/components/items/FileItem/ImageItem.tsx +22 -28
- package/src/components/items/FileItem/VideoItem.tsx +90 -27
- package/src/components/items/FileItem/shaderBaseVars.ts +8 -0
- package/src/utils/effects/useImageFx.ts +12 -9
- package/src/utils/effects/useVideoFx.ts +123 -0
- package/cntrl-site-sdk-nextjs-1.2.3.tgz +0 -0
package/eslint.config.mjs
CHANGED
|
@@ -13,6 +13,22 @@ export default antfu(
|
|
|
13
13
|
'style/member-delimiter-style': 'off',
|
|
14
14
|
'semi': ['error', 'always'],
|
|
15
15
|
'no-alert': 'off',
|
|
16
|
+
'ts/consistent-type-definitions': 'off',
|
|
17
|
+
'perfectionist/sort-imports': 'off',
|
|
18
|
+
'perfectionist/sort-named-imports': 'off',
|
|
19
|
+
'ts/consistent-type-imports': 'off',
|
|
20
|
+
'jsonc/sort-keys': 'off',
|
|
21
|
+
'node/prefer-global/process': 'off',
|
|
22
|
+
'style/arrow-parens': 'off',
|
|
23
|
+
'ts/method-signature-style': 'off',
|
|
24
|
+
'style/brace-style': 'off',
|
|
25
|
+
'style/jsx-one-expression-per-line': 'off',
|
|
26
|
+
'test/prefer-lowercase-title': 'off',
|
|
27
|
+
'perfectionist/sort-exports': 'off',
|
|
28
|
+
'no-extra-boolean-cast': 'off',
|
|
29
|
+
'prefer-promise-reject-errors': 'off',
|
|
30
|
+
'new-cap': 'off',
|
|
31
|
+
'perfectionist/sort-named-exports': 'off'
|
|
16
32
|
},
|
|
17
33
|
},
|
|
18
34
|
);
|
|
@@ -18,14 +18,7 @@ const useImageFx_1 = require("../../../utils/effects/useImageFx");
|
|
|
18
18
|
const useElementRect_1 = require("../../../utils/useElementRect");
|
|
19
19
|
const useLayoutContext_1 = require("../../useLayoutContext");
|
|
20
20
|
const getStyleFromItemStateAndParams_1 = require("../../../utils/getStyleFromItemStateAndParams");
|
|
21
|
-
const
|
|
22
|
-
uniform sampler2D u_image;
|
|
23
|
-
uniform sampler2D u_pattern;
|
|
24
|
-
uniform vec2 u_imgDimensions;
|
|
25
|
-
uniform vec2 u_patternDimensions;
|
|
26
|
-
uniform float u_time;
|
|
27
|
-
uniform vec2 u_cursor;
|
|
28
|
-
varying vec2 v_texCoord;`;
|
|
21
|
+
const shaderBaseVars_1 = require("./shaderBaseVars");
|
|
29
22
|
const ImageItem = ({ item, sectionId, onResize, interactionCtrl, onVisibilityChange }) => {
|
|
30
23
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
31
24
|
const id = (0, react_1.useId)();
|
|
@@ -46,7 +39,7 @@ const ImageItem = ({ item, sectionId, onResize, interactionCtrl, onVisibilityCha
|
|
|
46
39
|
return acc;
|
|
47
40
|
}, {});
|
|
48
41
|
const layoutValues = [item.area, item.layoutParams];
|
|
49
|
-
const fullShaderCode = `${baseVariables}\n${controlsVariables}\n${fragmentShader}`;
|
|
42
|
+
const fullShaderCode = `${shaderBaseVars_1.baseVariables}\n${controlsVariables}\n${fragmentShader}`;
|
|
50
43
|
const area = layoutId ? item.area[layoutId] : null;
|
|
51
44
|
const exemplary = (_a = layouts === null || layouts === void 0 ? void 0 : layouts.find(l => l.id === layoutId)) === null || _a === void 0 ? void 0 : _a.exemplary;
|
|
52
45
|
const width = area && exemplary ? area.width * exemplary : 0;
|
|
@@ -80,7 +73,9 @@ const ImageItem = ({ item, sectionId, onResize, interactionCtrl, onVisibilityCha
|
|
|
80
73
|
(0, react_1.useEffect)(() => {
|
|
81
74
|
onVisibilityChange === null || onVisibilityChange === void 0 ? void 0 : onVisibilityChange(isInteractive);
|
|
82
75
|
}, [isInteractive, onVisibilityChange]);
|
|
83
|
-
return ((0, jsx_runtime_1.jsx)(LinkWrapper_1.LinkWrapper, { url: (_l = item.link) === null || _l === void 0 ? void 0 : _l.url, target: (_m = item.link) === null || _m === void 0 ? void 0 : _m.target, children: (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { className: `image-wrapper-${item.id}`, ref: setWrapperRef, style: Object.assign(Object.assign(Object.assign(Object.assign({}, (opacity !== undefined ? { opacity } : {})), (angle !== undefined ? { transform: `rotate(${angle}deg)` } : {})), (blur !== undefined ? { filter: `blur(${blur * 100}vw)` } : {})), { transition: (_o = wrapperStateParams === null || wrapperStateParams === void 0 ? void 0 : wrapperStateParams.transition) !== null && _o !== void 0 ? _o : 'none' }), children: hasGLEffect && isFXAllowed
|
|
76
|
+
return ((0, jsx_runtime_1.jsx)(LinkWrapper_1.LinkWrapper, { url: (_l = item.link) === null || _l === void 0 ? void 0 : _l.url, target: (_m = item.link) === null || _m === void 0 ? void 0 : _m.target, children: (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { className: `image-wrapper-${item.id}`, ref: setWrapperRef, style: Object.assign(Object.assign(Object.assign(Object.assign({}, (opacity !== undefined ? { opacity } : {})), (angle !== undefined ? { transform: `rotate(${angle}deg)` } : {})), (blur !== undefined ? { filter: `blur(${blur * 100}vw)` } : {})), { transition: (_o = wrapperStateParams === null || wrapperStateParams === void 0 ? void 0 : wrapperStateParams.transition) !== null && _o !== void 0 ? _o : 'none' }), children: hasGLEffect && isFXAllowed
|
|
77
|
+
? ((0, jsx_runtime_1.jsx)("canvas", { style: inlineStyles, ref: fxCanvas, className: `img-canvas image-${item.id}`, width: rectWidth, height: rectHeight }))
|
|
78
|
+
: ((0, jsx_runtime_1.jsx)("img", { alt: "", className: `image image-${item.id}`, style: inlineStyles, src: item.commonParams.url })) }), (0, jsx_runtime_1.jsx)(style_1.default, { id: id, children: `
|
|
84
79
|
.image-wrapper-${item.id} {
|
|
85
80
|
position: absolute;
|
|
86
81
|
width: 100%;
|
|
@@ -17,15 +17,36 @@ const useRegisterResize_1 = require("../../../common/useRegisterResize");
|
|
|
17
17
|
const useLayoutContext_1 = require("../../useLayoutContext");
|
|
18
18
|
const ScrollPlaybackVideo_1 = require("../../ScrollPlaybackVideo");
|
|
19
19
|
const getStyleFromItemStateAndParams_1 = require("../../../utils/getStyleFromItemStateAndParams");
|
|
20
|
+
const shaderBaseVars_1 = require("./shaderBaseVars");
|
|
21
|
+
const useVideoFx_1 = require("../../../utils/effects/useVideoFx");
|
|
22
|
+
const useElementRect_1 = require("../../../utils/useElementRect");
|
|
20
23
|
const VideoItem = ({ item, sectionId, onResize, interactionCtrl, onVisibilityChange }) => {
|
|
21
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
24
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
|
|
22
25
|
const id = (0, react_1.useId)();
|
|
23
26
|
const { layouts } = (0, useCntrlContext_1.useCntrlContext)();
|
|
27
|
+
const layoutId = (0, useLayoutContext_1.useLayoutContext)();
|
|
24
28
|
const { radius: itemRadius, strokeWidth: itemStrokeWidth, strokeColor: itemStrokeColor, opacity: itemOpacity, blur: itemBlur } = (0, useFileItem_1.useFileItem)(item, sectionId);
|
|
25
29
|
const itemAngle = (0, useItemAngle_1.useItemAngle)(item, sectionId);
|
|
26
30
|
const [ref, setRef] = (0, react_1.useState)(null);
|
|
27
31
|
const videoRef = (0, react_1.useRef)(null);
|
|
28
|
-
const
|
|
32
|
+
const fxCanvas = (0, react_1.useRef)(null);
|
|
33
|
+
const { url, hasGLEffect, fragmentShader, FXControls, FXCursor } = item.commonParams;
|
|
34
|
+
const isInitialRef = (0, react_1.useRef)(true);
|
|
35
|
+
const area = layoutId ? item.area[layoutId] : null;
|
|
36
|
+
const exemplary = (_a = layouts === null || layouts === void 0 ? void 0 : layouts.find(l => l.id === layoutId)) === null || _a === void 0 ? void 0 : _a.exemplary;
|
|
37
|
+
const width = area && exemplary ? area.width * exemplary : 0;
|
|
38
|
+
const height = area && exemplary ? area.height * exemplary : 0;
|
|
39
|
+
const controls = FXControls !== null && FXControls !== void 0 ? FXControls : [];
|
|
40
|
+
const controlsVariables = controls.map((c) => `uniform ${c.type} ${c.shaderParam};`)
|
|
41
|
+
.join('\n');
|
|
42
|
+
const controlValues = controls.reduce((acc, control) => {
|
|
43
|
+
acc[control.shaderParam] = control.value;
|
|
44
|
+
return acc;
|
|
45
|
+
}, {});
|
|
46
|
+
const fullShaderCode = `${shaderBaseVars_1.baseVariables}\n${controlsVariables}\n${fragmentShader}`;
|
|
47
|
+
const rect = (0, useElementRect_1.useElementRect)(ref);
|
|
48
|
+
const rectWidth = Math.floor((_b = rect === null || rect === void 0 ? void 0 : rect.width) !== null && _b !== void 0 ? _b : 0);
|
|
49
|
+
const rectHeight = Math.floor((_c = rect === null || rect === void 0 ? void 0 : rect.height) !== null && _c !== void 0 ? _c : 0);
|
|
29
50
|
const scrollPlayback = layoutId ? item.layoutParams[layoutId].scrollPlayback : null;
|
|
30
51
|
const layoutValues = [item.area, item.layoutParams];
|
|
31
52
|
const hasScrollPlayback = scrollPlayback !== null;
|
|
@@ -35,19 +56,32 @@ const VideoItem = ({ item, sectionId, onResize, interactionCtrl, onVisibilityCha
|
|
|
35
56
|
var _a;
|
|
36
57
|
const strokeColor = (0, getStyleFromItemStateAndParams_1.getStyleFromItemStateAndParams)((_a = videoStateParams === null || videoStateParams === void 0 ? void 0 : videoStateParams.styles) === null || _a === void 0 ? void 0 : _a.color, itemStrokeColor);
|
|
37
58
|
return strokeColor ? color_1.CntrlColor.parse(strokeColor) : undefined;
|
|
38
|
-
}, [(
|
|
39
|
-
const angle = (0, getStyleFromItemStateAndParams_1.getStyleFromItemStateAndParams)((
|
|
40
|
-
const opacity = (0, getStyleFromItemStateAndParams_1.getStyleFromItemStateAndParams)((
|
|
41
|
-
const blur = (0, getStyleFromItemStateAndParams_1.getStyleFromItemStateAndParams)((
|
|
42
|
-
const strokeWidth = (0, getStyleFromItemStateAndParams_1.getStyleFromItemStateAndParams)((
|
|
43
|
-
const radius = (0, getStyleFromItemStateAndParams_1.getStyleFromItemStateAndParams)((
|
|
59
|
+
}, [(_d = videoStateParams === null || videoStateParams === void 0 ? void 0 : videoStateParams.styles) === null || _d === void 0 ? void 0 : _d.color, itemStrokeColor]);
|
|
60
|
+
const angle = (0, getStyleFromItemStateAndParams_1.getStyleFromItemStateAndParams)((_e = wrapperStateParams === null || wrapperStateParams === void 0 ? void 0 : wrapperStateParams.styles) === null || _e === void 0 ? void 0 : _e.angle, itemAngle);
|
|
61
|
+
const opacity = (0, getStyleFromItemStateAndParams_1.getStyleFromItemStateAndParams)((_f = wrapperStateParams === null || wrapperStateParams === void 0 ? void 0 : wrapperStateParams.styles) === null || _f === void 0 ? void 0 : _f.opacity, itemOpacity);
|
|
62
|
+
const blur = (0, getStyleFromItemStateAndParams_1.getStyleFromItemStateAndParams)((_g = wrapperStateParams === null || wrapperStateParams === void 0 ? void 0 : wrapperStateParams.styles) === null || _g === void 0 ? void 0 : _g.blur, itemBlur);
|
|
63
|
+
const strokeWidth = (0, getStyleFromItemStateAndParams_1.getStyleFromItemStateAndParams)((_h = wrapperStateParams === null || wrapperStateParams === void 0 ? void 0 : wrapperStateParams.styles) === null || _h === void 0 ? void 0 : _h.strokeWidth, itemStrokeWidth);
|
|
64
|
+
const radius = (0, getStyleFromItemStateAndParams_1.getStyleFromItemStateAndParams)((_j = wrapperStateParams === null || wrapperStateParams === void 0 ? void 0 : wrapperStateParams.styles) === null || _j === void 0 ? void 0 : _j.radius, itemRadius);
|
|
65
|
+
(0, react_1.useEffect)(() => {
|
|
66
|
+
isInitialRef.current = false;
|
|
67
|
+
}, []);
|
|
68
|
+
const isFXAllowed = (0, useVideoFx_1.useVideoFx)(fxCanvas.current, !!(hasGLEffect && !isInitialRef.current), {
|
|
69
|
+
videoUrl: url,
|
|
70
|
+
fragmentShader: fullShaderCode,
|
|
71
|
+
cursor: FXCursor,
|
|
72
|
+
controls: controlValues
|
|
73
|
+
}, width, height);
|
|
44
74
|
(0, useRegisterResize_1.useRegisterResize)(ref, onResize);
|
|
45
|
-
const inlineStyles = Object.assign(Object.assign(Object.assign(Object.assign({}, (radius !== undefined ? { borderRadius: `${radius * 100}vw` } : {})), (strokeWidth !== undefined ? { borderWidth: `${strokeWidth * 100}vw` } : {})), (borderColor ? { borderColor: `${borderColor.toCss()}` } : {})), { transition: (
|
|
75
|
+
const inlineStyles = Object.assign(Object.assign(Object.assign(Object.assign({}, (radius !== undefined ? { borderRadius: `${radius * 100}vw` } : {})), (strokeWidth !== undefined ? { borderWidth: `${strokeWidth * 100}vw` } : {})), (borderColor ? { borderColor: `${borderColor.toCss()}` } : {})), { transition: (_k = videoStateParams === null || videoStateParams === void 0 ? void 0 : videoStateParams.transition) !== null && _k !== void 0 ? _k : 'none' });
|
|
46
76
|
const isInteractive = opacity !== 0;
|
|
47
77
|
(0, react_1.useEffect)(() => {
|
|
48
78
|
onVisibilityChange === null || onVisibilityChange === void 0 ? void 0 : onVisibilityChange(isInteractive);
|
|
49
79
|
}, [isInteractive, onVisibilityChange]);
|
|
50
|
-
return ((0, jsx_runtime_1.jsxs)(LinkWrapper_1.LinkWrapper, { url: (
|
|
80
|
+
return ((0, jsx_runtime_1.jsxs)(LinkWrapper_1.LinkWrapper, { url: (_l = item.link) === null || _l === void 0 ? void 0 : _l.url, target: (_m = item.link) === null || _m === void 0 ? void 0 : _m.target, children: [(0, jsx_runtime_1.jsx)("div", { className: `video-wrapper-${item.id}`, ref: setRef, style: Object.assign(Object.assign(Object.assign(Object.assign({}, (opacity !== undefined ? { opacity } : {})), (angle !== undefined ? { transform: `rotate(${angle}deg)` } : {})), (blur !== undefined ? { filter: `blur(${blur * 100}vw)` } : {})), { transition: (_o = wrapperStateParams === null || wrapperStateParams === void 0 ? void 0 : wrapperStateParams.transition) !== null && _o !== void 0 ? _o : 'none' }), children: hasScrollPlayback
|
|
81
|
+
? ((0, jsx_runtime_1.jsx)(ScrollPlaybackVideo_1.ScrollPlaybackVideo, { sectionId: sectionId, src: item.commonParams.url, playbackParams: scrollPlayback, style: inlineStyles, className: `video video-playback-wrapper video-${item.id}` }))
|
|
82
|
+
: ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: hasGLEffect && isFXAllowed
|
|
83
|
+
? ((0, jsx_runtime_1.jsx)("canvas", { style: inlineStyles, ref: fxCanvas, className: `video-canvas video-${item.id}`, width: rectWidth, height: rectHeight }))
|
|
84
|
+
: ((0, jsx_runtime_1.jsx)("video", { poster: (_p = item.commonParams.coverUrl) !== null && _p !== void 0 ? _p : '', ref: videoRef, autoPlay: true, muted: true, loop: true, playsInline: true, className: `video video-${item.id}`, style: inlineStyles, children: (0, jsx_runtime_1.jsx)("source", { src: item.commonParams.url }) })) })) }), (0, jsx_runtime_1.jsx)(style_1.default, { id: id, children: `
|
|
51
85
|
.video-wrapper-${item.id} {
|
|
52
86
|
position: absolute;
|
|
53
87
|
overflow: hidden;
|
|
@@ -74,6 +108,14 @@ const VideoItem = ({ item, sectionId, onResize, interactionCtrl, onVisibilityCha
|
|
|
74
108
|
display: flex;
|
|
75
109
|
justify-content: center;
|
|
76
110
|
}
|
|
111
|
+
.video-canvas {
|
|
112
|
+
border: solid;
|
|
113
|
+
width: 100%;
|
|
114
|
+
height: 100%;
|
|
115
|
+
pointer-events: none;
|
|
116
|
+
border-width: 0;
|
|
117
|
+
box-sizing: border-box;
|
|
118
|
+
}
|
|
77
119
|
${(0, sdk_1.getLayoutStyles)(layouts, layoutValues, ([area, layoutParams]) => {
|
|
78
120
|
return (`
|
|
79
121
|
.video-wrapper-${item.id} {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.baseVariables = void 0;
|
|
4
|
+
exports.baseVariables = `precision mediump float;
|
|
5
|
+
uniform sampler2D u_image;
|
|
6
|
+
uniform sampler2D u_pattern;
|
|
7
|
+
uniform vec2 u_imgDimensions;
|
|
8
|
+
uniform vec2 u_patternDimensions;
|
|
9
|
+
uniform float u_time;
|
|
10
|
+
uniform vec2 u_cursor;
|
|
11
|
+
varying vec2 v_texCoord;`;
|
|
@@ -9,12 +9,17 @@ const PATTERN_2_URL = 'https://cdn.cntrl.site/client-app-files/bayer16.png';
|
|
|
9
9
|
function useImageFx(canvas, enabled, { imageUrl, fragmentShader, cursor, controls }, width, height) {
|
|
10
10
|
const [isFXAllowed, setIsFXAllowed] = (0, react_1.useState)(true);
|
|
11
11
|
const mousePos = (0, react_1.useRef)([0.0, 0.0]);
|
|
12
|
+
const imageTextureManager = (0, react_1.useMemo)(() => {
|
|
13
|
+
if (!imageUrl || !enabled)
|
|
14
|
+
return;
|
|
15
|
+
return new effects_1.ImageTextureManager(imageUrl);
|
|
16
|
+
}, [imageUrl, enabled]);
|
|
12
17
|
const imageFx = (0, react_1.useMemo)(() => {
|
|
13
|
-
if (!
|
|
14
|
-
return
|
|
18
|
+
if (!cursor || !imageTextureManager)
|
|
19
|
+
return;
|
|
15
20
|
const { type, x, y } = cursor;
|
|
16
|
-
return new effects_1.
|
|
17
|
-
}, [
|
|
21
|
+
return new effects_1.MediaEffect(imageTextureManager, PATTERN_URL, PATTERN_2_URL, fragmentShader, Object.assign({ time: 0, cursor: type === 'mouse' ? mousePos.current : [x, y] }, controls), width, height);
|
|
22
|
+
}, [imageTextureManager, fragmentShader, width, height]);
|
|
18
23
|
(0, react_1.useEffect)(() => {
|
|
19
24
|
if (!cursor || cursor.type !== 'mouse' || !canvas || !imageFx)
|
|
20
25
|
return;
|
|
@@ -45,7 +50,7 @@ function useImageFx(canvas, enabled, { imageUrl, fragmentShader, cursor, control
|
|
|
45
50
|
try {
|
|
46
51
|
imageFx.render(gl);
|
|
47
52
|
}
|
|
48
|
-
catch (
|
|
53
|
+
catch (_a) {
|
|
49
54
|
setIsFXAllowed(false);
|
|
50
55
|
}
|
|
51
56
|
frame = requestAnimationFrame(renderFrame);
|
|
@@ -65,7 +70,7 @@ function useImageFx(canvas, enabled, { imageUrl, fragmentShader, cursor, control
|
|
|
65
70
|
try {
|
|
66
71
|
imageFx.prepare(gl);
|
|
67
72
|
}
|
|
68
|
-
catch (
|
|
73
|
+
catch (_a) {
|
|
69
74
|
setIsFXAllowed(false);
|
|
70
75
|
}
|
|
71
76
|
return () => {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useVideoFx = void 0;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
const effects_1 = require("@cntrl-site/effects");
|
|
6
|
+
const rangeMap_1 = require("../rangeMap");
|
|
7
|
+
const PATTERN_URL = 'https://cdn.cntrl.site/client-app-files/texture2.png';
|
|
8
|
+
const PATTERN_2_URL = 'https://cdn.cntrl.site/client-app-files/bayer16.png';
|
|
9
|
+
function useVideoFx(canvas, enabled, { videoUrl, fragmentShader, cursor, controls }, width, height) {
|
|
10
|
+
const [isFXAllowed, setIsFXAllowed] = (0, react_1.useState)(true);
|
|
11
|
+
const [isReady, setIsReady] = (0, react_1.useState)(false);
|
|
12
|
+
const active = enabled && isReady;
|
|
13
|
+
const mousePos = (0, react_1.useRef)([0.0, 0.0]);
|
|
14
|
+
const videoTextureManager = (0, react_1.useMemo)(() => {
|
|
15
|
+
if (!videoUrl || !enabled)
|
|
16
|
+
return;
|
|
17
|
+
return new effects_1.VideoTextureManager(videoUrl);
|
|
18
|
+
}, [videoUrl, enabled]);
|
|
19
|
+
const videoFx = (0, react_1.useMemo)(() => {
|
|
20
|
+
if (!cursor || !videoTextureManager)
|
|
21
|
+
return;
|
|
22
|
+
const { type, x, y } = cursor;
|
|
23
|
+
return new effects_1.MediaEffect(videoTextureManager, PATTERN_URL, PATTERN_2_URL, fragmentShader, Object.assign({ time: 0, cursor: type === 'mouse' ? mousePos.current : [x, y] }, controls), width, height);
|
|
24
|
+
}, [videoTextureManager, fragmentShader, width, height]);
|
|
25
|
+
(0, react_1.useEffect)(() => {
|
|
26
|
+
if (!videoTextureManager)
|
|
27
|
+
return;
|
|
28
|
+
videoTextureManager.onReadyStatusChange(setIsReady);
|
|
29
|
+
}, [videoTextureManager]);
|
|
30
|
+
(0, react_1.useEffect)(() => {
|
|
31
|
+
if (!cursor || cursor.type !== 'mouse' || !canvas || !videoFx)
|
|
32
|
+
return;
|
|
33
|
+
const handleMouseMove = (evt) => {
|
|
34
|
+
const rect = canvas.getBoundingClientRect();
|
|
35
|
+
const x = (0, rangeMap_1.rangeMap)(evt.clientX, rect.left, rect.left + rect.width, 0, 1, true);
|
|
36
|
+
const y = (0, rangeMap_1.rangeMap)(evt.clientY, rect.top, rect.top + rect.height, 0, 1, true);
|
|
37
|
+
videoFx.setParam('cursor', [x, y]);
|
|
38
|
+
};
|
|
39
|
+
window.addEventListener('mousemove', handleMouseMove, { capture: true, passive: true });
|
|
40
|
+
return () => {
|
|
41
|
+
window.removeEventListener('mousemove', handleMouseMove);
|
|
42
|
+
};
|
|
43
|
+
}, [canvas, videoFx, cursor === null || cursor === void 0 ? void 0 : cursor.type]);
|
|
44
|
+
(0, react_1.useEffect)(() => {
|
|
45
|
+
const gl = canvas === null || canvas === void 0 ? void 0 : canvas.getContext('webgl2');
|
|
46
|
+
if (!active || !canvas || !gl || !videoFx)
|
|
47
|
+
return;
|
|
48
|
+
gl.enable(gl.BLEND);
|
|
49
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
50
|
+
let running = false;
|
|
51
|
+
let time = 0;
|
|
52
|
+
let frame;
|
|
53
|
+
const renderFrame = () => {
|
|
54
|
+
time += 0.1;
|
|
55
|
+
videoFx.setViewport(Math.floor(canvas.width), Math.floor(canvas.height));
|
|
56
|
+
videoFx.setParam('time', time);
|
|
57
|
+
try {
|
|
58
|
+
videoFx.render(gl);
|
|
59
|
+
}
|
|
60
|
+
catch (_a) {
|
|
61
|
+
setIsFXAllowed(false);
|
|
62
|
+
}
|
|
63
|
+
frame = requestAnimationFrame(renderFrame);
|
|
64
|
+
};
|
|
65
|
+
const observer = new IntersectionObserver(([entry]) => {
|
|
66
|
+
if (entry.isIntersecting && !running) {
|
|
67
|
+
frame = requestAnimationFrame(renderFrame);
|
|
68
|
+
running = true;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (!entry.isIntersecting && running) {
|
|
72
|
+
window.cancelAnimationFrame(frame);
|
|
73
|
+
running = false;
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
observer.observe(canvas);
|
|
77
|
+
try {
|
|
78
|
+
videoFx.prepare(gl);
|
|
79
|
+
}
|
|
80
|
+
catch (_a) {
|
|
81
|
+
setIsFXAllowed(false);
|
|
82
|
+
}
|
|
83
|
+
return () => {
|
|
84
|
+
window.cancelAnimationFrame(frame);
|
|
85
|
+
observer.disconnect();
|
|
86
|
+
};
|
|
87
|
+
}, [canvas, videoFx, active]);
|
|
88
|
+
return isFXAllowed;
|
|
89
|
+
}
|
|
90
|
+
exports.useVideoFx = useVideoFx;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cntrl-site/sdk-nextjs",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "SDK for Next.js",
|
|
5
5
|
"author": "arsen@momdesign.nyc",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@antfu/eslint-config": "^3.8.0",
|
|
32
32
|
"@cntrl-site/color": "^1.0.0",
|
|
33
|
-
"@cntrl-site/effects": "^1.
|
|
34
|
-
"@cntrl-site/sdk": "^1.
|
|
33
|
+
"@cntrl-site/effects": "^1.3.1",
|
|
34
|
+
"@cntrl-site/sdk": "^1.15.0",
|
|
35
35
|
"@types/vimeo__player": "^2.18.0",
|
|
36
36
|
"@vimeo/player": "^2.25.0",
|
|
37
37
|
"html-react-parser": "^3.0.1",
|
|
@@ -7,20 +7,12 @@ import { LinkWrapper } from '../LinkWrapper';
|
|
|
7
7
|
import { useFileItem } from './useFileItem';
|
|
8
8
|
import { useItemAngle } from '../useItemAngle';
|
|
9
9
|
import { useCntrlContext } from '../../../provider/useCntrlContext';
|
|
10
|
-
import { useRegisterResize } from
|
|
10
|
+
import { useRegisterResize } from '../../../common/useRegisterResize';
|
|
11
11
|
import { useImageFx } from '../../../utils/effects/useImageFx';
|
|
12
12
|
import { useElementRect } from '../../../utils/useElementRect';
|
|
13
13
|
import { useLayoutContext } from '../../useLayoutContext';
|
|
14
14
|
import { getStyleFromItemStateAndParams } from '../../../utils/getStyleFromItemStateAndParams';
|
|
15
|
-
|
|
16
|
-
const baseVariables = `precision mediump float;
|
|
17
|
-
uniform sampler2D u_image;
|
|
18
|
-
uniform sampler2D u_pattern;
|
|
19
|
-
uniform vec2 u_imgDimensions;
|
|
20
|
-
uniform vec2 u_patternDimensions;
|
|
21
|
-
uniform float u_time;
|
|
22
|
-
uniform vec2 u_cursor;
|
|
23
|
-
varying vec2 v_texCoord;`;
|
|
15
|
+
import { baseVariables } from './shaderBaseVars';
|
|
24
16
|
|
|
25
17
|
export const ImageItem: FC<ItemProps<TImageItem>> = ({ item, sectionId, onResize, interactionCtrl, onVisibilityChange }) => {
|
|
26
18
|
const id = useId();
|
|
@@ -41,7 +33,7 @@ export const ImageItem: FC<ItemProps<TImageItem>> = ({ item, sectionId, onResize
|
|
|
41
33
|
const isInitialRef = useRef(true);
|
|
42
34
|
const controls = FXControls ?? [];
|
|
43
35
|
const controlsVariables = controls.map((c) => `uniform ${c.type} ${c.shaderParam};`)
|
|
44
|
-
|
|
36
|
+
.join('\n');
|
|
45
37
|
const controlValues = controls.reduce<Record<string, ControlValue>>((acc, control) => {
|
|
46
38
|
acc[control.shaderParam] = control.value;
|
|
47
39
|
return acc;
|
|
@@ -74,7 +66,7 @@ export const ImageItem: FC<ItemProps<TImageItem>> = ({ item, sectionId, onResize
|
|
|
74
66
|
const rectWidth = Math.floor(rect?.width ?? 0);
|
|
75
67
|
const rectHeight = Math.floor(rect?.height ?? 0);
|
|
76
68
|
const borderColor = useMemo(() => {
|
|
77
|
-
const borderColor = getStyleFromItemStateAndParams(imgStateParams?.styles?.strokeColor, itemStrokeColor)
|
|
69
|
+
const borderColor = getStyleFromItemStateAndParams(imgStateParams?.styles?.strokeColor, itemStrokeColor);
|
|
78
70
|
return borderColor ? CntrlColor.parse(borderColor) : undefined;
|
|
79
71
|
}, [itemStrokeColor, imgStateParams?.styles?.strokeColor]);
|
|
80
72
|
const radius = getStyleFromItemStateAndParams(imgStateParams?.styles?.radius, itemRadius);
|
|
@@ -105,22 +97,24 @@ export const ImageItem: FC<ItemProps<TImageItem>> = ({ item, sectionId, onResize
|
|
|
105
97
|
transition: wrapperStateParams?.transition ?? 'none'
|
|
106
98
|
}}
|
|
107
99
|
>
|
|
108
|
-
{hasGLEffect && isFXAllowed
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
100
|
+
{hasGLEffect && isFXAllowed
|
|
101
|
+
? (
|
|
102
|
+
<canvas
|
|
103
|
+
style={inlineStyles}
|
|
104
|
+
ref={fxCanvas}
|
|
105
|
+
className={`img-canvas image-${item.id}`}
|
|
106
|
+
width={rectWidth}
|
|
107
|
+
height={rectHeight}
|
|
108
|
+
/>
|
|
109
|
+
)
|
|
110
|
+
: (
|
|
111
|
+
<img
|
|
112
|
+
alt=""
|
|
113
|
+
className={`image image-${item.id}`}
|
|
114
|
+
style={inlineStyles}
|
|
115
|
+
src={item.commonParams.url}
|
|
116
|
+
/>
|
|
117
|
+
)}
|
|
124
118
|
</div>
|
|
125
119
|
<JSXStyle id={id}>{`
|
|
126
120
|
.image-wrapper-${item.id} {
|
|
@@ -7,14 +7,18 @@ import { LinkWrapper } from '../LinkWrapper';
|
|
|
7
7
|
import { useFileItem } from './useFileItem';
|
|
8
8
|
import { useItemAngle } from '../useItemAngle';
|
|
9
9
|
import { useCntrlContext } from '../../../provider/useCntrlContext';
|
|
10
|
-
import { useRegisterResize } from
|
|
10
|
+
import { useRegisterResize } from '../../../common/useRegisterResize';
|
|
11
11
|
import { useLayoutContext } from '../../useLayoutContext';
|
|
12
12
|
import { ScrollPlaybackVideo } from '../../ScrollPlaybackVideo';
|
|
13
13
|
import { getStyleFromItemStateAndParams } from '../../../utils/getStyleFromItemStateAndParams';
|
|
14
|
+
import { baseVariables } from './shaderBaseVars';
|
|
15
|
+
import { useVideoFx } from '../../../utils/effects/useVideoFx';
|
|
16
|
+
import { useElementRect } from '../../../utils/useElementRect';
|
|
14
17
|
|
|
15
18
|
export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId, onResize, interactionCtrl, onVisibilityChange }) => {
|
|
16
19
|
const id = useId();
|
|
17
20
|
const { layouts } = useCntrlContext();
|
|
21
|
+
const layoutId = useLayoutContext();
|
|
18
22
|
const {
|
|
19
23
|
radius: itemRadius,
|
|
20
24
|
strokeWidth: itemStrokeWidth,
|
|
@@ -25,7 +29,24 @@ export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId, onResize
|
|
|
25
29
|
const itemAngle = useItemAngle(item, sectionId);
|
|
26
30
|
const [ref, setRef] = useState<HTMLDivElement | null>(null);
|
|
27
31
|
const videoRef = useRef<HTMLVideoElement | null>(null);
|
|
28
|
-
const
|
|
32
|
+
const fxCanvas = useRef<HTMLCanvasElement | null>(null);
|
|
33
|
+
const { url, hasGLEffect, fragmentShader, FXControls, FXCursor } = item.commonParams;
|
|
34
|
+
const isInitialRef = useRef(true);
|
|
35
|
+
const area = layoutId ? item.area[layoutId] : null;
|
|
36
|
+
const exemplary = layouts?.find(l => l.id === layoutId)?.exemplary;
|
|
37
|
+
const width = area && exemplary ? area.width * exemplary : 0;
|
|
38
|
+
const height = area && exemplary ? area.height * exemplary : 0;
|
|
39
|
+
const controls = FXControls ?? [];
|
|
40
|
+
const controlsVariables = controls.map((c) => `uniform ${c.type} ${c.shaderParam};`)
|
|
41
|
+
.join('\n');
|
|
42
|
+
const controlValues = controls.reduce<Record<string, ControlValue>>((acc, control) => {
|
|
43
|
+
acc[control.shaderParam] = control.value;
|
|
44
|
+
return acc;
|
|
45
|
+
}, {});
|
|
46
|
+
const fullShaderCode = `${baseVariables}\n${controlsVariables}\n${fragmentShader}`;
|
|
47
|
+
const rect = useElementRect(ref);
|
|
48
|
+
const rectWidth = Math.floor(rect?.width ?? 0);
|
|
49
|
+
const rectHeight = Math.floor(rect?.height ?? 0);
|
|
29
50
|
const scrollPlayback = layoutId ? item.layoutParams[layoutId].scrollPlayback : null;
|
|
30
51
|
const layoutValues: Record<string, any>[] = [item.area, item.layoutParams];
|
|
31
52
|
const hasScrollPlayback = scrollPlayback !== null;
|
|
@@ -40,10 +61,25 @@ export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId, onResize
|
|
|
40
61
|
const blur = getStyleFromItemStateAndParams(wrapperStateParams?.styles?.blur, itemBlur);
|
|
41
62
|
const strokeWidth = getStyleFromItemStateAndParams(wrapperStateParams?.styles?.strokeWidth, itemStrokeWidth);
|
|
42
63
|
const radius = getStyleFromItemStateAndParams(wrapperStateParams?.styles?.radius, itemRadius);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
isInitialRef.current = false;
|
|
66
|
+
}, []);
|
|
67
|
+
const isFXAllowed = useVideoFx(
|
|
68
|
+
fxCanvas.current,
|
|
69
|
+
!!(hasGLEffect && !isInitialRef.current),
|
|
70
|
+
{
|
|
71
|
+
videoUrl: url,
|
|
72
|
+
fragmentShader: fullShaderCode,
|
|
73
|
+
cursor: FXCursor,
|
|
74
|
+
controls: controlValues
|
|
75
|
+
},
|
|
76
|
+
width,
|
|
77
|
+
height
|
|
78
|
+
);
|
|
43
79
|
useRegisterResize(ref, onResize);
|
|
44
80
|
const inlineStyles = {
|
|
45
81
|
...(radius !== undefined ? { borderRadius: `${radius * 100}vw` } : {}),
|
|
46
|
-
...(strokeWidth !== undefined
|
|
82
|
+
...(strokeWidth !== undefined ? { borderWidth: `${strokeWidth * 100}vw` } : {}),
|
|
47
83
|
...(borderColor ? { borderColor: `${borderColor.toCss()}` } : {}),
|
|
48
84
|
transition: videoStateParams?.transition ?? 'none'
|
|
49
85
|
};
|
|
@@ -64,28 +100,45 @@ export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId, onResize
|
|
|
64
100
|
transition: wrapperStateParams?.transition ?? 'none'
|
|
65
101
|
}}
|
|
66
102
|
>
|
|
67
|
-
{hasScrollPlayback
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
103
|
+
{hasScrollPlayback
|
|
104
|
+
? (
|
|
105
|
+
<ScrollPlaybackVideo
|
|
106
|
+
sectionId={sectionId}
|
|
107
|
+
src={item.commonParams.url}
|
|
108
|
+
playbackParams={scrollPlayback}
|
|
109
|
+
style={inlineStyles}
|
|
110
|
+
className={`video video-playback-wrapper video-${item.id}`}
|
|
111
|
+
/>
|
|
112
|
+
)
|
|
113
|
+
: (
|
|
114
|
+
<>
|
|
115
|
+
{hasGLEffect && isFXAllowed
|
|
116
|
+
? (
|
|
117
|
+
<canvas
|
|
118
|
+
style={inlineStyles}
|
|
119
|
+
ref={fxCanvas}
|
|
120
|
+
className={`video-canvas video-${item.id}`}
|
|
121
|
+
width={rectWidth}
|
|
122
|
+
height={rectHeight}
|
|
123
|
+
/>
|
|
124
|
+
)
|
|
125
|
+
: (
|
|
126
|
+
<video
|
|
127
|
+
poster={item.commonParams.coverUrl ?? ''}
|
|
128
|
+
ref={videoRef}
|
|
129
|
+
autoPlay
|
|
130
|
+
muted
|
|
131
|
+
loop
|
|
132
|
+
playsInline
|
|
133
|
+
className={`video video-${item.id}`}
|
|
134
|
+
style={inlineStyles}
|
|
135
|
+
>
|
|
136
|
+
<source src={item.commonParams.url} />
|
|
137
|
+
</video>
|
|
138
|
+
)}
|
|
139
|
+
|
|
140
|
+
</>
|
|
141
|
+
)}
|
|
89
142
|
</div>
|
|
90
143
|
<JSXStyle id={id}>{`
|
|
91
144
|
.video-wrapper-${item.id} {
|
|
@@ -114,8 +167,16 @@ export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId, onResize
|
|
|
114
167
|
display: flex;
|
|
115
168
|
justify-content: center;
|
|
116
169
|
}
|
|
170
|
+
.video-canvas {
|
|
171
|
+
border: solid;
|
|
172
|
+
width: 100%;
|
|
173
|
+
height: 100%;
|
|
174
|
+
pointer-events: none;
|
|
175
|
+
border-width: 0;
|
|
176
|
+
box-sizing: border-box;
|
|
177
|
+
}
|
|
117
178
|
${getLayoutStyles(layouts, layoutValues, ([area, layoutParams]) => {
|
|
118
|
-
|
|
179
|
+
return (`
|
|
119
180
|
.video-wrapper-${item.id} {
|
|
120
181
|
opacity: ${layoutParams.opacity};
|
|
121
182
|
transform: rotate(${area.angle}deg);
|
|
@@ -127,8 +188,10 @@ export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId, onResize
|
|
|
127
188
|
border-width: ${layoutParams.strokeWidth * 100}vw;
|
|
128
189
|
}
|
|
129
190
|
`);
|
|
130
|
-
|
|
191
|
+
})}
|
|
131
192
|
`}</JSXStyle>
|
|
132
193
|
</LinkWrapper>
|
|
133
194
|
);
|
|
134
195
|
};
|
|
196
|
+
|
|
197
|
+
type ControlValue = number | [number, number];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { MediaEffect, ImageTextureManager } from '@cntrl-site/effects';
|
|
3
3
|
import { rangeMap } from '../rangeMap';
|
|
4
4
|
|
|
5
5
|
export interface FXCursor {
|
|
@@ -12,7 +12,6 @@ interface FxParams {
|
|
|
12
12
|
imageUrl?: string;
|
|
13
13
|
fragmentShader: string | null;
|
|
14
14
|
cursor: FXCursor | null;
|
|
15
|
-
// TODO use types from SDk
|
|
16
15
|
controls?: Record<string, number | [number, number]>;
|
|
17
16
|
}
|
|
18
17
|
|
|
@@ -33,11 +32,15 @@ export function useImageFx(
|
|
|
33
32
|
): boolean {
|
|
34
33
|
const [isFXAllowed, setIsFXAllowed] = useState(true);
|
|
35
34
|
const mousePos = useRef<[number, number]>([0.0, 0.0]);
|
|
36
|
-
const
|
|
37
|
-
if (!imageUrl || !
|
|
35
|
+
const imageTextureManager = useMemo(() => {
|
|
36
|
+
if (!imageUrl || !enabled) return;
|
|
37
|
+
return new ImageTextureManager(imageUrl);
|
|
38
|
+
}, [imageUrl, enabled]);
|
|
39
|
+
const imageFx = useMemo<MediaEffect | undefined>(() => {
|
|
40
|
+
if (!cursor || !imageTextureManager) return;
|
|
38
41
|
const { type, x, y } = cursor;
|
|
39
|
-
return new
|
|
40
|
-
|
|
42
|
+
return new MediaEffect(
|
|
43
|
+
imageTextureManager,
|
|
41
44
|
PATTERN_URL,
|
|
42
45
|
PATTERN_2_URL,
|
|
43
46
|
fragmentShader!,
|
|
@@ -49,7 +52,7 @@ export function useImageFx(
|
|
|
49
52
|
width,
|
|
50
53
|
height
|
|
51
54
|
);
|
|
52
|
-
}, [
|
|
55
|
+
}, [imageTextureManager, fragmentShader, width, height]);
|
|
53
56
|
|
|
54
57
|
useEffect(() => {
|
|
55
58
|
if (!cursor || cursor.type !== 'mouse' || !canvas || !imageFx) return;
|
|
@@ -80,7 +83,7 @@ export function useImageFx(
|
|
|
80
83
|
imageFx.setParam('time', time);
|
|
81
84
|
try {
|
|
82
85
|
imageFx.render(gl);
|
|
83
|
-
} catch
|
|
86
|
+
} catch {
|
|
84
87
|
setIsFXAllowed(false);
|
|
85
88
|
}
|
|
86
89
|
frame = requestAnimationFrame(renderFrame);
|
|
@@ -100,7 +103,7 @@ export function useImageFx(
|
|
|
100
103
|
observer.observe(canvas);
|
|
101
104
|
try {
|
|
102
105
|
imageFx.prepare(gl);
|
|
103
|
-
} catch
|
|
106
|
+
} catch {
|
|
104
107
|
setIsFXAllowed(false);
|
|
105
108
|
}
|
|
106
109
|
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { MediaEffect, VideoTextureManager } from '@cntrl-site/effects';
|
|
3
|
+
import { rangeMap } from '../rangeMap';
|
|
4
|
+
|
|
5
|
+
export interface FXCursor {
|
|
6
|
+
type: 'mouse' | 'manual';
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface FxParams {
|
|
12
|
+
videoUrl?: string;
|
|
13
|
+
fragmentShader: string | null;
|
|
14
|
+
cursor: FXCursor | null;
|
|
15
|
+
controls?: Record<string, number | [number, number]>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const PATTERN_URL = 'https://cdn.cntrl.site/client-app-files/texture2.png';
|
|
19
|
+
const PATTERN_2_URL = 'https://cdn.cntrl.site/client-app-files/bayer16.png';
|
|
20
|
+
|
|
21
|
+
export function useVideoFx(
|
|
22
|
+
canvas: HTMLCanvasElement | null | undefined,
|
|
23
|
+
enabled: boolean,
|
|
24
|
+
{
|
|
25
|
+
videoUrl,
|
|
26
|
+
fragmentShader,
|
|
27
|
+
cursor,
|
|
28
|
+
controls
|
|
29
|
+
}: FxParams,
|
|
30
|
+
width: number,
|
|
31
|
+
height: number
|
|
32
|
+
): boolean {
|
|
33
|
+
const [isFXAllowed, setIsFXAllowed] = useState(true);
|
|
34
|
+
const [isReady, setIsReady] = useState(false);
|
|
35
|
+
const active = enabled && isReady;
|
|
36
|
+
const mousePos = useRef<[number, number]>([0.0, 0.0]);
|
|
37
|
+
const videoTextureManager = useMemo(() => {
|
|
38
|
+
if (!videoUrl || !enabled) return;
|
|
39
|
+
return new VideoTextureManager(videoUrl);
|
|
40
|
+
}, [videoUrl, enabled]);
|
|
41
|
+
const videoFx = useMemo<MediaEffect | undefined>(() => {
|
|
42
|
+
if (!cursor || !videoTextureManager) return;
|
|
43
|
+
const { type, x, y } = cursor;
|
|
44
|
+
return new MediaEffect(
|
|
45
|
+
videoTextureManager,
|
|
46
|
+
PATTERN_URL,
|
|
47
|
+
PATTERN_2_URL,
|
|
48
|
+
fragmentShader!,
|
|
49
|
+
{
|
|
50
|
+
time: 0,
|
|
51
|
+
cursor: type === 'mouse' ? mousePos.current : [x, y],
|
|
52
|
+
...controls
|
|
53
|
+
},
|
|
54
|
+
width,
|
|
55
|
+
height
|
|
56
|
+
);
|
|
57
|
+
}, [videoTextureManager, fragmentShader, width, height]);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!videoTextureManager) return;
|
|
61
|
+
videoTextureManager.onReadyStatusChange(setIsReady);
|
|
62
|
+
}, [videoTextureManager]);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!cursor || cursor.type !== 'mouse' || !canvas || !videoFx) return;
|
|
66
|
+
const handleMouseMove = (evt: MouseEvent) => {
|
|
67
|
+
const rect = canvas.getBoundingClientRect();
|
|
68
|
+
const x = rangeMap(evt.clientX, rect.left, rect.left + rect.width, 0, 1, true);
|
|
69
|
+
const y = rangeMap(evt.clientY, rect.top, rect.top + rect.height, 0, 1, true);
|
|
70
|
+
videoFx.setParam('cursor', [x, y]);
|
|
71
|
+
};
|
|
72
|
+
window.addEventListener('mousemove', handleMouseMove, { capture: true, passive: true });
|
|
73
|
+
return () => {
|
|
74
|
+
window.removeEventListener('mousemove', handleMouseMove);
|
|
75
|
+
};
|
|
76
|
+
}, [canvas, videoFx, cursor?.type]);
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const gl = canvas?.getContext('webgl2');
|
|
80
|
+
if (!active || !canvas || !gl || !videoFx) return;
|
|
81
|
+
gl.enable(gl.BLEND);
|
|
82
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
83
|
+
let running = false;
|
|
84
|
+
let time = 0;
|
|
85
|
+
let frame: number;
|
|
86
|
+
|
|
87
|
+
const renderFrame = () => {
|
|
88
|
+
time += 0.1;
|
|
89
|
+
videoFx.setViewport(Math.floor(canvas.width), Math.floor(canvas.height));
|
|
90
|
+
videoFx.setParam('time', time);
|
|
91
|
+
try {
|
|
92
|
+
videoFx.render(gl);
|
|
93
|
+
} catch {
|
|
94
|
+
setIsFXAllowed(false);
|
|
95
|
+
}
|
|
96
|
+
frame = requestAnimationFrame(renderFrame);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const observer = new IntersectionObserver(([entry]) => {
|
|
100
|
+
if (entry.isIntersecting && !running) {
|
|
101
|
+
frame = requestAnimationFrame(renderFrame);
|
|
102
|
+
running = true;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (!entry.isIntersecting && running) {
|
|
106
|
+
window.cancelAnimationFrame(frame);
|
|
107
|
+
running = false;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
observer.observe(canvas);
|
|
111
|
+
try {
|
|
112
|
+
videoFx.prepare(gl);
|
|
113
|
+
} catch {
|
|
114
|
+
setIsFXAllowed(false);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return () => {
|
|
118
|
+
window.cancelAnimationFrame(frame);
|
|
119
|
+
observer.disconnect();
|
|
120
|
+
};
|
|
121
|
+
}, [canvas, videoFx, active]);
|
|
122
|
+
return isFXAllowed;
|
|
123
|
+
}
|
|
Binary file
|