react-shadertoy 0.2.0 → 0.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/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +148 -41
- package/dist/index.mjs +148 -41
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { CSSProperties, RefObject } from 'react';
|
|
3
3
|
|
|
4
|
-
/** Texture source: URL string for
|
|
5
|
-
type TextureSource = string;
|
|
4
|
+
/** Texture source: URL string, or an HTML element for dynamic textures */
|
|
5
|
+
type TextureSource = string | HTMLImageElement | HTMLVideoElement | HTMLCanvasElement;
|
|
6
6
|
/** Texture inputs mapped to Shadertoy channels */
|
|
7
7
|
type TextureInputs = {
|
|
8
8
|
iChannel0?: TextureSource;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { CSSProperties, RefObject } from 'react';
|
|
3
3
|
|
|
4
|
-
/** Texture source: URL string for
|
|
5
|
-
type TextureSource = string;
|
|
4
|
+
/** Texture source: URL string, or an HTML element for dynamic textures */
|
|
5
|
+
type TextureSource = string | HTMLImageElement | HTMLVideoElement | HTMLCanvasElement;
|
|
6
6
|
/** Texture inputs mapped to Shadertoy channels */
|
|
7
7
|
type TextureInputs = {
|
|
8
8
|
iChannel0?: TextureSource;
|
package/dist/index.js
CHANGED
|
@@ -153,50 +153,159 @@ function dispose(state) {
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
// src/textures.ts
|
|
156
|
-
function
|
|
156
|
+
function isPOT(v) {
|
|
157
|
+
return (v & v - 1) === 0 && v > 0;
|
|
158
|
+
}
|
|
159
|
+
function initTexture(gl, unit) {
|
|
157
160
|
const texture = gl.createTexture();
|
|
158
161
|
gl.activeTexture(gl.TEXTURE0 + unit);
|
|
159
162
|
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
160
|
-
gl.texImage2D(
|
|
161
|
-
gl.TEXTURE_2D,
|
|
162
|
-
0,
|
|
163
|
-
gl.RGBA,
|
|
164
|
-
1,
|
|
165
|
-
1,
|
|
166
|
-
0,
|
|
167
|
-
gl.RGBA,
|
|
168
|
-
gl.UNSIGNED_BYTE,
|
|
169
|
-
new Uint8Array([255, 0, 255, 255])
|
|
170
|
-
);
|
|
171
163
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
172
164
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
173
165
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
174
166
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
167
|
+
return texture;
|
|
168
|
+
}
|
|
169
|
+
function uploadElement(gl, texture, unit, el) {
|
|
170
|
+
gl.activeTexture(gl.TEXTURE0 + unit);
|
|
171
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
172
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, el);
|
|
173
|
+
}
|
|
174
|
+
function createTexture(gl, source, unit) {
|
|
175
|
+
const texture = initTexture(gl, unit);
|
|
176
|
+
if (typeof source === "string") {
|
|
177
|
+
gl.texImage2D(
|
|
178
|
+
gl.TEXTURE_2D,
|
|
179
|
+
0,
|
|
180
|
+
gl.RGBA,
|
|
181
|
+
1,
|
|
182
|
+
1,
|
|
183
|
+
0,
|
|
184
|
+
gl.RGBA,
|
|
185
|
+
gl.UNSIGNED_BYTE,
|
|
186
|
+
new Uint8Array([255, 0, 255, 255])
|
|
187
|
+
);
|
|
188
|
+
const state2 = {
|
|
189
|
+
texture,
|
|
190
|
+
width: 1,
|
|
191
|
+
height: 1,
|
|
192
|
+
unit,
|
|
193
|
+
loaded: false,
|
|
194
|
+
needsUpdate: false,
|
|
195
|
+
source
|
|
196
|
+
};
|
|
197
|
+
const promise = new Promise((resolve, reject) => {
|
|
198
|
+
const img = new Image();
|
|
199
|
+
img.crossOrigin = "anonymous";
|
|
200
|
+
img.onload = () => {
|
|
201
|
+
if (gl.isContextLost()) {
|
|
202
|
+
resolve();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
uploadElement(gl, texture, unit, img);
|
|
206
|
+
if (isPOT(img.width) && isPOT(img.height)) {
|
|
207
|
+
gl.generateMipmap(gl.TEXTURE_2D);
|
|
208
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
|
|
209
|
+
}
|
|
210
|
+
state2.width = img.width;
|
|
211
|
+
state2.height = img.height;
|
|
212
|
+
state2.loaded = true;
|
|
181
213
|
resolve();
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
214
|
+
};
|
|
215
|
+
img.onerror = () => reject(new Error(`Failed to load texture: ${source}`));
|
|
216
|
+
img.src = source;
|
|
217
|
+
});
|
|
218
|
+
return { state: state2, promise };
|
|
219
|
+
}
|
|
220
|
+
if (source instanceof HTMLImageElement) {
|
|
221
|
+
const state2 = {
|
|
222
|
+
texture,
|
|
223
|
+
width: source.naturalWidth || 1,
|
|
224
|
+
height: source.naturalHeight || 1,
|
|
225
|
+
unit,
|
|
226
|
+
loaded: source.complete,
|
|
227
|
+
needsUpdate: false,
|
|
228
|
+
source
|
|
195
229
|
};
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
230
|
+
if (source.complete && source.naturalWidth > 0) {
|
|
231
|
+
uploadElement(gl, texture, unit, source);
|
|
232
|
+
state2.width = source.naturalWidth;
|
|
233
|
+
state2.height = source.naturalHeight;
|
|
234
|
+
return { state: state2, promise: null };
|
|
235
|
+
}
|
|
236
|
+
const promise = new Promise((resolve, reject) => {
|
|
237
|
+
source.onload = () => {
|
|
238
|
+
if (gl.isContextLost()) {
|
|
239
|
+
resolve();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
uploadElement(gl, texture, unit, source);
|
|
243
|
+
state2.width = source.naturalWidth;
|
|
244
|
+
state2.height = source.naturalHeight;
|
|
245
|
+
state2.loaded = true;
|
|
246
|
+
resolve();
|
|
247
|
+
};
|
|
248
|
+
source.onerror = () => reject(new Error("Failed to load image element"));
|
|
249
|
+
});
|
|
250
|
+
return { state: state2, promise };
|
|
251
|
+
}
|
|
252
|
+
if (source instanceof HTMLVideoElement) {
|
|
253
|
+
const w = source.videoWidth || 1;
|
|
254
|
+
const h = source.videoHeight || 1;
|
|
255
|
+
if (source.readyState >= 2) {
|
|
256
|
+
uploadElement(gl, texture, unit, source);
|
|
257
|
+
} else {
|
|
258
|
+
gl.texImage2D(
|
|
259
|
+
gl.TEXTURE_2D,
|
|
260
|
+
0,
|
|
261
|
+
gl.RGBA,
|
|
262
|
+
1,
|
|
263
|
+
1,
|
|
264
|
+
0,
|
|
265
|
+
gl.RGBA,
|
|
266
|
+
gl.UNSIGNED_BYTE,
|
|
267
|
+
new Uint8Array([0, 0, 0, 255])
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
const state2 = {
|
|
271
|
+
texture,
|
|
272
|
+
width: w,
|
|
273
|
+
height: h,
|
|
274
|
+
unit,
|
|
275
|
+
loaded: source.readyState >= 2,
|
|
276
|
+
needsUpdate: true,
|
|
277
|
+
source
|
|
278
|
+
};
|
|
279
|
+
return { state: state2, promise: null };
|
|
280
|
+
}
|
|
281
|
+
uploadElement(gl, texture, unit, source);
|
|
282
|
+
const state = {
|
|
283
|
+
texture,
|
|
284
|
+
width: source.width,
|
|
285
|
+
height: source.height,
|
|
286
|
+
unit,
|
|
287
|
+
loaded: true,
|
|
288
|
+
needsUpdate: true,
|
|
289
|
+
source
|
|
290
|
+
};
|
|
291
|
+
return { state, promise: null };
|
|
292
|
+
}
|
|
293
|
+
function updateDynamicTextures(gl, textures) {
|
|
294
|
+
for (const tex of textures) {
|
|
295
|
+
if (!tex || !tex.needsUpdate || !tex.source) continue;
|
|
296
|
+
if (tex.source instanceof HTMLVideoElement) {
|
|
297
|
+
const v = tex.source;
|
|
298
|
+
if (v.readyState < 2) continue;
|
|
299
|
+
uploadElement(gl, tex.texture, tex.unit, v);
|
|
300
|
+
tex.width = v.videoWidth;
|
|
301
|
+
tex.height = v.videoHeight;
|
|
302
|
+
tex.loaded = true;
|
|
303
|
+
} else if (tex.source instanceof HTMLCanvasElement) {
|
|
304
|
+
uploadElement(gl, tex.texture, tex.unit, tex.source);
|
|
305
|
+
tex.width = tex.source.width;
|
|
306
|
+
tex.height = tex.source.height;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
200
309
|
}
|
|
201
310
|
function bindTextures(gl, locations, textures) {
|
|
202
311
|
for (let i = 0; i < 4; i++) {
|
|
@@ -214,9 +323,6 @@ function disposeTextures(gl, textures) {
|
|
|
214
323
|
if (tex) gl.deleteTexture(tex.texture);
|
|
215
324
|
}
|
|
216
325
|
}
|
|
217
|
-
function isPOT(v) {
|
|
218
|
-
return (v & v - 1) === 0 && v > 0;
|
|
219
|
-
}
|
|
220
326
|
|
|
221
327
|
// src/uniforms.ts
|
|
222
328
|
function updateUniforms(state, delta, speed, mouse) {
|
|
@@ -313,10 +419,10 @@ function useShadertoy({
|
|
|
313
419
|
if (texturesProp) {
|
|
314
420
|
for (let i = 0; i < 4; i++) {
|
|
315
421
|
const src = texturesProp[CHANNEL_KEYS[i]];
|
|
316
|
-
if (
|
|
317
|
-
const { state, promise } =
|
|
422
|
+
if (src != null) {
|
|
423
|
+
const { state, promise } = createTexture(result.gl, src, i);
|
|
318
424
|
result.textures[i] = state;
|
|
319
|
-
texturePromises.push(promise);
|
|
425
|
+
if (promise) texturePromises.push(promise);
|
|
320
426
|
}
|
|
321
427
|
}
|
|
322
428
|
}
|
|
@@ -342,6 +448,7 @@ function useShadertoy({
|
|
|
342
448
|
lastTimestamp = timestamp;
|
|
343
449
|
if (!pausedRef.current && rendererRef.current) {
|
|
344
450
|
const r = rendererRef.current;
|
|
451
|
+
updateDynamicTextures(r.gl, r.textures);
|
|
345
452
|
bindTextures(r.gl, r.locations.iChannel, r.textures);
|
|
346
453
|
updateUniforms(r, delta, speedRef.current, mouseState.current);
|
|
347
454
|
render(r);
|
package/dist/index.mjs
CHANGED
|
@@ -126,50 +126,159 @@ function dispose(state) {
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
// src/textures.ts
|
|
129
|
-
function
|
|
129
|
+
function isPOT(v) {
|
|
130
|
+
return (v & v - 1) === 0 && v > 0;
|
|
131
|
+
}
|
|
132
|
+
function initTexture(gl, unit) {
|
|
130
133
|
const texture = gl.createTexture();
|
|
131
134
|
gl.activeTexture(gl.TEXTURE0 + unit);
|
|
132
135
|
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
133
|
-
gl.texImage2D(
|
|
134
|
-
gl.TEXTURE_2D,
|
|
135
|
-
0,
|
|
136
|
-
gl.RGBA,
|
|
137
|
-
1,
|
|
138
|
-
1,
|
|
139
|
-
0,
|
|
140
|
-
gl.RGBA,
|
|
141
|
-
gl.UNSIGNED_BYTE,
|
|
142
|
-
new Uint8Array([255, 0, 255, 255])
|
|
143
|
-
);
|
|
144
136
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
145
137
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
146
138
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
147
139
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
140
|
+
return texture;
|
|
141
|
+
}
|
|
142
|
+
function uploadElement(gl, texture, unit, el) {
|
|
143
|
+
gl.activeTexture(gl.TEXTURE0 + unit);
|
|
144
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
145
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, el);
|
|
146
|
+
}
|
|
147
|
+
function createTexture(gl, source, unit) {
|
|
148
|
+
const texture = initTexture(gl, unit);
|
|
149
|
+
if (typeof source === "string") {
|
|
150
|
+
gl.texImage2D(
|
|
151
|
+
gl.TEXTURE_2D,
|
|
152
|
+
0,
|
|
153
|
+
gl.RGBA,
|
|
154
|
+
1,
|
|
155
|
+
1,
|
|
156
|
+
0,
|
|
157
|
+
gl.RGBA,
|
|
158
|
+
gl.UNSIGNED_BYTE,
|
|
159
|
+
new Uint8Array([255, 0, 255, 255])
|
|
160
|
+
);
|
|
161
|
+
const state2 = {
|
|
162
|
+
texture,
|
|
163
|
+
width: 1,
|
|
164
|
+
height: 1,
|
|
165
|
+
unit,
|
|
166
|
+
loaded: false,
|
|
167
|
+
needsUpdate: false,
|
|
168
|
+
source
|
|
169
|
+
};
|
|
170
|
+
const promise = new Promise((resolve, reject) => {
|
|
171
|
+
const img = new Image();
|
|
172
|
+
img.crossOrigin = "anonymous";
|
|
173
|
+
img.onload = () => {
|
|
174
|
+
if (gl.isContextLost()) {
|
|
175
|
+
resolve();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
uploadElement(gl, texture, unit, img);
|
|
179
|
+
if (isPOT(img.width) && isPOT(img.height)) {
|
|
180
|
+
gl.generateMipmap(gl.TEXTURE_2D);
|
|
181
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
|
|
182
|
+
}
|
|
183
|
+
state2.width = img.width;
|
|
184
|
+
state2.height = img.height;
|
|
185
|
+
state2.loaded = true;
|
|
154
186
|
resolve();
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
187
|
+
};
|
|
188
|
+
img.onerror = () => reject(new Error(`Failed to load texture: ${source}`));
|
|
189
|
+
img.src = source;
|
|
190
|
+
});
|
|
191
|
+
return { state: state2, promise };
|
|
192
|
+
}
|
|
193
|
+
if (source instanceof HTMLImageElement) {
|
|
194
|
+
const state2 = {
|
|
195
|
+
texture,
|
|
196
|
+
width: source.naturalWidth || 1,
|
|
197
|
+
height: source.naturalHeight || 1,
|
|
198
|
+
unit,
|
|
199
|
+
loaded: source.complete,
|
|
200
|
+
needsUpdate: false,
|
|
201
|
+
source
|
|
168
202
|
};
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
203
|
+
if (source.complete && source.naturalWidth > 0) {
|
|
204
|
+
uploadElement(gl, texture, unit, source);
|
|
205
|
+
state2.width = source.naturalWidth;
|
|
206
|
+
state2.height = source.naturalHeight;
|
|
207
|
+
return { state: state2, promise: null };
|
|
208
|
+
}
|
|
209
|
+
const promise = new Promise((resolve, reject) => {
|
|
210
|
+
source.onload = () => {
|
|
211
|
+
if (gl.isContextLost()) {
|
|
212
|
+
resolve();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
uploadElement(gl, texture, unit, source);
|
|
216
|
+
state2.width = source.naturalWidth;
|
|
217
|
+
state2.height = source.naturalHeight;
|
|
218
|
+
state2.loaded = true;
|
|
219
|
+
resolve();
|
|
220
|
+
};
|
|
221
|
+
source.onerror = () => reject(new Error("Failed to load image element"));
|
|
222
|
+
});
|
|
223
|
+
return { state: state2, promise };
|
|
224
|
+
}
|
|
225
|
+
if (source instanceof HTMLVideoElement) {
|
|
226
|
+
const w = source.videoWidth || 1;
|
|
227
|
+
const h = source.videoHeight || 1;
|
|
228
|
+
if (source.readyState >= 2) {
|
|
229
|
+
uploadElement(gl, texture, unit, source);
|
|
230
|
+
} else {
|
|
231
|
+
gl.texImage2D(
|
|
232
|
+
gl.TEXTURE_2D,
|
|
233
|
+
0,
|
|
234
|
+
gl.RGBA,
|
|
235
|
+
1,
|
|
236
|
+
1,
|
|
237
|
+
0,
|
|
238
|
+
gl.RGBA,
|
|
239
|
+
gl.UNSIGNED_BYTE,
|
|
240
|
+
new Uint8Array([0, 0, 0, 255])
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
const state2 = {
|
|
244
|
+
texture,
|
|
245
|
+
width: w,
|
|
246
|
+
height: h,
|
|
247
|
+
unit,
|
|
248
|
+
loaded: source.readyState >= 2,
|
|
249
|
+
needsUpdate: true,
|
|
250
|
+
source
|
|
251
|
+
};
|
|
252
|
+
return { state: state2, promise: null };
|
|
253
|
+
}
|
|
254
|
+
uploadElement(gl, texture, unit, source);
|
|
255
|
+
const state = {
|
|
256
|
+
texture,
|
|
257
|
+
width: source.width,
|
|
258
|
+
height: source.height,
|
|
259
|
+
unit,
|
|
260
|
+
loaded: true,
|
|
261
|
+
needsUpdate: true,
|
|
262
|
+
source
|
|
263
|
+
};
|
|
264
|
+
return { state, promise: null };
|
|
265
|
+
}
|
|
266
|
+
function updateDynamicTextures(gl, textures) {
|
|
267
|
+
for (const tex of textures) {
|
|
268
|
+
if (!tex || !tex.needsUpdate || !tex.source) continue;
|
|
269
|
+
if (tex.source instanceof HTMLVideoElement) {
|
|
270
|
+
const v = tex.source;
|
|
271
|
+
if (v.readyState < 2) continue;
|
|
272
|
+
uploadElement(gl, tex.texture, tex.unit, v);
|
|
273
|
+
tex.width = v.videoWidth;
|
|
274
|
+
tex.height = v.videoHeight;
|
|
275
|
+
tex.loaded = true;
|
|
276
|
+
} else if (tex.source instanceof HTMLCanvasElement) {
|
|
277
|
+
uploadElement(gl, tex.texture, tex.unit, tex.source);
|
|
278
|
+
tex.width = tex.source.width;
|
|
279
|
+
tex.height = tex.source.height;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
173
282
|
}
|
|
174
283
|
function bindTextures(gl, locations, textures) {
|
|
175
284
|
for (let i = 0; i < 4; i++) {
|
|
@@ -187,9 +296,6 @@ function disposeTextures(gl, textures) {
|
|
|
187
296
|
if (tex) gl.deleteTexture(tex.texture);
|
|
188
297
|
}
|
|
189
298
|
}
|
|
190
|
-
function isPOT(v) {
|
|
191
|
-
return (v & v - 1) === 0 && v > 0;
|
|
192
|
-
}
|
|
193
299
|
|
|
194
300
|
// src/uniforms.ts
|
|
195
301
|
function updateUniforms(state, delta, speed, mouse) {
|
|
@@ -286,10 +392,10 @@ function useShadertoy({
|
|
|
286
392
|
if (texturesProp) {
|
|
287
393
|
for (let i = 0; i < 4; i++) {
|
|
288
394
|
const src = texturesProp[CHANNEL_KEYS[i]];
|
|
289
|
-
if (
|
|
290
|
-
const { state, promise } =
|
|
395
|
+
if (src != null) {
|
|
396
|
+
const { state, promise } = createTexture(result.gl, src, i);
|
|
291
397
|
result.textures[i] = state;
|
|
292
|
-
texturePromises.push(promise);
|
|
398
|
+
if (promise) texturePromises.push(promise);
|
|
293
399
|
}
|
|
294
400
|
}
|
|
295
401
|
}
|
|
@@ -315,6 +421,7 @@ function useShadertoy({
|
|
|
315
421
|
lastTimestamp = timestamp;
|
|
316
422
|
if (!pausedRef.current && rendererRef.current) {
|
|
317
423
|
const r = rendererRef.current;
|
|
424
|
+
updateDynamicTextures(r.gl, r.textures);
|
|
318
425
|
bindTextures(r.gl, r.locations.iChannel, r.textures);
|
|
319
426
|
updateUniforms(r, delta, speedRef.current, mouseState.current);
|
|
320
427
|
render(r);
|