@cntrl-site/sdk-nextjs 0.19.4 → 0.20.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.
@@ -0,0 +1,15 @@
1
+ <component name="InspectionProjectProfileManager">
2
+ <profile version="1.0">
3
+ <option name="myName" value="Project Default" />
4
+ <inspection_tool class="HtmlUnknownAttribute" enabled="true" level="WARNING" enabled_by_default="true">
5
+ <option name="myValues">
6
+ <value>
7
+ <list size="1">
8
+ <item index="0" class="java.lang.String" itemvalue="jsx" />
9
+ </list>
10
+ </value>
11
+ </option>
12
+ <option name="myCustomValuesEnabled" value="true" />
13
+ </inspection_tool>
14
+ </profile>
15
+ </component>
Binary file
@@ -15,8 +15,10 @@ const useItemAngle_1 = require("../useItemAngle");
15
15
  const useCntrlContext_1 = require("../../provider/useCntrlContext");
16
16
  const HoverStyles_1 = require("../../utils/HoverStyles/HoverStyles");
17
17
  const useRegisterResize_1 = require("../../common/useRegisterResize");
18
+ const useImageFx_1 = require("../../utils/effects/useImageFx");
19
+ const useElementRect_1 = require("../../utils/useElementRect");
18
20
  const ImageItem = ({ item, sectionId, onResize }) => {
19
- var _a, _b;
21
+ var _a, _b, _c, _d;
20
22
  const id = (0, react_1.useId)();
21
23
  const { layouts } = (0, useCntrlContext_1.useCntrlContext)();
22
24
  const { radius, strokeWidth, opacity, strokeColor, blur } = (0, useFileItem_1.useFileItem)(item, sectionId);
@@ -24,15 +26,25 @@ const ImageItem = ({ item, sectionId, onResize }) => {
24
26
  const borderColor = (0, react_1.useMemo)(() => color_1.CntrlColor.parse(strokeColor), [strokeColor]);
25
27
  const [ref, setRef] = (0, react_1.useState)(null);
26
28
  (0, useRegisterResize_1.useRegisterResize)(ref, onResize);
27
- return ((0, jsx_runtime_1.jsx)(LinkWrapper_1.LinkWrapper, Object.assign({ url: (_a = item.link) === null || _a === void 0 ? void 0 : _a.url, target: (_b = item.link) === null || _b === void 0 ? void 0 : _b.target }, { children: (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", Object.assign({ className: `image-wrapper-${item.id}`, ref: setRef, style: {
29
+ const { url, hasGLEffect, fragmentShader } = item.commonParams;
30
+ const fxCanvas = (0, react_1.useRef)(null);
31
+ (0, useImageFx_1.useImageFx)(fxCanvas.current, hasGLEffect !== null && hasGLEffect !== void 0 ? hasGLEffect : false, {
32
+ imageUrl: url,
33
+ fragmentShader
34
+ });
35
+ const rect = (0, useElementRect_1.useElementRect)(ref);
36
+ const rectWidth = Math.floor((_a = rect === null || rect === void 0 ? void 0 : rect.width) !== null && _a !== void 0 ? _a : 0);
37
+ const rectHeight = Math.floor((_b = rect === null || rect === void 0 ? void 0 : rect.height) !== null && _b !== void 0 ? _b : 0);
38
+ const inlineStyles = {
39
+ borderRadius: `${radius * 100}vw`,
40
+ borderWidth: `${strokeWidth * 100}vw`,
41
+ borderColor: `${borderColor.toCss()}`
42
+ };
43
+ return ((0, jsx_runtime_1.jsx)(LinkWrapper_1.LinkWrapper, Object.assign({ url: (_c = item.link) === null || _c === void 0 ? void 0 : _c.url, target: (_d = item.link) === null || _d === void 0 ? void 0 : _d.target }, { children: (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", Object.assign({ className: `image-wrapper-${item.id}`, ref: setRef, style: {
28
44
  opacity: `${opacity}`,
29
45
  transform: `rotate(${angle}deg)`,
30
46
  filter: `blur(${blur * 100}vw)`
31
- } }, { children: (0, jsx_runtime_1.jsx)("img", { className: `image image-${item.id}`, style: {
32
- borderRadius: `${radius * 100}vw`,
33
- borderWidth: `${strokeWidth * 100}vw`,
34
- borderColor: `${borderColor.toCss()}`
35
- }, src: item.commonParams.url }) })), (0, jsx_runtime_1.jsx)(style_1.default, Object.assign({ id: id }, { children: `
47
+ } }, { children: item.commonParams.hasGLEffect ? ((0, jsx_runtime_1.jsx)("canvas", { style: inlineStyles, ref: fxCanvas, className: "img-canvas", width: rectWidth, height: rectHeight })) : ((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, Object.assign({ id: id }, { children: `
36
48
  @supports not (color: oklch(42% 0.3 90 / 1)) {
37
49
  .image-${item.id} {
38
50
  border-color: ${borderColor.fmt('rgba')};
@@ -53,6 +65,15 @@ const ImageItem = ({ item, sectionId, onResize }) => {
53
65
  pointer-events: none;
54
66
  border-style: solid;
55
67
  overflow: hidden;
68
+ box-sizing: border-box;
69
+ }
70
+ .img-canvas {
71
+ border: solid;
72
+ width: 100%;
73
+ height: 100%;
74
+ pointer-events: none;
75
+ border-width: 0;
76
+ box-sizing: border-box;
56
77
  }
57
78
  ${(0, sdk_1.getLayoutStyles)(layouts, [item.state.hover], ([hoverParams]) => {
58
79
  return (`
@@ -4,6 +4,7 @@ exports.Animator = void 0;
4
4
  const sdk_1 = require("@cntrl-site/sdk");
5
5
  const color_1 = require("@cntrl-site/color");
6
6
  const binSearchInsertAt_1 = require("../binSearchInsertAt");
7
+ const rangeMap_1 = require("../rangeMap");
7
8
  const compare = (lhs, rhs) => lhs.position - rhs.position;
8
9
  const insertBin = (0, binSearchInsertAt_1.createInsert)(binSearchInsertAt_1.binSearchInsertAt, compare);
9
10
  class Animator {
@@ -28,8 +29,8 @@ class Animator {
28
29
  }
29
30
  const { start, end } = this.getStartEnd(pos, keyframes);
30
31
  return {
31
- width: rangeMap(pos, start.position, end.position, start.value.width, end.value.width, true),
32
- height: rangeMap(pos, start.position, end.position, start.value.height, end.value.height, true)
32
+ width: (0, rangeMap_1.rangeMap)(pos, start.position, end.position, start.value.width, end.value.width, true),
33
+ height: (0, rangeMap_1.rangeMap)(pos, start.position, end.position, start.value.height, end.value.height, true)
33
34
  };
34
35
  }
35
36
  getPositions(values, pos) {
@@ -45,8 +46,8 @@ class Animator {
45
46
  }
46
47
  const { start, end } = this.getStartEnd(pos, keyframes);
47
48
  return {
48
- left: rangeMap(pos, start.position, end.position, start.value.left, end.value.left, true),
49
- top: rangeMap(pos, start.position, end.position, start.value.top, end.value.top, true)
49
+ left: (0, rangeMap_1.rangeMap)(pos, start.position, end.position, start.value.left, end.value.left, true),
50
+ top: (0, rangeMap_1.rangeMap)(pos, start.position, end.position, start.value.top, end.value.top, true)
50
51
  };
51
52
  }
52
53
  getColor(values, pos) {
@@ -91,7 +92,7 @@ class Animator {
91
92
  }
92
93
  const { start, end } = this.getStartEnd(pos, keyframes);
93
94
  return {
94
- radius: rangeMap(pos, start.position, end.position, start.value.radius, end.value.radius, true)
95
+ radius: (0, rangeMap_1.rangeMap)(pos, start.position, end.position, start.value.radius, end.value.radius, true)
95
96
  };
96
97
  }
97
98
  getBorderWidth(values, pos) {
@@ -106,7 +107,7 @@ class Animator {
106
107
  }
107
108
  const { start, end } = this.getStartEnd(pos, keyframes);
108
109
  return {
109
- borderWidth: rangeMap(pos, start.position, end.position, start.value.borderWidth, end.value.borderWidth, true)
110
+ borderWidth: (0, rangeMap_1.rangeMap)(pos, start.position, end.position, start.value.borderWidth, end.value.borderWidth, true)
110
111
  };
111
112
  }
112
113
  getRotation(values, pos) {
@@ -121,7 +122,7 @@ class Animator {
121
122
  }
122
123
  const { start, end } = this.getStartEnd(pos, keyframes);
123
124
  return {
124
- angle: rangeMap(pos, start.position, end.position, start.value.angle, end.value.angle, true)
125
+ angle: (0, rangeMap_1.rangeMap)(pos, start.position, end.position, start.value.angle, end.value.angle, true)
125
126
  };
126
127
  }
127
128
  getOpacity(values, pos) {
@@ -136,7 +137,7 @@ class Animator {
136
137
  }
137
138
  const { start, end } = this.getStartEnd(pos, keyframes);
138
139
  return {
139
- opacity: rangeMap(pos, start.position, end.position, start.value.opacity, end.value.opacity, true)
140
+ opacity: (0, rangeMap_1.rangeMap)(pos, start.position, end.position, start.value.opacity, end.value.opacity, true)
140
141
  };
141
142
  }
142
143
  getScale(values, pos) {
@@ -151,7 +152,7 @@ class Animator {
151
152
  }
152
153
  const { start, end } = this.getStartEnd(pos, keyframes);
153
154
  return {
154
- scale: rangeMap(pos, start.position, end.position, start.value.scale, end.value.scale, true)
155
+ scale: (0, rangeMap_1.rangeMap)(pos, start.position, end.position, start.value.scale, end.value.scale, true)
155
156
  };
156
157
  }
157
158
  getBlur(values, pos) {
@@ -166,7 +167,7 @@ class Animator {
166
167
  }
167
168
  const { start, end } = this.getStartEnd(pos, keyframes);
168
169
  return {
169
- blur: rangeMap(pos, start.position, end.position, start.value.blur, end.value.blur, true)
170
+ blur: (0, rangeMap_1.rangeMap)(pos, start.position, end.position, start.value.blur, end.value.blur, true)
170
171
  };
171
172
  }
172
173
  getBackdropBlur(values, pos) {
@@ -181,7 +182,7 @@ class Animator {
181
182
  }
182
183
  const { start, end } = this.getStartEnd(pos, keyframes);
183
184
  return {
184
- backdropBlur: rangeMap(pos, start.position, end.position, start.value.backdropBlur, end.value.backdropBlur, true)
185
+ backdropBlur: (0, rangeMap_1.rangeMap)(pos, start.position, end.position, start.value.backdropBlur, end.value.backdropBlur, true)
185
186
  };
186
187
  }
187
188
  getTextColor(values, pos) {
@@ -211,7 +212,7 @@ class Animator {
211
212
  }
212
213
  const { start, end } = this.getStartEnd(pos, keyframes);
213
214
  return {
214
- letterSpacing: rangeMap(pos, start.position, end.position, start.value.letterSpacing, end.value.letterSpacing, true)
215
+ letterSpacing: (0, rangeMap_1.rangeMap)(pos, start.position, end.position, start.value.letterSpacing, end.value.letterSpacing, true)
215
216
  };
216
217
  }
217
218
  getWordSpacing(values, pos) {
@@ -226,7 +227,7 @@ class Animator {
226
227
  }
227
228
  const { start, end } = this.getStartEnd(pos, keyframes);
228
229
  return {
229
- wordSpacing: rangeMap(pos, start.position, end.position, start.value.wordSpacing, end.value.wordSpacing, true)
230
+ wordSpacing: (0, rangeMap_1.rangeMap)(pos, start.position, end.position, start.value.wordSpacing, end.value.wordSpacing, true)
230
231
  };
231
232
  }
232
233
  getStartEnd(position, keyframes) {
@@ -242,7 +243,7 @@ class Animator {
242
243
  }
243
244
  }
244
245
  getRangeColor(start, end, position) {
245
- const rangeAmount = rangeMap(position, start.position, end.position, 0, 1, true);
246
+ const rangeAmount = (0, rangeMap_1.rangeMap)(position, start.position, end.position, 0, 1, true);
246
247
  const startColor = color_1.CntrlColor.parse(start.value.color);
247
248
  const endColor = color_1.CntrlColor.parse(end.value.color);
248
249
  const mixedColor = startColor.mix(endColor, rangeAmount);
@@ -268,11 +269,3 @@ function createKeyframesMap() {
268
269
  [sdk_1.KeyframeType.TextColor]: []
269
270
  };
270
271
  }
271
- function rangeMap(n, start1, stop1, start2, stop2, withinBounds = false) {
272
- const mapped = (n - start1) / (stop1 - start1) * (stop2 - start2) + start2;
273
- if (withinBounds && n < start1)
274
- return start2;
275
- if (withinBounds && n > stop1)
276
- return stop2;
277
- return mapped;
278
- }
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useImageFx = 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 useImageFx(canvas, enabled, { imageUrl, fragmentShader }) {
10
+ const cursor = (0, react_1.useRef)([0, 0]);
11
+ const imageFx = (0, react_1.useMemo)(() => {
12
+ if (!imageUrl)
13
+ return undefined;
14
+ return new effects_1.ImageEffect(imageUrl, PATTERN_URL, PATTERN_2_URL, fragmentShader, { time: 0, cursor: cursor.current });
15
+ }, [imageUrl, fragmentShader]);
16
+ (0, react_1.useEffect)(() => {
17
+ if (!canvas || !imageFx)
18
+ return;
19
+ const handleMouseMove = (evt) => {
20
+ const rect = canvas.getBoundingClientRect();
21
+ const x = (0, rangeMap_1.rangeMap)(evt.clientX, rect.left, rect.left + rect.width, 0, 1);
22
+ const y = (0, rangeMap_1.rangeMap)(evt.clientY, rect.top, rect.top + rect.height, 0, 1);
23
+ imageFx.setParam('cursor', [x, y]);
24
+ };
25
+ window.addEventListener('mousemove', handleMouseMove, { capture: true, passive: true });
26
+ return () => {
27
+ window.removeEventListener('mousemove', handleMouseMove);
28
+ };
29
+ }, [canvas, imageFx]);
30
+ (0, react_1.useEffect)(() => {
31
+ const gl = canvas === null || canvas === void 0 ? void 0 : canvas.getContext('webgl2');
32
+ if (!enabled || !canvas || !gl || !imageFx)
33
+ return;
34
+ let running = false;
35
+ let time = 0;
36
+ let frame;
37
+ const renderFrame = () => {
38
+ time += 0.1;
39
+ imageFx.setViewport(Math.floor(canvas.width), Math.floor(canvas.height));
40
+ imageFx.setParam('time', time);
41
+ imageFx.render(gl);
42
+ frame = requestAnimationFrame(renderFrame);
43
+ };
44
+ const observer = new IntersectionObserver(([entry]) => {
45
+ if (entry.isIntersecting && !running) {
46
+ frame = requestAnimationFrame(renderFrame);
47
+ running = true;
48
+ return;
49
+ }
50
+ if (!entry.isIntersecting && running) {
51
+ window.cancelAnimationFrame(frame);
52
+ running = false;
53
+ }
54
+ });
55
+ observer.observe(canvas);
56
+ imageFx.prepare(gl);
57
+ return () => {
58
+ window.cancelAnimationFrame(frame);
59
+ observer.disconnect();
60
+ };
61
+ }, [canvas, imageFx, enabled]);
62
+ }
63
+ exports.useImageFx = useImageFx;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.rangeMap = void 0;
4
+ function rangeMap(n, start1, stop1, start2, stop2, withinBounds = false) {
5
+ const mapped = (n - start1) / (stop1 - start1) * (stop2 - start2) + start2;
6
+ if (withinBounds && n < start1)
7
+ return start2;
8
+ if (withinBounds && n > stop1)
9
+ return stop2;
10
+ return mapped;
11
+ }
12
+ exports.rangeMap = rangeMap;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useElementRect = void 0;
4
+ const react_1 = require("react");
5
+ function useElementRect(element) {
6
+ const [rect, setRect] = (0, react_1.useState)(() => element === null || element === void 0 ? void 0 : element.getBoundingClientRect());
7
+ (0, react_1.useEffect)(() => {
8
+ if (!element)
9
+ return;
10
+ const observer = new ResizeObserver(entries => {
11
+ const el = entries.find(entry => entry.target === element);
12
+ if (!el) {
13
+ throw new Error('Element is not found in observed entries');
14
+ }
15
+ setRect(el.target.getBoundingClientRect());
16
+ });
17
+ observer.observe(element);
18
+ return () => {
19
+ observer.unobserve(element);
20
+ observer.disconnect();
21
+ };
22
+ }, [element]);
23
+ return rect;
24
+ }
25
+ exports.useElementRect = useElementRect;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cntrl-site/sdk-nextjs",
3
- "version": "0.19.4",
3
+ "version": "0.20.1",
4
4
  "description": "SDK for Next.js",
5
5
  "main": "lib/index.js",
6
6
  "types": "src/index.ts",
@@ -22,7 +22,8 @@
22
22
  "homepage": "https://github.com/cntrl-site/sdk-nextjs#readme",
23
23
  "dependencies": {
24
24
  "@cntrl-site/color": "^1.0.0",
25
- "@cntrl-site/sdk": "^1.4.0",
25
+ "@cntrl-site/effects": "^1.0.5-1",
26
+ "@cntrl-site/sdk": "^1.4.1",
26
27
  "@types/vimeo__player": "^2.18.0",
27
28
  "@vimeo/player": "^2.20.1",
28
29
  "html-react-parser": "^3.0.1",
@@ -1,4 +1,4 @@
1
- import { FC, useId, useMemo, useState } from 'react';
1
+ import { FC, useId, useMemo, useRef, useState } from 'react';
2
2
  import JSXStyle from 'styled-jsx/style';
3
3
  import { CntrlColor } from '@cntrl-site/color';
4
4
  import { ArticleItemType, getLayoutStyles, ImageItem as TImageItem } from '@cntrl-site/sdk';
@@ -9,6 +9,8 @@ import { useItemAngle } from '../useItemAngle';
9
9
  import { useCntrlContext } from '../../provider/useCntrlContext';
10
10
  import { getHoverStyles, getTransitions } from '../../utils/HoverStyles/HoverStyles';
11
11
  import { useRegisterResize } from "../../common/useRegisterResize";
12
+ import { useImageFx } from '../../utils/effects/useImageFx';
13
+ import { useElementRect } from '../../utils/useElementRect';
12
14
 
13
15
  export const ImageItem: FC<ItemProps<TImageItem>> = ({ item, sectionId, onResize }) => {
14
16
  const id = useId();
@@ -18,6 +20,20 @@ export const ImageItem: FC<ItemProps<TImageItem>> = ({ item, sectionId, onResize
18
20
  const borderColor = useMemo(() => CntrlColor.parse(strokeColor), [strokeColor]);
19
21
  const [ref, setRef] = useState<HTMLDivElement | null>(null);
20
22
  useRegisterResize(ref, onResize);
23
+ const { url, hasGLEffect, fragmentShader } = item.commonParams;
24
+ const fxCanvas = useRef<HTMLCanvasElement | null>(null);
25
+ useImageFx(fxCanvas.current, hasGLEffect ?? false, {
26
+ imageUrl: url,
27
+ fragmentShader
28
+ });
29
+ const rect = useElementRect(ref);
30
+ const rectWidth = Math.floor(rect?.width ?? 0);
31
+ const rectHeight = Math.floor(rect?.height ?? 0);
32
+ const inlineStyles = {
33
+ borderRadius: `${radius * 100}vw`,
34
+ borderWidth: `${strokeWidth * 100}vw`,
35
+ borderColor: `${borderColor.toCss()}`
36
+ };
21
37
  return (
22
38
  <LinkWrapper url={item.link?.url} target={item.link?.target}>
23
39
  <>
@@ -30,15 +46,23 @@ export const ImageItem: FC<ItemProps<TImageItem>> = ({ item, sectionId, onResize
30
46
  filter: `blur(${blur * 100}vw)`
31
47
  }}
32
48
  >
33
- <img
34
- className={`image image-${item.id}`}
35
- style={{
36
- borderRadius: `${radius * 100}vw`,
37
- borderWidth: `${strokeWidth * 100}vw`,
38
- borderColor: `${borderColor.toCss()}`
39
- }}
40
- src={item.commonParams.url}
41
- />
49
+ {item.commonParams.hasGLEffect ? (
50
+ <canvas
51
+ style={inlineStyles}
52
+ ref={fxCanvas}
53
+ className="img-canvas"
54
+ width={rectWidth}
55
+ height={rectHeight}
56
+ />
57
+ ) : (
58
+ <img
59
+ alt=""
60
+ className={`image image-${item.id}`}
61
+ style={inlineStyles}
62
+ src={item.commonParams.url}
63
+ />
64
+ )}
65
+
42
66
  </div>
43
67
  <JSXStyle id={id}>{`
44
68
  @supports not (color: oklch(42% 0.3 90 / 1)) {
@@ -61,6 +85,15 @@ export const ImageItem: FC<ItemProps<TImageItem>> = ({ item, sectionId, onResize
61
85
  pointer-events: none;
62
86
  border-style: solid;
63
87
  overflow: hidden;
88
+ box-sizing: border-box;
89
+ }
90
+ .img-canvas {
91
+ border: solid;
92
+ width: 100%;
93
+ height: 100%;
94
+ pointer-events: none;
95
+ border-width: 0;
96
+ box-sizing: border-box;
64
97
  }
65
98
  ${getLayoutStyles(layouts, [item.state.hover], ([hoverParams]) => {
66
99
  return (`
@@ -1,6 +1,7 @@
1
1
  import { KeyframeType, KeyframeValueMap } from '@cntrl-site/sdk';
2
2
  import { CntrlColor } from '@cntrl-site/color';
3
3
  import { binSearchInsertAt, createInsert } from '../binSearchInsertAt';
4
+ import { rangeMap } from '../rangeMap';
4
5
 
5
6
  export interface AnimationData<T extends KeyframeType> {
6
7
  position: number;
@@ -348,17 +349,3 @@ function createKeyframesMap(): KeyframesMap {
348
349
  [KeyframeType.TextColor]: []
349
350
  };
350
351
  }
351
-
352
- function rangeMap(
353
- n: number,
354
- start1: number,
355
- stop1: number,
356
- start2: number,
357
- stop2: number,
358
- withinBounds: boolean = false
359
- ): number {
360
- const mapped = (n - start1) / (stop1 - start1) * (stop2 - start2) + start2;
361
- if (withinBounds && n < start1) return start2;
362
- if (withinBounds && n > stop1) return stop2;
363
- return mapped;
364
- }
@@ -0,0 +1,80 @@
1
+ import { useEffect, useMemo, useRef } from 'react';
2
+ import { ImageEffect } from '@cntrl-site/effects';
3
+ import { rangeMap } from '../rangeMap';
4
+
5
+ interface FxParams {
6
+ imageUrl?: string;
7
+ fragmentShader?: string;
8
+ }
9
+
10
+ const PATTERN_URL = 'https://cdn.cntrl.site/client-app-files/texture2.png';
11
+ const PATTERN_2_URL = 'https://cdn.cntrl.site/client-app-files/bayer16.png';
12
+
13
+ export function useImageFx(
14
+ canvas: HTMLCanvasElement | null | undefined,
15
+ enabled: boolean,
16
+ {
17
+ imageUrl,
18
+ fragmentShader
19
+ }: FxParams
20
+ ): void {
21
+ const cursor = useRef<[number, number]>([0, 0]);
22
+ const imageFx = useMemo<ImageEffect | undefined>(() => {
23
+ if (!imageUrl) return undefined;
24
+ return new ImageEffect(
25
+ imageUrl,
26
+ PATTERN_URL,
27
+ PATTERN_2_URL,
28
+ fragmentShader!,
29
+ { time: 0, cursor: cursor.current });
30
+ }, [imageUrl, fragmentShader]);
31
+
32
+ useEffect(() => {
33
+ if (!canvas || !imageFx) return;
34
+ const handleMouseMove = (evt: MouseEvent) => {
35
+ const rect = canvas.getBoundingClientRect();
36
+ const x = rangeMap(evt.clientX, rect.left, rect.left + rect.width, 0, 1);
37
+ const y = rangeMap(evt.clientY, rect.top, rect.top + rect.height, 0, 1);
38
+ imageFx.setParam('cursor', [x, y]);
39
+ };
40
+ window.addEventListener('mousemove', handleMouseMove, { capture: true, passive: true });
41
+ return () => {
42
+ window.removeEventListener('mousemove', handleMouseMove);
43
+ };
44
+ }, [canvas, imageFx]);
45
+
46
+ useEffect(() => {
47
+ const gl = canvas?.getContext('webgl2');
48
+ if (!enabled || !canvas || !gl || !imageFx) return;
49
+ let running = false;
50
+ let time = 0;
51
+ let frame: number;
52
+
53
+ const renderFrame = () => {
54
+ time += 0.1;
55
+ imageFx.setViewport(Math.floor(canvas.width), Math.floor(canvas.height));
56
+ imageFx.setParam('time', time);
57
+ imageFx.render(gl);
58
+ frame = requestAnimationFrame(renderFrame);
59
+ };
60
+
61
+ const observer = new IntersectionObserver(([entry]) => {
62
+ if (entry.isIntersecting && !running) {
63
+ frame = requestAnimationFrame(renderFrame);
64
+ running = true;
65
+ return;
66
+ }
67
+ if (!entry.isIntersecting && running) {
68
+ window.cancelAnimationFrame(frame);
69
+ running = false;
70
+ }
71
+ });
72
+ observer.observe(canvas);
73
+ imageFx.prepare(gl);
74
+
75
+ return () => {
76
+ window.cancelAnimationFrame(frame);
77
+ observer.disconnect();
78
+ };
79
+ }, [canvas, imageFx, enabled]);
80
+ }
@@ -0,0 +1,13 @@
1
+ export function rangeMap(
2
+ n: number,
3
+ start1: number,
4
+ stop1: number,
5
+ start2: number,
6
+ stop2: number,
7
+ withinBounds: boolean = false
8
+ ): number {
9
+ const mapped = (n - start1) / (stop1 - start1) * (stop2 - start2) + start2;
10
+ if (withinBounds && n < start1) return start2;
11
+ if (withinBounds && n > stop1) return stop2;
12
+ return mapped;
13
+ }
@@ -0,0 +1,23 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ export function useElementRect(element: HTMLElement | null): DOMRect | undefined {
4
+ const [rect, setRect] = useState<DOMRect | undefined>(() => element?.getBoundingClientRect());
5
+
6
+ useEffect(() => {
7
+ if (!element) return;
8
+ const observer = new ResizeObserver(entries => {
9
+ const el = entries.find(entry => entry.target === element);
10
+ if (!el) {
11
+ throw new Error('Element is not found in observed entries');
12
+ }
13
+ setRect(el.target.getBoundingClientRect());
14
+ });
15
+ observer.observe(element);
16
+ return () => {
17
+ observer.unobserve(element);
18
+ observer.disconnect();
19
+ };
20
+ }, [element]);
21
+
22
+ return rect;
23
+ }