Skip to content

Commit

Permalink
feat: support nested elements and inputs (#55)
Browse files Browse the repository at this point in the history
* feat: support nested elements in dom-to-canvas

* feat: add rerender hook

* fix: remove SVG wrapper to fix position bug

* docs: revise log shader

* Revert "docs: revise log shader"

This reverts commit cae9dbc.

* refactor: tidy InputSection

* feat: save original opacity of the element

* feat: avoid flashing the original element in text rerender

* docs: improve text input example

* docs: update div examples

* perf: reduce DOM traversal

* fix: SVG getting overdrawn and scaled incorrectly

* perf: avoid rendering the same element concurrrently

* feat: use OffscreenCanvas for better quality and performance

* chore: remove unused css

* refactor: revise hooks types

* feat: use MutationObserver to watch element updates

* refactor: move property dec

* refactor: prefer # over private

* chore: remove unused

* docs: update Div section desc
  • Loading branch information
fand authored Jun 12, 2024
1 parent 10a95b7 commit 92d3ac8
Show file tree
Hide file tree
Showing 11 changed files with 414 additions and 185 deletions.
36 changes: 36 additions & 0 deletions packages/docs/src/dom/DivSection.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/* .DivSection {
text-align: left;
} */

.DivSection h2 {
font-style: italic;
overflow: visible;
text-align: center;
}

.DivSections {
width: 720px;
max-width: 90%;
margin: 0 auto;
}
.DivSectionField {
text-align: left;
display: flex;
flex-direction: column;
margin-bottom: 1em;
}

.DivSection label {
font-weight: bold;
}

.DivSection input,
.DivSection textarea {
font-size: 1em;
border-radius: 3px;
padding: 2px 10px;
width: 100%;
}
.DivSection textarea {
height: 5em;
}
131 changes: 131 additions & 0 deletions packages/docs/src/dom/DivSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import React, { useState, useCallback, useRef, useEffect } from "react";
import * as VFX from "react-vfx";
import "./DivSection.css";
import { InlineCode } from "./Code";

const shader = `
precision mediump float;
uniform vec2 resolution;
uniform vec2 offset;
uniform float time;
uniform sampler2D src;
uniform float dist;
float noise(float y, float t) {
float n = (
sin(y * .07 + t * 8. + sin(y * .5 + t * 10.)) +
sin(y * .7 + t * 2. + sin(y * .3 + t * 8.)) * .7 +
sin(y * 1.1 + t * 2.8) * .4
);
n += sin(y * 124. + t * 100.7) * sin(y * 877. - t * 38.8) * .3;
return n;
}
void main (void) {
vec2 uv = (gl_FragCoord.xy - offset) / resolution;
float t = mod(time, 30.);
float amp = (3. + dist * 30.) / resolution.x;
vec2 uvr = uv, uvg = uv, uvb = uv;
if (abs(noise(uv.y, t)) > 1. || dist > 0.03) {
uvr.x += noise(uv.y, t) * amp;
uvg.x += noise(uv.y, t + 10.) * amp;
uvb.x += noise(uv.y, t + 20.) * amp;
}
vec4 cr = texture2D(src, uvr);
vec4 cg = texture2D(src, uvg);
vec4 cb = texture2D(src, uvb);
gl_FragColor = vec4(
cr.r,
cg.g,
cb.b,
step(.1, cr.a + cg.a + cb.a)
);
}
`;

const DivSection: React.FC = () => {
const divRef = useRef<HTMLDivElement>(null);
const { rerenderElement } = VFX.useVFX();

const distRef = useRef(0);

useEffect(() => {
let isMounted = true;
const decay = () => {
distRef.current *= 0.8;
if (isMounted) {
requestAnimationFrame(decay);
}
};
decay();

return () => {
isMounted = false;
};
});

const onChange = () => {
rerenderElement(divRef.current);
distRef.current = 1;
};

return (
<section className="DivSection">
<h3>Div (experimental)</h3>
<p>
REACT-VFX also has <InlineCode>VFXDiv</InlineCode>, which allow
us to wrap any elements...
<br />
so you can make an interactive form with WebGL effects!!
</p>
<VFX.VFXDiv
shader={shader}
ref={divRef}
uniforms={{
dist: () => distRef.current,
}}
>
<div className="DivSections">
<div className="DivSectionField">
<label htmlFor="DivInput">Input (type="text")</label>
<input
id="DivInput"
type="text"
defaultValue="You can edit me!"
onChange={onChange}
/>
</div>

<div className="DivSectionField">
<label htmlFor="DivInputRange">
Input (type="range")
</label>
<input
id="DivInputRange"
type="range"
min="0"
max="100"
defaultValue="0"
onChange={onChange}
/>
</div>

<div className="DivSectionField">
<label htmlFor="DivTextArea">Textarea</label>
<textarea
id="DivTextArea"
onChange={onChange}
defaultValue="You can even resize me!"
/>
</div>
</div>
</VFX.VFXDiv>
</section>
);
};

export default DivSection;
6 changes: 2 additions & 4 deletions packages/docs/src/dom/InputSection.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,19 @@
text-align: center;
}

