Skip to content

Commit 675e859

Browse files
authored
feat: support HTMLCanvasElement for input (#81)
* feat: accept HTMLCanvasElement as the input * feat: support HTMLCanvasElement in VFX.update() * docs: add a canvas example * chore: adjust shader * chore: perf * chore: comment
1 parent 56306c0 commit 675e859

File tree

6 files changed

+174
-4
lines changed

6 files changed

+174
-4
lines changed

packages/docs-vfx-js/index.css

+5-1
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,6 @@ a:visited {
207207
gap: 16px;
208208
overflow: hidden;
209209
}
210-
211210
#div p {
212211
margin: 0;
213212
}
@@ -221,6 +220,11 @@ a:visited {
221220
resize: vertical;
222221
}
223222

223+
#canvas {
224+
width: 100%;
225+
aspect-ratio: 4 / 3;
226+
}
227+
224228
/*================ AuthorSection ================*/
225229

226230
.AuthorSection {

packages/docs-vfx-js/index.html

+35
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,41 @@ <h2>Examples</h2>
256256
</div>
257257
</div>
258258
</div>
259+
260+
<div class="row">
261+
<div class="col">
262+
<p>Canvas</p>
263+
<pre><code class="language-html">
264+
&lt;!--
265+
VFX-JS also supports HTMLCanvasElement as the input.
266+
You can draw 2D graphics and text in canvas,
267+
then pass it to VFX-JS to add post effects.
268+
--&gt;
269+
&lt;canvas id="canvas"/&gt;
270+
</code></pre>
271+
272+
<pre><code class="language-javascript">
273+
const canvas = document.getElementById("canvas");
274+
const ctx = canvas.getContext("2d");
275+
276+
function drawCanvas() {
277+
...
278+
279+
// Update texture when the canvas has been updated
280+
vfx.update(canvas);
281+
282+
requestAnimationFrame(drawCanvas);
283+
}
284+
drawCanvas();
285+
286+
vfx.add(canvas, { shader });
287+
</code></pre>
288+
</div>
289+
<div class="col">
290+
<p>Output:</p>
291+
<canvas id="canvas" />
292+
</div>
293+
</div>
259294
</section>
260295

261296
<div>

packages/docs-vfx-js/src/main.ts

+110
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import "prism-themes/themes/prism-nord.min.css";
66
Prism.manual = true;
77
Prism.highlightAll();
88

9+
function lerp(a: number, b: number, t: number) {
10+
return a * (1 - t) + b * t;
11+
}
12+
913
const shaders: Record<string, string> = {
1014
logo: `
1115
precision highp float;
@@ -142,6 +146,32 @@ const shaders: Record<string, string> = {
142146
img.a *= 0.5;
143147
gl_FragColor = img;
144148
}
149+
`,
150+
canvas: `
151+
precision highp float;
152+
uniform vec2 resolution;
153+
uniform vec2 offset;
154+
uniform float time;
155+
uniform sampler2D src;
156+
157+
#define ZOOM(uv, x) ((uv - .5) / x + .5)
158+
159+
void main (void) {
160+
vec2 uv = (gl_FragCoord.xy - offset) / resolution;
161+
162+
float r = sin(time) * 0.5 + 0.5;
163+
164+
float l = pow(length(uv - .5), 2.);
165+
uv = (uv - .5) * (1. - l * 0.3 * r) + .5;
166+
167+
168+
float n = 0.02 + r * 0.03;
169+
vec4 cr = texture2D(src, ZOOM(uv, 1.00));
170+
vec4 cg = texture2D(src, ZOOM(uv, (1. + n)));
171+
vec4 cb = texture2D(src, ZOOM(uv, (1. + n * 2.)));
172+
173+
gl_FragColor = vec4(cr.r, cg.g, cb.b, 1);
174+
}
145175
`,
146176
custom: `
147177
precision highp float;
@@ -218,6 +248,85 @@ class App {
218248
});
219249
}
220250

251+
initCanvas() {
252+
const canvas = document.getElementById("canvas") as HTMLCanvasElement;
253+
const ctx = canvas.getContext("2d")!;
254+
const { width, height } = canvas.getBoundingClientRect();
255+
const ratio = window.devicePixelRatio ?? 1;
256+
canvas.width = width * ratio;
257+
canvas.height = height * ratio;
258+
ctx.scale(ratio, ratio);
259+
260+
let target = [width / 2, height / 2];
261+
let p = target;
262+
const ps = [p];
263+
let isMouseOn = false;
264+
const startTime = Date.now();
265+
266+
canvas.addEventListener("mousemove", (e) => {
267+
isMouseOn = true;
268+
target = [e.offsetX, e.offsetY];
269+
});
270+
canvas.addEventListener("mouseleave", (e) => {
271+
isMouseOn = false;
272+
});
273+
274+
let isInside = false;
275+
const io = new IntersectionObserver(
276+
(changes) => {
277+
for (const c of changes) {
278+
isInside = c.intersectionRatio > 0.1;
279+
}
280+
},
281+
{ threshold: [0, 1, 0.2, 0.8] },
282+
);
283+
io.observe(canvas);
284+
285+
const drawMouseStalker = () => {
286+
requestAnimationFrame(drawMouseStalker);
287+
288+
if (!isInside) {
289+
return;
290+
}
291+
292+
if (!isMouseOn) {
293+
const t = Date.now() / 1000 - startTime;
294+
target = [
295+
width * 0.5 + Math.sin(t * 1.3) * width * 0.3,
296+
height * 0.5 + Math.sin(t * 1.7) * height * 0.3,
297+
];
298+
}
299+
p = [lerp(p[0], target[0], 0.1), lerp(p[1], target[1], 0.1)];
300+
301+
ps.push(p);
302+
ps.splice(0, ps.length - 30);
303+
304+
ctx.clearRect(0, 0, width, height);
305+
ctx.fillStyle = "black";
306+
ctx.fillRect(0, 0, width, height);
307+
308+
ctx.fillStyle = "white";
309+
ctx.font = `bold ${width * 0.14}px sans-serif`;
310+
ctx.fillText("HOVER ME", width / 2, height / 2);
311+
ctx.textBaseline = "middle";
312+
ctx.textAlign = "center";
313+
314+
for (let i = 0; i < ps.length; i++) {
315+
const [x, y] = ps[i];
316+
const t = (i / ps.length) * 255;
317+
ctx.fillStyle = `rgba(${255 - t}, 255, ${t}, ${(i / ps.length) * 0.5 + 0.5})`;
318+
ctx.beginPath();
319+
ctx.arc(x, y, i + 20, 0, 2 * Math.PI);
320+
ctx.fill();
321+
}
322+
323+
this.vfx.update(canvas);
324+
};
325+
drawMouseStalker();
326+
327+
this.vfx.add(canvas, { shader: shaders.canvas });
328+
}
329+
221330
initCustomShader() {
222331
const e = document.getElementById("custom")!;
223332
this.vfx.add(e, {
@@ -265,6 +374,7 @@ window.addEventListener("load", () => {
265374
app.initBG();
266375
app.initVFX();
267376
app.initDiv();
377+
app.initCanvas();
268378
app.initCustomShader();
269379
app.hideMask();
270380
setTimeout(() => {

packages/vfx-js/src/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ export type VFXUniformValue =
165165
/**
166166
* @internal
167167
*/
168-
export type VFXElementType = "img" | "video" | "text";
168+
export type VFXElementType = "img" | "video" | "text" | "canvas";
169169

170170
/**
171171
* @internal

packages/vfx-js/src/vfx-player.ts

+10
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,9 @@ export class VFXPlayer {
202202
} else if (element instanceof HTMLVideoElement) {
203203
texture = new THREE.VideoTexture(element);
204204
type = "video" as VFXElementType;
205+
} else if (element instanceof HTMLCanvasElement) {
206+
texture = new THREE.CanvasTexture(element);
207+
type = "canvas" as VFXElementType;
205208
} else {
206209
const canvas = await dom2canvas(element, originalOpacity);
207210
texture = new THREE.CanvasTexture(canvas);
@@ -315,6 +318,13 @@ export class VFXPlayer {
315318
return Promise.resolve();
316319
}
317320

321+
updateCanvasElement(element: HTMLCanvasElement): void {
322+
const e = this.#elements.find((e) => e.element === element);
323+
if (e) {
324+
e.uniforms["src"].value.needsUpdate = true;
325+
}
326+
}
327+
318328
isPlaying(): boolean {
319329
return this.#playRequest !== undefined;
320330
}

packages/vfx-js/src/vfx.ts

+13-2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ export class VFX {
4444
this.#addImage(element, opts);
4545
} else if (element instanceof HTMLVideoElement) {
4646
this.#addVideo(element, opts);
47+
} else if (element instanceof HTMLCanvasElement) {
48+
this.#addCanvas(element, opts);
4749
} else {
4850
this.#addText(element, opts);
4951
}
@@ -64,8 +66,13 @@ export class VFX {
6466
*
6567
* This is useful to apply effects to eleents whose contents change dynamically (e.g. input, textare etc).
6668
*/
67-
update(element: HTMLElement): Promise<void> {
68-
return this.#player.updateTextElement(element);
69+
async update(element: HTMLElement): Promise<void> {
70+
if (element instanceof HTMLCanvasElement) {
71+
this.#player.updateCanvasElement(element);
72+
return;
73+
} else {
74+
return this.#player.updateTextElement(element);
75+
}
6976
}
7077

7178
/**
@@ -119,6 +126,10 @@ export class VFX {
119126
}
120127
}
121128

129+
#addCanvas(element: HTMLCanvasElement, opts: VFXProps): void {
130+
this.#player.addElement(element, opts);
131+
}
132+
122133
#addText(element: HTMLElement, opts: VFXProps): void {
123134
this.#player.addElement(element, opts);
124135
}

0 commit comments

Comments
 (0)