.InputSection textarea {
.InputSection input {
font-size: 1em;
border-radius: 3px;
padding: 2px 10px;
width: 960px;
width: 720px;
max-width: 90%;
}

.InputSection button {
display: block;
font-size: 1em;
color: white;
font-weight: bold;
border-radius: 10px;
padding: 2px;
margin: 40px auto;
cursor: pointer;
}
55 changes: 39 additions & 16 deletions packages/docs/src/dom/InputSection.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,50 @@
import React, { useState, useCallback } from "react";
import * as VFX from "react-vfx";
import "./InputSection.css";
import { Code, InlineCode } from "./Code";
import dedent from "dedent";
import debounce from "lodash.debounce";

const InputSection: React.FC = () => {
const [text, setText] = useState("Try editing text!");
const [debouncedText, setDebouncedText] = useState(text);

const update = useCallback(() => {
setDebouncedText(text);
}, [text]);
const [text, setText] = useState("Edit me!!!");

return (
<section className="InputSection">
<p style={{ fontSize: "48px", fontWeight: "bold" }}>
<VFX.VFXSpan shader="rainbow">{debouncedText}</VFX.VFXSpan>
<section>
<h3>Text</h3>
<p>
Use <InlineCode>{"<VFXSpan>"}</InlineCode> instead of{" "}
<InlineCode>{"<span>"}</InlineCode>.<br />
</p>
<Code>
{dedent`
import { VFXSpan } from 'react-vfx';
<VFXSpan>Hello world!</VFXSpan>
`}
</Code>
<p>
<InlineCode>{"<VFXSpan>"}</InlineCode> automatically re-renders
when its content is updated.
</p>
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
></textarea>
<button type="button" onClick={update}>
<VFX.VFXSpan shader="rainbow">FIRE</VFX.VFXSpan>
</button>

<section className="InputSection">
<p
style={{
fontSize: "48px",
fontWeight: "bold",
}}
>
<VFX.VFXSpan shader="rainbow">
{text === "" ? "Input something..." : text}
</VFX.VFXSpan>
</p>
<input
type="text"
value={text}
placeholder="Input something..."
onChange={(e) => setText(e.target.value)}
/>
</section>
</section>
);
};
Expand Down
25 changes: 3 additions & 22 deletions packages/docs/src/dom/UsageSection.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";
import * as VFX from "react-vfx";
import InputSection from "./InputSection";
import DivSection from "./DivSection";
import dedent from "dedent";
import { Code, InlineCode } from "./Code";

Expand Down Expand Up @@ -169,28 +170,8 @@ const UsageSection: React.VFC = () => (
`}
</Code>
</section>
<section>
<h3>Text</h3>
<p>
Use <InlineCode>{"<VFXSpan>"}</InlineCode> instead of{" "}
<InlineCode>{"<span>"}</InlineCode>.<br />
</p>
<Code>
{dedent`
import { VFXSpan } from 'react-vfx';
<VFXSpan>Hello world!</VFXSpan>
`}
</Code>
<p>
<InlineCode>{"<VFXSpan>"}</InlineCode> automatically
re-renders when its content is updated.
</p>
<InputSection />
<p>
<i>NOTE: VFXSpan doesn't work with nested elements.</i>
</p>
</section>
<InputSection />
<DivSection />
</section>
<section>
<h2 id="custom-shaders">Custom Shaders</h2>
Expand Down
Loading

0 comments on commit 92d3ac8

Please sign in to comment.