docs: add texture mask text catalog entry (#650)

* feat(registry): add texture mask PNGs for texture-mask-text component

* feat(registry): add texture-mask-text CSS snippet

* feat(registry): add registry-item.json for texture-mask-text

* feat(registry): add texture-mask-text demo composition

* feat(registry): register texture-mask-text component in manifest

* style: format texture-mask-text files with oxfmt

* fix: set mask-image directly on texture classes instead of via CSS custom property

url() inside CSS custom properties doesn't resolve correctly with mask-image
in some browsers. Move mask-image declarations to each texture class directly.

* docs: add texture mask text catalog entry

* test: lint texture mask text usage

* fix: harden texture mask text docs and lint

* fix: stabilize texture mask asset paths

* fix: address texture catalog review feedback

* fix: harden texture mask text instructions

* docs: remove texture catalog intro copy

* docs: use canonical texture preview URL

* docs: use cdn texture mask assets

* fix: escape catalog frontmatter safely

* test: stabilize windows render cli test

* test: pin texture catalog instructions
This commit is contained in:
Vance Ingalls
2026-05-07 00:23:36 -07:00
committed by GitHub
parent 73966cd53b
commit edac92b431
85 changed files with 2779 additions and 33 deletions
@@ -0,0 +1,487 @@
---
title: "Texture Mask Text"
---
`text` `texture` `mask` `effect`
<div className="hf-texture-preview-panel">
<div className="hf-texture-preview-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/brick.png')" }}>
<div className="hf-texture-preview-label">Brick</div>
<div className="hf-texture-preview-shadow"><div className="hf-texture-preview-word">BRICK</div></div>
</div>
<div className="hf-texture-preview-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/rock.png')" }}>
<div className="hf-texture-preview-label">Rock</div>
<div className="hf-texture-preview-shadow"><div className="hf-texture-preview-word">ROCK</div></div>
</div>
<div className="hf-texture-preview-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/ground-103.png')" }}>
<div className="hf-texture-preview-label">Ground 103</div>
<div className="hf-texture-preview-shadow"><div className="hf-texture-preview-word">GROUND</div></div>
</div>
<div className="hf-texture-preview-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/wood.png')" }}>
<div className="hf-texture-preview-label">Wood</div>
<div className="hf-texture-preview-shadow"><div className="hf-texture-preview-word">WOOD</div></div>
</div>
<div className="hf-texture-preview-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/metal.png')" }}>
<div className="hf-texture-preview-label">Metal</div>
<div className="hf-texture-preview-shadow"><div className="hf-texture-preview-word">METAL</div></div>
</div>
<div className="hf-texture-preview-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/lava.png')" }}>
<div className="hf-texture-preview-label">Lava</div>
<div className="hf-texture-preview-shadow"><div className="hf-texture-preview-word">LAVA</div></div>
</div>
</div>
## Install
<CodeGroup>
```bash Terminal
npx hyperframes add texture-mask-text
```
</CodeGroup>
## Details
| Property | Value |
| --- | --- |
| Type | Component |
## Agent Usage
Use this wording when asking an agent to apply a texture:
```text
Use the Texture Mask Text catalog component.
1. From the project root, run:
npx hyperframes add texture-mask-text
2. That command creates this installed snippet:
compositions/components/texture-mask-text/texture-mask-text.html
3. Open that file and paste the real <style> block
near the bottom into the composition once. That CSS defines
hf-texture-text and every hf-texture-* class.
4. Apply this class to the target text:
class="hf-texture-text hf-texture-brick"
5. For another material, copy one hf-texture-* class
from the Texture Examples cards.
6. This is the proper way to apply drop shadow
to textured text: wrap the text and put
filter on the wrapper, not on the text.
Use this markup:
<div style="filter: drop-shadow(1px 2px 1px rgba(0,0,0,0.48))">
<div class="hf-texture-text hf-texture-brick">TEXT</div>
</div>
```
After install, the snippet lives at `compositions/components/texture-mask-text/texture-mask-text.html` inside the project where you ran `npx hyperframes add texture-mask-text`. The part to paste is the real `<style>` element near the bottom of that file; the texture PNGs install to `assets/texture-mask-text/masks/` and are referenced by project-root URLs in that CSS.
Swap `hf-texture-brick` for the class shown on any texture card below. The base class `hf-texture-text` is always required.
## Animated Texture
Animate the texture by moving the mask position on the text element. Keep drop shadow on a wrapper so the shadow follows the textured contour.
<div className="hf-texture-animate-demo" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/lava.png')" }}>
<div className="hf-texture-animate-meta">
<div className="hf-texture-animate-label">Animated mask position</div>
<code className="hf-texture-animate-class">hf-texture-text hf-texture-lava</code>
</div>
<div className="hf-texture-animate-shadow">
<div className="hf-texture-animate-word">MOTION</div>
</div>
</div>
```html
<div class="texture-shadow">
<div class="hf-texture-text hf-texture-lava animated-texture">MOTION</div>
</div>
```
```css
.animated-texture {
--mask-size: 180% 180%;
--mask-position: 0% 50%;
}
```
```js
const tl = gsap.timeline({ paused: true });
tl.to(".animated-texture", {
"--mask-position": "100% 50%",
duration: 1.2,
ease: "sine.inOut",
yoyo: true,
repeat: 1,
}, 0);
window.__timelines["my-composition"] = tl;
```
## Texture Examples
<div className="hf-texture-example-groups">
<div>
<h3 className="hf-texture-example-title">Masonry</h3>
<div className="hf-texture-example-grid">
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/brick.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Brick</div><code className="hf-texture-example-class">hf-texture-brick</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">BRICK</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-brick</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/bricks-104.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Bricks 104</div><code className="hf-texture-example-class">hf-texture-bricks-104</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">BRICK</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-bricks-104</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/bricks-102.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Bricks 102</div><code className="hf-texture-example-class">hf-texture-bricks-102</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">BRICK</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-bricks-102</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/bricks-101.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Bricks 101</div><code className="hf-texture-example-class">hf-texture-bricks-101</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">BRICK</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-bricks-101</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/bricks-075-a.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Bricks 075 A</div><code className="hf-texture-example-class">hf-texture-bricks-075-a</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">BRICK</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-bricks-075-a</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/concrete.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Concrete</div><code className="hf-texture-example-class">hf-texture-concrete</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">CONCRETE</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-concrete</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/concrete-034.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Concrete 034</div><code className="hf-texture-example-class">hf-texture-concrete-034</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">CONCRETE</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-concrete-034</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/concrete-047-a.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Concrete 047 A</div><code className="hf-texture-example-class">hf-texture-concrete-047-a</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">CONCRETE</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-concrete-047-a</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/concrete-046.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Concrete 046</div><code className="hf-texture-example-class">hf-texture-concrete-046</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">CONCRETE</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-concrete-046</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/concrete-042-a.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Concrete 042 A</div><code className="hf-texture-example-class">hf-texture-concrete-042-a</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">CONCRETE</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-concrete-042-a</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/plaster-001.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Plaster 001</div><code className="hf-texture-example-class">hf-texture-plaster-001</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">PLASTER</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-plaster-001</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/painted-plaster-017.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Painted Plaster 017</div><code className="hf-texture-example-class">hf-texture-painted-plaster-017</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">PLASTER</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-painted-plaster-017</code></div>
</div>
</div>
</div>
<div>
<h3 className="hf-texture-example-title">Stone</h3>
<div className="hf-texture-example-grid">
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/rock.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Rock</div><code className="hf-texture-example-class">hf-texture-rock</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">ROCK</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-rock</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/rock-063.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Rock 063</div><code className="hf-texture-example-class">hf-texture-rock-063</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">ROCK</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-rock-063</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/rock-058.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Rock 058</div><code className="hf-texture-example-class">hf-texture-rock-058</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">ROCK</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-rock-058</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/onyx.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Onyx</div><code className="hf-texture-example-class">hf-texture-onyx</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">ONYX</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-onyx</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/marble-012.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Marble 012</div><code className="hf-texture-example-class">hf-texture-marble-012</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">MARBLE</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-marble-012</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/marble-016.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Marble 016</div><code className="hf-texture-example-class">hf-texture-marble-016</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">MARBLE</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-marble-016</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/travertine-009.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Travertine 009</div><code className="hf-texture-example-class">hf-texture-travertine-009</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">STONE</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-travertine-009</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/paving-stones-150.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Paving Stones 150</div><code className="hf-texture-example-class">hf-texture-paving-stones-150</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">STONE</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-paving-stones-150</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/paving-stones-138.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Paving Stones 138</div><code className="hf-texture-example-class">hf-texture-paving-stones-138</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">STONE</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-paving-stones-138</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/tiles-138.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Tiles 138</div><code className="hf-texture-example-class">hf-texture-tiles-138</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">TILE</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-tiles-138</code></div>
</div>
</div>
</div>
<div>
<h3 className="hf-texture-example-title">Ground / Road</h3>
<div className="hf-texture-example-grid">
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/ground-103.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Ground 103</div><code className="hf-texture-example-class">hf-texture-ground-103</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">GROUND</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-ground-103</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/ground-037.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Ground 037</div><code className="hf-texture-example-class">hf-texture-ground-037</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">GROUND</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-ground-037</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/ground-054.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Ground 054</div><code className="hf-texture-example-class">hf-texture-ground-054</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">GROUND</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-ground-054</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/ground-104.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Ground 104</div><code className="hf-texture-example-class">hf-texture-ground-104</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">GROUND</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-ground-104</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/ground-068.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Ground 068</div><code className="hf-texture-example-class">hf-texture-ground-068</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">GROUND</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-ground-068</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/ground-080.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Ground 080</div><code className="hf-texture-example-class">hf-texture-ground-080</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">GROUND</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-ground-080</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/road-012-a.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Road 012 A</div><code className="hf-texture-example-class">hf-texture-road-012-a</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">ROAD</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-road-012-a</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/road-008-a.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Road 008 A</div><code className="hf-texture-example-class">hf-texture-road-008-a</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">ROAD</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-road-008-a</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/road-007.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Road 007</div><code className="hf-texture-example-class">hf-texture-road-007</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">ROAD</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-road-007</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/road-013-a.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Road 013 A</div><code className="hf-texture-example-class">hf-texture-road-013-a</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">ROAD</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-road-013-a</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/road-012-b.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Road 012 B</div><code className="hf-texture-example-class">hf-texture-road-012-b</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">ROAD</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-road-012-b</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/road-009-c.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Road 009 C</div><code className="hf-texture-example-class">hf-texture-road-009-c</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">ROAD</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-road-009-c</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/asphalt-031.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Asphalt 031</div><code className="hf-texture-example-class">hf-texture-asphalt-031</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">ASPHALT</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-asphalt-031</code></div>
</div>
</div>
</div>
<div>
<h3 className="hf-texture-example-title">Wood</h3>
<div className="hf-texture-example-grid">
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/wood.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Wood</div><code className="hf-texture-example-class">hf-texture-wood</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">WOOD</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-wood</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/wood-094.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Wood 094</div><code className="hf-texture-example-class">hf-texture-wood-094</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">WOOD</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-wood-094</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/wood-092.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Wood 092</div><code className="hf-texture-example-class">hf-texture-wood-092</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">WOOD</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-wood-092</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/wood-051.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Wood 051</div><code className="hf-texture-example-class">hf-texture-wood-051</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">WOOD</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-wood-051</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/wood-066.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Wood 066</div><code className="hf-texture-example-class">hf-texture-wood-066</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">WOOD</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-wood-066</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/wood-049.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Wood 049</div><code className="hf-texture-example-class">hf-texture-wood-049</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">WOOD</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-wood-049</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/wood-058.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Wood 058</div><code className="hf-texture-example-class">hf-texture-wood-058</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">WOOD</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-wood-058</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/wood-floor-051.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Wood Floor 051</div><code className="hf-texture-example-class">hf-texture-wood-floor-051</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">FLOOR</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-wood-floor-051</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/wood-floor-064.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Wood Floor 064</div><code className="hf-texture-example-class">hf-texture-wood-floor-064</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">FLOOR</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-wood-floor-064</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/wood-floor-070.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Wood Floor 070</div><code className="hf-texture-example-class">hf-texture-wood-floor-070</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">FLOOR</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-wood-floor-070</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/bark-014.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Bark 014</div><code className="hf-texture-example-class">hf-texture-bark-014</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">BARK</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-bark-014</code></div>
</div>
</div>
</div>
<div>
<h3 className="hf-texture-example-title">Metal</h3>
<div className="hf-texture-example-grid">
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/metal.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Metal</div><code className="hf-texture-example-class">hf-texture-metal</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">METAL</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-metal</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/metal-049-a.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Metal 049 A</div><code className="hf-texture-example-class">hf-texture-metal-049-a</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">METAL</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-metal-049-a</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/metal-055-a.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Metal 055 A</div><code className="hf-texture-example-class">hf-texture-metal-055-a</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">METAL</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-metal-055-a</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/metal-046-b.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Metal 046 B</div><code className="hf-texture-example-class">hf-texture-metal-046-b</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">METAL</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-metal-046-b</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/metal-061-b.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Metal 061 B</div><code className="hf-texture-example-class">hf-texture-metal-061-b</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">METAL</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-metal-061-b</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/metal-048-a.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Metal 048 A</div><code className="hf-texture-example-class">hf-texture-metal-048-a</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">METAL</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-metal-048-a</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/metal-032.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Metal 032</div><code className="hf-texture-example-class">hf-texture-metal-032</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">METAL</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-metal-032</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/metal-041-a.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Metal 041 A</div><code className="hf-texture-example-class">hf-texture-metal-041-a</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">METAL</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-metal-041-a</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/metal-038.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Metal 038</div><code className="hf-texture-example-class">hf-texture-metal-038</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">METAL</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-metal-038</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/diamond-plate-009.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Diamond Plate 009</div><code className="hf-texture-example-class">hf-texture-diamond-plate-009</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">PLATE</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-diamond-plate-009</code></div>
</div>
</div>
</div>
<div>
<h3 className="hf-texture-example-title">Organic / Soft</h3>
<div className="hf-texture-example-grid">
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/lava.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Lava</div><code className="hf-texture-example-class">hf-texture-lava</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">LAVA</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-lava</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/grass-005.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Grass 005</div><code className="hf-texture-example-class">hf-texture-grass-005</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">GRASS</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-grass-005</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/grass-001.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Grass 001</div><code className="hf-texture-example-class">hf-texture-grass-001</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">GRASS</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-grass-001</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/grass-004.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Grass 004</div><code className="hf-texture-example-class">hf-texture-grass-004</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">GRASS</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-grass-004</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/carpet.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Carpet</div><code className="hf-texture-example-class">hf-texture-carpet</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">WOVEN</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-carpet</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/fabric-083.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Fabric 083</div><code className="hf-texture-example-class">hf-texture-fabric-083</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">FABRIC</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-fabric-083</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/snow.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Snow</div><code className="hf-texture-example-class">hf-texture-snow</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">SNOW</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-snow</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/snow-015.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Snow 015</div><code className="hf-texture-example-class">hf-texture-snow-015</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">SNOW</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-snow-015</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/leather-037.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Leather 037</div><code className="hf-texture-example-class">hf-texture-leather-037</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">LEATHER</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-leather-037</code></div>
</div>
<div className="hf-texture-example-card" style={{ "--mask-url": "url('https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text/masks/fabric-080.png')" }}>
<div className="hf-texture-example-meta"><div className="hf-texture-example-label">Fabric 080</div><code className="hf-texture-example-class">hf-texture-fabric-080</code></div>
<div className="hf-texture-example-shadow"><div className="hf-texture-example-word">FABRIC</div></div>
<div className="hf-texture-example-usage">Use <code>hf-texture-text hf-texture-fabric-080</code></div>
</div>
</div>
</div>
</div>
## Usage
After `npx hyperframes add texture-mask-text`, the installed snippet lives at `compositions/components/texture-mask-text/texture-mask-text.html` inside your current HyperFrames project. Open that file and paste the real `<style>` element near the bottom into your composition once; it defines `hf-texture-text` and every `hf-texture-*` class used by the examples above. Keep the installed texture PNGs in `assets/texture-mask-text/masks/`; the CSS references them with project-root URLs.
+263
View File
@@ -147,3 +147,266 @@ a:not([class]) {
a:not([class]):hover {
opacity: 0.85;
}
/* Catalog texture examples */
.hf-texture-preview-panel {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
margin: 24px 0 40px;
}
.hf-texture-preview-card {
min-width: 0;
border: 1px solid rgba(127, 127, 127, 0.22);
border-radius: 8px;
padding: 16px;
background: #101010;
}
.hf-texture-preview-label {
margin-bottom: 12px;
color: rgba(255, 255, 255, 0.6);
font-size: 12px;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.hf-texture-preview-shadow {
filter: drop-shadow(1px 2px 1px rgba(0, 0, 0, 0.48));
}
.hf-texture-preview-word {
color: #fff;
font-family: Impact, 'Arial Black', sans-serif;
font-size: 42px;
line-height: 0.9;
letter-spacing: 0;
text-align: center;
text-transform: uppercase;
white-space: nowrap;
-webkit-mask-image: var(--mask-url);
mask-image: var(--mask-url);
-webkit-mask-size: cover;
mask-size: cover;
-webkit-mask-position: center;
mask-position: center;
-webkit-mask-mode: luminance;
mask-mode: luminance;
}
.hf-texture-animate-demo {
border: 1px solid rgba(127, 127, 127, 0.22);
border-radius: 8px;
padding: 22px;
margin: 20px 0 24px;
background:
linear-gradient(90deg, rgba(255, 255, 255, 0.54) 1px, transparent 1px),
#f1f1f1;
background-size: 44px 100%;
}
.hf-texture-animate-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 18px;
}
.hf-texture-animate-label {
min-width: 0;
color: rgba(24, 24, 24, 0.62);
font-size: 13px;
line-height: 1.2;
}
.hf-texture-animate-class {
max-width: 62%;
padding: 2px 6px;
border: 1px solid rgba(24, 24, 24, 0.12);
border-radius: 6px;
background: rgba(255, 255, 255, 0.5);
color: rgba(24, 24, 24, 0.78);
font-size: 11px;
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.hf-texture-animate-shadow {
filter: drop-shadow(1px 2px 1px rgba(0, 0, 0, 0.48));
}
.hf-texture-animate-word {
color: #181818;
font-family: Impact, 'Arial Black', sans-serif;
font-size: 92px;
line-height: 0.9;
letter-spacing: 0;
text-align: center;
text-transform: uppercase;
white-space: nowrap;
-webkit-mask-image: var(--mask-url);
mask-image: var(--mask-url);
-webkit-mask-size: 180% 180%;
mask-size: 180% 180%;
-webkit-mask-position: 0% 50%;
mask-position: 0% 50%;
-webkit-mask-mode: luminance;
mask-mode: luminance;
animation: hf-texture-mask-pan 2.1s ease-in-out infinite alternate;
}
@keyframes hf-texture-mask-pan {
from {
-webkit-mask-position: 0% 50%;
mask-position: 0% 50%;
}
to {
-webkit-mask-position: 100% 50%;
mask-position: 100% 50%;
}
}
.hf-texture-example-groups {
display: grid;
gap: 32px;
margin: 20px 0 40px;
}
.hf-texture-example-title {
margin: 0 0 10px;
font-size: 14px;
font-weight: 600;
}
.hf-texture-example-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(100%, 320px), 1fr));
gap: 16px;
}
.hf-texture-example-card {
border: 1px solid rgba(127, 127, 127, 0.22);
border-radius: 8px;
padding: 20px;
background: #f1f1f1;
}
.hf-texture-example-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 14px;
}
.hf-texture-example-label {
min-width: 0;
color: rgba(24, 24, 24, 0.62);
font-size: 13px;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.hf-texture-example-class {
max-width: 62%;
padding: 2px 6px;
border: 1px solid rgba(24, 24, 24, 0.12);
border-radius: 6px;
background: rgba(255, 255, 255, 0.5);
color: rgba(24, 24, 24, 0.78);
font-size: 11px;
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.hf-texture-example-shadow {
filter: drop-shadow(1px 2px 1px rgba(0, 0, 0, 0.48));
}
.hf-texture-example-word {
color: #181818;
font-family: Impact, 'Arial Black', sans-serif;
font-size: 54px;
line-height: 0.9;
letter-spacing: 0;
text-align: center;
text-transform: uppercase;
white-space: nowrap;
-webkit-mask-image: var(--mask-url);
mask-image: var(--mask-url);
-webkit-mask-size: cover;
mask-size: cover;
-webkit-mask-position: center;
mask-position: center;
-webkit-mask-mode: luminance;
mask-mode: luminance;
}
.hf-texture-example-usage {
display: flex;
align-items: flex-start;
gap: 6px;
min-width: 0;
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid rgba(24, 24, 24, 0.1);
color: rgba(24, 24, 24, 0.56);
font-size: 12px;
line-height: 1.3;
}
.hf-texture-example-usage code {
min-width: 0;
color: rgba(24, 24, 24, 0.82);
font-size: 11px;
line-height: 1.3;
white-space: normal;
overflow-wrap: anywhere;
}
@media (max-width: 860px) {
.hf-texture-preview-panel {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 520px) {
.hf-texture-preview-panel {
grid-template-columns: 1fr;
}
.hf-texture-preview-word {
font-size: 40px;
}
.hf-texture-animate-meta {
align-items: flex-start;
flex-direction: column;
}
.hf-texture-animate-class {
max-width: 100%;
}
.hf-texture-animate-word {
font-size: 56px;
}
}
@media (prefers-reduced-motion: reduce) {
.hf-texture-animate-word {
animation: none;
}
}
+2 -1
View File
@@ -176,7 +176,8 @@
"pages": [
"catalog/components/grain-overlay",
"catalog/components/grid-pixelate-wipe",
"catalog/components/shimmer-sweep"
"catalog/components/shimmer-sweep",
"catalog/components/texture-mask-text"
]
},
{
+14
View File
@@ -348,6 +348,20 @@
"href": "/catalog/blocks/swirl-vortex",
"preview": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/swirl-vortex.png"
},
{
"name": "texture-mask-text",
"type": "component",
"title": "Texture Mask Text",
"description": "CSS luminance masks that cut holes through letterforms - 66 pre-built texture masks from ambientCG PBR color maps",
"tags": [
"text",
"texture",
"mask",
"effect"
],
"href": "/catalog/components/texture-mask-text",
"preview": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/texture-mask-text.png"
},
{
"name": "thermal-distortion",
"type": "block",
+8 -2
View File
@@ -52,6 +52,11 @@ const COMPONENT_ITEM: RegistryItem = {
target: "compositions/components/my-component/my-component.css",
type: "hyperframes:style",
},
{
path: "assets/mask.png",
target: "assets/my-component/mask.png",
type: "hyperframes:asset",
},
],
};
@@ -190,7 +195,7 @@ describe("runAdd (integration, mocked registry)", () => {
}
});
it("remaps component targets per hyperframes.json paths.components", async () => {
it("remaps component snippet/style targets while leaving asset targets stable", async () => {
const dir = tmp();
try {
const baseUrl = uniqueBase();
@@ -206,9 +211,10 @@ describe("runAdd (integration, mocked registry)", () => {
projectDir: dir,
skipClipboard: true,
});
expect(result.written.length).toBe(2);
expect(result.written.length).toBe(3);
expect(existsSync(join(dir, "src/fx/my-component/my-component.html"))).toBe(true);
expect(existsSync(join(dir, "src/fx/my-component/my-component.css"))).toBe(true);
expect(existsSync(join(dir, "assets/my-component/mask.png"))).toBe(true);
expect(result.snippet).toContain("src/fx/my-component/my-component.html");
} finally {
rmSync(dir, { recursive: true, force: true });
+1 -1
View File
@@ -82,7 +82,7 @@ describe("renderLocal browser GPU config", () => {
browserGpuMode: "software",
resolved: true,
});
});
}, 15_000);
it("forwards browserGpuMode='auto' into producer config (probe-then-choose)", async () => {
await renderLocal("/tmp/project", "/tmp/out.mp4", {
+104
View File
@@ -391,6 +391,110 @@ describe("audio_src_not_found", () => {
});
});
describe("texture_mask_asset_not_found", () => {
it("errors when CSS mask-image references a missing local texture", () => {
const html = `<html><body>
<div data-composition-id="main" data-width="1920" data-height="1080">
<div class="hf-texture-text hf-texture-lava">TEXT</div>
</div>
<style>
.hf-texture-lava {
-webkit-mask-image: url("masks/lava.png");
mask-image: url("masks/lava.png");
}
</style>
<script>window.__timelines = window.__timelines || {}; window.__timelines["main"] = gsap.timeline({ paused: true });</script>
</body></html>`;
const project = makeProject(html);
const { totalErrors, results } = lintProject(project);
const finding = results[0]?.result.findings.find(
(item) => item.code === "texture_mask_asset_not_found",
);
expect(totalErrors).toBeGreaterThan(0);
expect(finding).toBeDefined();
expect(finding?.severity).toBe("error");
expect(finding?.message).toContain("masks/lava.png");
});
it("does not error when the referenced texture mask exists", () => {
const html = `<html><body>
<div data-composition-id="main" data-width="1920" data-height="1080">
<div class="hf-texture-text hf-texture-lava">TEXT</div>
</div>
<style>
.hf-texture-lava {
-webkit-mask-image: url("masks/lava.png");
mask-image: url("masks/lava.png");
}
</style>
<script>window.__timelines = window.__timelines || {}; window.__timelines["main"] = gsap.timeline({ paused: true });</script>
</body></html>`;
const project = makeProject(html);
mkdirSync(join(project.dir, "masks"), { recursive: true });
writeFileSync(join(project.dir, "masks", "lava.png"), "fake");
const { results } = lintProject(project);
const finding = results[0]?.result.findings.find(
(item) => item.code === "texture_mask_asset_not_found",
);
expect(finding).toBeUndefined();
});
it("resolves mask-image URLs inside linked sub-composition stylesheets", () => {
const project = makeProject(validHtml(), {
"scene.html": `<html><head><link rel="stylesheet" href="scene.css"></head><body>
<div data-composition-id="scene" data-width="1920" data-height="1080">
<div class="hf-texture-text hf-texture-lava">TEXT</div>
</div>
<script>window.__timelines = window.__timelines || {}; window.__timelines["scene"] = gsap.timeline({ paused: true });</script>
</body></html>`,
});
writeFileSync(
join(project.dir, "compositions", "scene.css"),
'.hf-texture-lava { mask-image: url("masks/lava.png"); }',
);
mkdirSync(join(project.dir, "compositions", "masks"), { recursive: true });
writeFileSync(join(project.dir, "compositions", "masks", "lava.png"), "fake");
const { results } = lintProject(project);
const finding = results[0]?.result.findings.find(
(item) => item.code === "texture_mask_asset_not_found",
);
expect(finding).toBeUndefined();
});
it("resolves root-absolute mask-image URLs from the project root", () => {
const html = `<html><body>
<div data-composition-id="main" data-width="1920" data-height="1080">
<div class="hf-texture-text hf-texture-lava">TEXT</div>
</div>
<style>
.hf-texture-lava {
-webkit-mask-image: url("/assets/texture-mask-text/masks/lava.png");
mask-image: url("/assets/texture-mask-text/masks/lava.png");
}
</style>
<script>window.__timelines = window.__timelines || {}; window.__timelines["main"] = gsap.timeline({ paused: true });</script>
</body></html>`;
const project = makeProject(html);
mkdirSync(join(project.dir, "assets", "texture-mask-text", "masks"), {
recursive: true,
});
writeFileSync(join(project.dir, "assets", "texture-mask-text", "masks", "lava.png"), "fake");
const { results } = lintProject(project);
const finding = results[0]?.result.findings.find(
(item) => item.code === "texture_mask_asset_not_found",
);
expect(finding).toBeUndefined();
});
});
describe("multiple_root_compositions", () => {
it("fires when two HTML files have data-composition-id", () => {
const project = makeProject(validHtml());
+116
View File
@@ -18,6 +18,12 @@ interface HtmlSource {
compSrcPath?: string;
}
interface CssSource {
content: string;
/** Root-relative path to the CSS file. Undefined means inline HTML CSS. */
rootRelativePath?: string;
}
export interface ProjectLintResult {
results: Array<{ file: string; result: HyperframeLintResult }>;
totalErrors: number;
@@ -26,6 +32,16 @@ export interface ProjectLintResult {
}
const AUDIO_EXTENSIONS = new Set([".mp3", ".wav", ".aac", ".ogg", ".m4a", ".flac", ".opus"]);
const STYLE_BLOCK_RE = /<style\b[^>]*>([\s\S]*?)<\/style>/gi;
const OPEN_TAG_RE = /<([a-z][\w:-]*)(\s[^<>]*?)?>/gi;
const MASK_IMAGE_URL_RE =
/\b(?:-webkit-)?mask-image\s*:\s*[^;{}]*url\(\s*(?:"([^"]+)"|'([^']+)'|([^"')\s]+))\s*\)/gi;
function readHtmlAttr(tag: string, name: string): string | null {
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const match = tag.match(new RegExp(`\\b${escaped}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`, "i"));
return match?.[1] ?? match?.[2] ?? null;
}
function isLocalStylesheetHref(href: string): boolean {
return !!href && !/^(https?:|data:|blob:|\/\/)/i.test(href);
@@ -53,6 +69,62 @@ function collectExternalStyles(
return styles;
}
function collectCssSources(projectDir: string, html: string, compSrcPath?: string): CssSource[] {
const sources: CssSource[] = [];
let styleMatch: RegExpExecArray | null;
const stylePattern = new RegExp(STYLE_BLOCK_RE.source, STYLE_BLOCK_RE.flags);
while ((styleMatch = stylePattern.exec(html)) !== null) {
sources.push({ content: styleMatch[1] ?? "" });
}
const linkRe = /<link\b[^>]*>/gi;
let linkMatch: RegExpExecArray | null;
while ((linkMatch = linkRe.exec(html)) !== null) {
const tag = linkMatch[0];
const rel = readHtmlAttr(tag, "rel") ?? "";
if (!rel.split(/\s+/).some((part) => part.toLowerCase() === "stylesheet")) continue;
const href = readHtmlAttr(tag, "href") ?? "";
if (!isLocalStylesheetHref(href)) continue;
const rootRelativePath = compSrcPath ? join(dirname(compSrcPath), href) : href;
const resolved = resolve(projectDir, rootRelativePath);
if (!existsSync(resolved)) continue;
sources.push({ content: readFileSync(resolved, "utf-8"), rootRelativePath });
}
let tagMatch: RegExpExecArray | null;
const tagPattern = new RegExp(OPEN_TAG_RE.source, OPEN_TAG_RE.flags);
while ((tagMatch = tagPattern.exec(html)) !== null) {
const tag = tagMatch[0];
const style = readHtmlAttr(tag, "style");
if (!style) continue;
sources.push({ content: style });
}
return sources;
}
function isRemoteOrInlineUrl(url: string): boolean {
return /^(https?:|data:|blob:|\/\/|#)/i.test(url);
}
function cleanAssetUrl(url: string): string {
return url.trim().split(/[?#]/, 1)[0] ?? "";
}
function resolveCssAssetPath(
projectDir: string,
url: string,
htmlCompSrcPath?: string,
cssRootRelativePath?: string,
): string {
if (url.startsWith("/")) return resolve(projectDir, url.slice(1));
if (cssRootRelativePath) return resolve(projectDir, join(dirname(cssRootRelativePath), url));
if (htmlCompSrcPath) return resolve(projectDir, rewriteAssetPath(htmlCompSrcPath, url));
return resolve(projectDir, url);
}
/**
* Lint the root index.html and all sub-compositions in the compositions/ directory.
* Returns aggregated results across all files.
@@ -101,6 +173,7 @@ export function lintProject(project: ProjectDir): ProjectLintResult {
const projectFindings = [
...lintProjectAudioFiles(project.dir, allHtmlSources),
...lintAudioSrcNotFound(project.dir, allHtmlSources),
...lintTextureMaskAssetNotFound(project.dir, allHtmlSources),
...lintMultipleRootCompositions(project.dir),
...lintDuplicateAudioTracks(allHtmlSources),
];
@@ -215,6 +288,49 @@ function lintAudioSrcNotFound(
return findings;
}
function lintTextureMaskAssetNotFound(
projectDir: string,
htmlSources: HtmlSource[],
): HyperframeLintFinding[] {
const missing = new Map<string, string>();
for (const { html, compSrcPath } of htmlSources) {
for (const cssSource of collectCssSources(projectDir, html, compSrcPath)) {
let match: RegExpExecArray | null;
const pattern = new RegExp(MASK_IMAGE_URL_RE.source, MASK_IMAGE_URL_RE.flags);
while ((match = pattern.exec(cssSource.content)) !== null) {
const rawUrl = match[1] ?? match[2] ?? match[3] ?? "";
const url = cleanAssetUrl(rawUrl);
if (!url || isRemoteOrInlineUrl(url)) continue;
if (/^__[A-Z_]+__$/.test(url)) continue;
const resolved = resolveCssAssetPath(
projectDir,
url,
compSrcPath,
cssSource.rootRelativePath,
);
if (existsSync(resolved)) continue;
missing.set(url, resolved);
}
}
}
if (missing.size === 0) return [];
const urls = [...missing.keys()];
return [
{
code: "texture_mask_asset_not_found",
severity: "error",
message: `CSS mask-image references file(s) not found in the project: ${urls.join(", ")}.`,
fixHint:
urls.length === 1
? `Add "${urls[0]}" to the project, or update the mask-image URL to point to an existing texture mask.`
: "Add the missing texture mask files to the project, or update the mask-image URLs to point to existing files.",
},
];
}
/**
* Error if multiple root-level HTML files with data-composition-id exist.
* Scans the project directory filesystem (not just what lintProject chose to read)
@@ -7,6 +7,7 @@ import { gsapRules } from "./rules/gsap";
import { captionRules } from "./rules/captions";
import { compositionRules } from "./rules/composition";
import { adapterRules } from "./rules/adapters";
import { textureRules } from "./rules/textures";
const ALL_RULES = [
...coreRules,
@@ -15,6 +16,7 @@ const ALL_RULES = [
...captionRules,
...compositionRules,
...adapterRules,
...textureRules,
];
export function lintHyperframeHtml(
@@ -35,6 +35,21 @@ describe("composition rules", () => {
expect(finding).toBeUndefined();
});
it("does not count inline style block internals as structural lines", () => {
const style = `<style>\n${Array.from({ length: 320 }, (_, i) => `.rule-${i} { color: red; }`).join("\n")}\n</style>`;
const html = `<!doctype html>
<html>
<head>${style}</head>
<body>
<div data-composition-id="main" data-start="0" data-duration="1">TEXT</div>
</body>
</html>`;
const result = lintHyperframeHtml(html, { filePath: "/project/index.html" });
const finding = result.findings.find((f) => f.code === "composition_file_too_large");
expect(finding).toBeUndefined();
});
it("does not warn for large registry source block files", () => {
const html = Array.from({ length: 301 }, (_, i) =>
i === 0 ? "<html><body>" : `<!-- filler ${i} -->`,
+5 -1
View File
@@ -16,6 +16,10 @@ function countPhysicalLines(source: string): number {
return withoutFinalNewline.split("\n").length;
}
function countStructuralLines(source: string): number {
return countPhysicalLines(source.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "<style></style>"));
}
function isRegistrySourceFile(filePath?: string): boolean {
if (!filePath) return false;
@@ -38,7 +42,7 @@ export const compositionRules: Array<(ctx: LintContext) => HyperframeLintFinding
({ rawSource, options }) => {
if (isRegistrySourceFile(options.filePath) || isRegistryInstalledFile(rawSource)) return [];
const lineCount = countPhysicalLines(rawSource);
const lineCount = countStructuralLines(rawSource);
if (lineCount <= MAX_COMPOSITION_LINES) return [];
const splitTarget = options.isSubComposition
@@ -0,0 +1,145 @@
import { describe, expect, it } from "vitest";
import { lintHyperframeHtml } from "../hyperframeLinter.js";
function baseHtml(body: string, style = ""): string {
return `<html><body>
<div data-composition-id="main" data-width="1920" data-height="1080">
${body}
</div>
<style>${style}</style>
<script>window.__timelines = window.__timelines || {}; window.__timelines["main"] = gsap.timeline({ paused: true });</script>
</body></html>`;
}
const textureCss = `
.hf-texture-text {
color: #fff;
-webkit-mask-size: var(--mask-size, cover);
mask-size: var(--mask-size, cover);
}
.hf-texture-lava {
-webkit-mask-image: url("masks/lava.png");
mask-image: url("masks/lava.png");
}
`;
describe("texture rules", () => {
it("does not warn for a valid texture mask text usage", () => {
const html = baseHtml(
'<div class="shadow"><div class="hf-texture-text hf-texture-lava">TEXT</div></div>',
`${textureCss}.shadow { filter: drop-shadow(1px 2px 1px rgba(0,0,0,.48)); }`,
);
const result = lintHyperframeHtml(html);
expect(result.findings.filter((finding) => finding.code.startsWith("texture_"))).toEqual([]);
});
it("warns when a material class is used without hf-texture-text", () => {
const html = baseHtml('<div class="hf-texture-lava">TEXT</div>', textureCss);
const result = lintHyperframeHtml(html);
const finding = result.findings.find((item) => item.code === "texture_class_missing_base");
expect(finding).toBeDefined();
expect(finding?.severity).toBe("warning");
expect(finding?.fixHint).toContain("hf-texture-text");
});
it("warns when hf-texture-text has no material class or custom mask image", () => {
const html = baseHtml('<div class="hf-texture-text">TEXT</div>', textureCss);
const result = lintHyperframeHtml(html);
const finding = result.findings.find((item) => item.code === "texture_text_missing_mask");
expect(finding).toBeDefined();
expect(finding?.severity).toBe("warning");
});
it("allows hf-texture-text with an inline custom mask image", () => {
const html = baseHtml(
'<div class="hf-texture-text" style="-webkit-mask-image:url(custom.png); mask-image:url(custom.png)">TEXT</div>',
textureCss,
);
const result = lintHyperframeHtml(html);
const finding = result.findings.find((item) => item.code === "texture_text_missing_mask");
expect(finding).toBeUndefined();
});
it("warns when a texture material class is not defined by local CSS", () => {
const html = baseHtml('<div class="hf-texture-text hf-texture-marbel">TEXT</div>', textureCss);
const result = lintHyperframeHtml(html);
const finding = result.findings.find((item) => item.code === "texture_class_unknown");
expect(finding).toBeDefined();
expect(finding?.message).toContain("hf-texture-marbel");
});
it("warns when drop-shadow is applied inline to the textured text element", () => {
const html = baseHtml(
'<div class="hf-texture-text hf-texture-lava" style="filter: drop-shadow(1px 2px 1px black)">TEXT</div>',
textureCss,
);
const result = lintHyperframeHtml(html);
const finding = result.findings.find((item) => item.code === "texture_drop_shadow_on_text");
expect(finding).toBeDefined();
expect(finding?.fixHint).toContain("wrapper");
});
it("warns when drop-shadow is applied by CSS directly to hf-texture-text", () => {
const html = baseHtml(
'<div class="hf-texture-text hf-texture-lava">TEXT</div>',
`${textureCss}.hf-texture-text { filter: drop-shadow(1px 2px 1px black); }`,
);
const result = lintHyperframeHtml(html);
const finding = result.findings.find((item) => item.code === "texture_drop_shadow_on_text");
expect(finding).toBeDefined();
expect(finding?.selector).toBe(".hf-texture-text");
});
it("warns when drop-shadow targets a material class before the mask rule is declared", () => {
const html = baseHtml(
'<div class="hf-texture-text hf-texture-lava">TEXT</div>',
`.hf-texture-lava { filter: drop-shadow(1px 2px 1px black); }
${textureCss}`,
);
const result = lintHyperframeHtml(html);
const finding = result.findings.find((item) => item.code === "texture_drop_shadow_on_text");
expect(finding).toBeDefined();
expect(finding?.selector).toBe(".hf-texture-lava");
});
it("warns when drop-shadow targets another class on the textured text element", () => {
const html = baseHtml(
'<div class="hf-texture-text hf-texture-lava headline">TEXT</div>',
`${textureCss}.headline { filter: drop-shadow(1px 2px 1px black); }`,
);
const result = lintHyperframeHtml(html);
const finding = result.findings.find((item) => item.code === "texture_drop_shadow_on_text");
expect(finding).toBeDefined();
expect(finding?.selector).toBe(".headline");
});
it("does not warn when another-class drop-shadow selector needs an unmatched ancestor", () => {
const html = baseHtml(
'<div class="hf-texture-text hf-texture-lava headline">TEXT</div>',
`${textureCss}.card .headline { filter: drop-shadow(1px 2px 1px black); }`,
);
const result = lintHyperframeHtml(html);
const finding = result.findings.find((item) => item.code === "texture_drop_shadow_on_text");
expect(finding).toBeUndefined();
});
});
+223
View File
@@ -0,0 +1,223 @@
import postcss from "postcss";
import type { LintContext, HyperframeLintFinding, OpenTag } from "../context";
import { readAttr, truncateSnippet } from "../utils";
const TEXTURE_BASE_CLASS = "hf-texture-text";
const TEXTURE_CLASS_PREFIX = "hf-texture-";
type DropShadowRule = {
selector: string;
directlyTargetsTexture: boolean;
};
function classNames(tag: OpenTag): string[] {
return (readAttr(tag.raw, "class") ?? "").split(/\s+/).filter(Boolean);
}
function isTextureMaterialClass(className: string): boolean {
return className.startsWith(TEXTURE_CLASS_PREFIX) && className !== TEXTURE_BASE_CLASS;
}
function hasInlineMaskImage(tag: OpenTag): boolean {
const style = readAttr(tag.raw, "style") ?? "";
return /\b(?:-webkit-)?mask-image\s*:/i.test(style);
}
function hasInlineDropShadow(tag: OpenTag): boolean {
const style = readAttr(tag.raw, "style") ?? "";
return /\bfilter\s*:\s*[^;]*\bdrop-shadow\s*\(/i.test(style);
}
function classNamesInSelector(selector: string): string[] {
const classes = new Set<string>();
const pattern = /\.([A-Za-z_][\w-]*)/g;
let match: RegExpExecArray | null;
while ((match = pattern.exec(selector)) !== null) {
const className = match[1];
if (!className) continue;
classes.add(className);
}
return [...classes];
}
function textureClassesInSelector(selector: string): string[] {
return classNamesInSelector(selector).filter(isTextureMaterialClass);
}
function simpleSelectorMatchesTag(selector: string, tag: OpenTag, tagClasses: string[]): boolean {
const trimmed = selector.trim();
const simpleSelectorPattern = /^(?:[A-Za-z][\w-]*)?(?:\.[A-Za-z_][\w-]*)+$/;
if (!simpleSelectorPattern.test(trimmed)) return false;
const typeMatch = /^([A-Za-z][\w-]*)/.exec(trimmed);
if (typeMatch && typeMatch[1]!.toLowerCase() !== tag.name) return false;
const selectorClasses = classNamesInSelector(trimmed);
return (
selectorClasses.length > 0 &&
selectorClasses.every((className) => tagClasses.includes(className))
);
}
function collectTextureCss(styles: LintContext["styles"]): {
definedTextureClasses: Set<string>;
dropShadowRules: DropShadowRule[];
} {
const definedTextureClasses = new Set<string>();
const dropShadowRules: DropShadowRule[] = [];
const roots: postcss.Root[] = [];
for (const style of styles) {
let root: postcss.Root;
try {
root = postcss.parse(style.content);
} catch {
continue;
}
roots.push(root);
root.walkRules((rule) => {
const selectors = rule.selectors ?? [];
let hasMaskImage = false;
for (const node of rule.nodes ?? []) {
if (node.type !== "decl") continue;
const prop = node.prop.toLowerCase();
if (prop === "mask-image" || prop === "-webkit-mask-image") hasMaskImage = true;
}
if (hasMaskImage) {
for (const selector of selectors) {
for (const className of textureClassesInSelector(selector)) {
definedTextureClasses.add(className);
}
}
}
});
}
for (const root of roots) {
root.walkRules((rule) => {
const selectors = rule.selectors ?? [];
let hasDropShadow = false;
for (const node of rule.nodes ?? []) {
if (node.type !== "decl") continue;
if (node.prop.toLowerCase() === "filter" && /\bdrop-shadow\s*\(/i.test(node.value)) {
hasDropShadow = true;
}
}
if (hasDropShadow) {
for (const selector of selectors) {
const targetsBaseClass = /\.hf-texture-text\b/.test(selector);
const targetsDefinedTextureClass = textureClassesInSelector(selector).some((className) =>
definedTextureClasses.has(className),
);
dropShadowRules.push({
selector,
directlyTargetsTexture: targetsBaseClass || targetsDefinedTextureClass,
});
}
}
});
}
return { definedTextureClasses, dropShadowRules };
}
export const textureRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
({ tags, styles }) => {
const findings: HyperframeLintFinding[] = [];
const { definedTextureClasses, dropShadowRules } = collectTextureCss(styles);
for (const { selector, directlyTargetsTexture } of dropShadowRules) {
if (!directlyTargetsTexture) continue;
findings.push({
code: "texture_drop_shadow_on_text",
severity: "warning",
message: "Drop shadow is applied directly to textured text.",
selector,
fixHint:
"Wrap the textured text and apply `filter: drop-shadow(...)` to the wrapper, not the `hf-texture-text` element.",
});
}
for (const tag of tags) {
if (tag.name === "style" || tag.name === "script") continue;
const classes = classNames(tag);
if (classes.length === 0) continue;
const hasBaseClass = classes.includes(TEXTURE_BASE_CLASS);
const textureClasses = classes.filter(isTextureMaterialClass);
if (textureClasses.length > 0 && !hasBaseClass) {
findings.push({
code: "texture_class_missing_base",
severity: "warning",
message: `Texture material class \`${textureClasses[0]}\` is used without \`${TEXTURE_BASE_CLASS}\`.`,
elementId: readAttr(tag.raw, "id") || undefined,
fixHint: `Add \`${TEXTURE_BASE_CLASS}\` alongside the material class, for example \`class="${TEXTURE_BASE_CLASS} ${textureClasses[0]}"\`.`,
snippet: truncateSnippet(tag.raw),
});
}
if (hasBaseClass && textureClasses.length === 0 && !hasInlineMaskImage(tag)) {
findings.push({
code: "texture_text_missing_mask",
severity: "warning",
message: `\`${TEXTURE_BASE_CLASS}\` is used without a texture material class or custom mask image.`,
elementId: readAttr(tag.raw, "id") || undefined,
fixHint:
"Add a material class such as `hf-texture-lava`, or set `mask-image` and `-webkit-mask-image` on the element.",
snippet: truncateSnippet(tag.raw),
});
}
for (const textureClass of textureClasses) {
if (definedTextureClasses.has(textureClass)) continue;
findings.push({
code: "texture_class_unknown",
severity: "warning",
message: `Texture material class \`${textureClass}\` is not defined by local CSS.`,
elementId: readAttr(tag.raw, "id") || undefined,
fixHint:
"Paste the Texture Mask Text component `<style>...</style>` block into the composition, or fix the texture class typo.",
snippet: truncateSnippet(tag.raw),
});
}
if (hasBaseClass) {
for (const rule of dropShadowRules) {
if (rule.directlyTargetsTexture) continue;
if (!simpleSelectorMatchesTag(rule.selector, tag, classes)) continue;
findings.push({
code: "texture_drop_shadow_on_text",
severity: "warning",
message: "Drop shadow is applied directly to textured text.",
selector: rule.selector,
elementId: readAttr(tag.raw, "id") || undefined,
fixHint:
"Wrap the textured text and apply `filter: drop-shadow(...)` to the wrapper, not the `hf-texture-text` element.",
snippet: truncateSnippet(tag.raw),
});
}
}
if (hasBaseClass && hasInlineDropShadow(tag)) {
findings.push({
code: "texture_drop_shadow_on_text",
severity: "warning",
message: "Drop shadow is applied directly to textured text.",
elementId: readAttr(tag.raw, "id") || undefined,
fixHint:
"Wrap the textured text and apply `filter: drop-shadow(...)` to the wrapper, not the `hf-texture-text` element.",
snippet: truncateSnippet(tag.raw),
});
}
}
return findings;
},
];
@@ -0,0 +1,18 @@
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
const here = dirname(fileURLToPath(import.meta.url));
const generatorSource = readFileSync(
resolve(here, "../../../../scripts/generate-catalog-pages.ts"),
"utf-8",
);
describe("catalog generator texture instructions", () => {
it("pins the unambiguous texture style-block copy instruction", () => {
expect(generatorSource).toContain("paste the real <style> block");
expect(generatorSource).toContain("near the bottom into the composition once");
expect(generatorSource).toContain("real \\`<style>\\` element near the bottom");
});
});
+286
View File
@@ -0,0 +1,286 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Texture Mask Text - Demo</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: #111;
}
</style>
</head>
<body>
<div
id="demo"
data-composition-id="texture-mask-text-demo"
data-width="1920"
data-height="1080"
data-duration="5"
data-start="0"
>
<div class="demo-canvas">
<div class="header">
<div class="title">Texture Mask Text</div>
<div class="count">66 ambientCG luminance masks</div>
</div>
<div class="groups" id="texture-groups"></div>
</div>
<style>
#demo {
width: 1920px;
height: 1080px;
overflow: hidden;
background: #111;
color: #fff;
font-family:
Arial Black,
Arial,
sans-serif;
}
.hf-texture-text {
color: #fff;
-webkit-mask-size: var(--mask-size, cover);
mask-size: var(--mask-size, cover);
-webkit-mask-position: var(--mask-position, center);
mask-position: var(--mask-position, center);
-webkit-mask-mode: luminance;
mask-mode: luminance;
filter: contrast(var(--mask-contrast, 1));
}
.demo-canvas {
width: 1920px;
height: 1080px;
background: #111;
padding: 42px 44px 38px;
}
.header {
height: 84px;
display: flex;
align-items: flex-start;
justify-content: space-between;
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
margin-bottom: 18px;
}
.title {
font-size: 34px;
line-height: 1;
letter-spacing: 0;
text-transform: uppercase;
}
.count {
font-family: Arial, sans-serif;
font-size: 18px;
line-height: 1;
letter-spacing: 0;
color: rgba(255, 255, 255, 0.55);
text-transform: uppercase;
}
.groups {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(2, 1fr);
gap: 22px 24px;
width: 1832px;
height: 898px;
}
.group-panel {
min-width: 0;
min-height: 0;
overflow: hidden;
}
.group-header {
height: 34px;
display: flex;
align-items: flex-start;
justify-content: space-between;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
margin-bottom: 10px;
}
.group-name {
font-family: Arial, sans-serif;
font-size: 13px;
line-height: 1;
letter-spacing: 0;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.74);
}
.group-count {
font-family: Arial, sans-serif;
font-size: 10px;
line-height: 1;
letter-spacing: 0;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.52);
}
.group-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px 10px;
}
.cell {
min-width: 0;
height: 66px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 9px;
overflow: hidden;
}
.label {
width: 100%;
font-family: Arial, sans-serif;
font-size: 11px;
line-height: 1.1;
letter-spacing: 0;
text-transform: uppercase;
text-align: center;
color: rgba(255, 255, 255, 0.5);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.shadow-wrap {
width: 100%;
filter: drop-shadow(2px 3px 4px rgba(0, 0, 0, 0.46));
}
.sample {
width: 100%;
font-family:
Impact,
Arial Black,
Arial,
sans-serif;
font-size: 31px;
line-height: 0.9;
letter-spacing: 0;
text-transform: uppercase;
text-align: center;
white-space: nowrap;
}
</style>
<script>
const textureMaskTextGroups = [
[
"Masonry",
"brick|Brick|MORTAR,bricks-104|Bricks 104|BRICK,bricks-102|Bricks 102|BRICK,bricks-101|Bricks 101|BRICK,bricks-075-a|Bricks 075 A|BRICK,concrete|Concrete|SCORED,concrete-034|Concrete 034|CONCRETE,concrete-047-a|Concrete 047 A|CONCRETE,concrete-046|Concrete 046|CONCRETE,concrete-042-a|Concrete 042 A|CONCRETE,plaster-001|Plaster 001|PLASTER,painted-plaster-017|Painted Plaster 017|PLASTER",
],
[
"Stone",
"rock|Rock|RUINED,rock-063|Rock 063|ROCK,rock-058|Rock 058|ROCK,onyx|Onyx|VEINED,marble-012|Marble 012|MARBLE,marble-016|Marble 016|MARBLE,travertine-009|Travertine 009|STONE,paving-stones-150|Paving Stones 150|STONE,paving-stones-138|Paving Stones 138|STONE,tiles-138|Tiles 138|TILE",
],
[
"Ground / Road",
"ground-103|Ground 103|GROUND,ground-037|Ground 037|GROUND,ground-054|Ground 054|GROUND,ground-104|Ground 104|GROUND,ground-068|Ground 068|GROUND,ground-080|Ground 080|GROUND,road-012-a|Road 012 A|ROAD,road-008-a|Road 008 A|ROAD,road-007|Road 007|ROAD,road-013-a|Road 013 A|ROAD,road-012-b|Road 012 B|ROAD,road-009-c|Road 009 C|ROAD,asphalt-031|Asphalt 031|ASPHALT",
],
[
"Wood",
"wood|Wood|SLICED,wood-094|Wood 094|WOOD,wood-092|Wood 092|WOOD,wood-051|Wood 051|WOOD,wood-066|Wood 066|WOOD,wood-049|Wood 049|WOOD,wood-058|Wood 058|WOOD,wood-floor-051|Wood Floor 051|FLOOR,wood-floor-064|Wood Floor 064|FLOOR,wood-floor-070|Wood Floor 070|FLOOR,bark-014|Bark 014|BARK",
],
[
"Metal",
"metal|Metal|FADING,metal-049-a|Metal 049 A|METAL,metal-055-a|Metal 055 A|METAL,metal-046-b|Metal 046 B|METAL,metal-061-b|Metal 061 B|METAL,metal-048-a|Metal 048 A|METAL,metal-032|Metal 032|METAL,metal-041-a|Metal 041 A|METAL,metal-038|Metal 038|METAL,diamond-plate-009|Diamond Plate 009|PLATE",
],
[
"Organic / Soft",
"lava|Lava|MOLTEN,grass-005|Grass 005|GRASS,grass-001|Grass 001|GRASS,grass-004|Grass 004|GRASS,carpet|Carpet|WOVEN,fabric-083|Fabric 083|FABRIC,snow|Snow|THAW,snow-015|Snow 015|SNOW,leather-037|Leather 037|LEATHER,fabric-080|Fabric 080|FABRIC",
],
];
const textureGroups = document.getElementById("texture-groups");
for (const [groupName, rows] of textureMaskTextGroups) {
const items = rows.split(",").map((row) => {
const [slug, title, sample] = row.split("|");
return { slug, title, sample };
});
const panel = document.createElement("section");
panel.className = "group-panel";
const groupHeader = document.createElement("div");
groupHeader.className = "group-header";
const name = document.createElement("div");
name.className = "group-name";
name.textContent = groupName;
const count = document.createElement("div");
count.className = "group-count";
count.textContent = `${items.length} masks`;
groupHeader.append(name, count);
const groupGrid = document.createElement("div");
groupGrid.className = "group-grid";
for (const item of items) {
const cell = document.createElement("div");
cell.className = "cell";
const label = document.createElement("div");
label.className = "label";
label.textContent = item.title;
const shadowWrap = document.createElement("div");
shadowWrap.className = "shadow-wrap";
const sample = document.createElement("div");
sample.className = "hf-texture-text sample";
sample.style.webkitMaskImage = `url("masks/${item.slug}.png")`;
sample.style.maskImage = `url("masks/${item.slug}.png")`;
sample.textContent = item.sample;
shadowWrap.appendChild(sample);
cell.append(label, shadowWrap);
groupGrid.appendChild(cell);
}
panel.append(groupHeader, groupGrid);
textureGroups.appendChild(panel);
}
window.__timelines = window.__timelines || {};
const textureMaskTextTimeline = {
play() {},
pause() {},
seek() {},
time() {
return 0;
},
duration() {
return 5;
},
add() {},
paused() {},
set() {},
};
window.__timelines["texture-mask-text-demo"] = textureMaskTextTimeline;
</script>
</div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

@@ -0,0 +1,443 @@
{
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
"name": "texture-mask-text",
"type": "hyperframes:component",
"title": "Texture Mask Text",
"description": "CSS luminance masks that cut holes through letterforms - 66 pre-built texture masks from ambientCG PBR color maps",
"tags": ["text", "texture", "mask", "effect"],
"textureGroups": [
{
"title": "Masonry",
"items": [
"brick",
"bricks-104",
"bricks-102",
"bricks-101",
"bricks-075-a",
"concrete",
"concrete-034",
"concrete-047-a",
"concrete-046",
"concrete-042-a",
"plaster-001",
"painted-plaster-017"
]
},
{
"title": "Stone",
"items": [
"rock",
"rock-063",
"rock-058",
"onyx",
"marble-012",
"marble-016",
"travertine-009",
"paving-stones-150",
"paving-stones-138",
"tiles-138"
]
},
{
"title": "Ground / Road",
"items": [
"ground-103",
"ground-037",
"ground-054",
"ground-104",
"ground-068",
"ground-080",
"road-012-a",
"road-008-a",
"road-007",
"road-013-a",
"road-012-b",
"road-009-c",
"asphalt-031"
]
},
{
"title": "Wood",
"items": [
"wood",
"wood-094",
"wood-092",
"wood-051",
"wood-066",
"wood-049",
"wood-058",
"wood-floor-051",
"wood-floor-064",
"wood-floor-070",
"bark-014"
]
},
{
"title": "Metal",
"items": [
"metal",
"metal-049-a",
"metal-055-a",
"metal-046-b",
"metal-061-b",
"metal-048-a",
"metal-032",
"metal-041-a",
"metal-038",
"diamond-plate-009"
]
},
{
"title": "Organic / Soft",
"items": [
"lava",
"grass-005",
"grass-001",
"grass-004",
"carpet",
"fabric-083",
"snow",
"snow-015",
"leather-037",
"fabric-080"
]
}
],
"files": [
{
"path": "texture-mask-text.html",
"target": "compositions/components/texture-mask-text/texture-mask-text.html",
"type": "hyperframes:snippet"
},
{
"path": "masks/lava.png",
"target": "assets/texture-mask-text/masks/lava.png",
"type": "hyperframes:asset"
},
{
"path": "masks/rock.png",
"target": "assets/texture-mask-text/masks/rock.png",
"type": "hyperframes:asset"
},
{
"path": "masks/concrete.png",
"target": "assets/texture-mask-text/masks/concrete.png",
"type": "hyperframes:asset"
},
{
"path": "masks/brick.png",
"target": "assets/texture-mask-text/masks/brick.png",
"type": "hyperframes:asset"
},
{
"path": "masks/wood.png",
"target": "assets/texture-mask-text/masks/wood.png",
"type": "hyperframes:asset"
},
{
"path": "masks/onyx.png",
"target": "assets/texture-mask-text/masks/onyx.png",
"type": "hyperframes:asset"
},
{
"path": "masks/carpet.png",
"target": "assets/texture-mask-text/masks/carpet.png",
"type": "hyperframes:asset"
},
{
"path": "masks/metal.png",
"target": "assets/texture-mask-text/masks/metal.png",
"type": "hyperframes:asset"
},
{
"path": "masks/snow.png",
"target": "assets/texture-mask-text/masks/snow.png",
"type": "hyperframes:asset"
},
{
"path": "masks/bricks-104.png",
"target": "assets/texture-mask-text/masks/bricks-104.png",
"type": "hyperframes:asset"
},
{
"path": "masks/ground-103.png",
"target": "assets/texture-mask-text/masks/ground-103.png",
"type": "hyperframes:asset"
},
{
"path": "masks/grass-005.png",
"target": "assets/texture-mask-text/masks/grass-005.png",
"type": "hyperframes:asset"
},
{
"path": "masks/rock-063.png",
"target": "assets/texture-mask-text/masks/rock-063.png",
"type": "hyperframes:asset"
},
{
"path": "masks/wood-094.png",
"target": "assets/texture-mask-text/masks/wood-094.png",
"type": "hyperframes:asset"
},
{
"path": "masks/road-012-a.png",
"target": "assets/texture-mask-text/masks/road-012-a.png",
"type": "hyperframes:asset"
},
{
"path": "masks/wood-092.png",
"target": "assets/texture-mask-text/masks/wood-092.png",
"type": "hyperframes:asset"
},
{
"path": "masks/metal-049-a.png",
"target": "assets/texture-mask-text/masks/metal-049-a.png",
"type": "hyperframes:asset"
},
{
"path": "masks/wood-floor-051.png",
"target": "assets/texture-mask-text/masks/wood-floor-051.png",
"type": "hyperframes:asset"
},
{
"path": "masks/paving-stones-150.png",
"target": "assets/texture-mask-text/masks/paving-stones-150.png",
"type": "hyperframes:asset"
},
{
"path": "masks/road-008-a.png",
"target": "assets/texture-mask-text/masks/road-008-a.png",
"type": "hyperframes:asset"
},
{
"path": "masks/road-007.png",
"target": "assets/texture-mask-text/masks/road-007.png",
"type": "hyperframes:asset"
},
{
"path": "masks/metal-055-a.png",
"target": "assets/texture-mask-text/masks/metal-055-a.png",
"type": "hyperframes:asset"
},
{
"path": "masks/concrete-034.png",
"target": "assets/texture-mask-text/masks/concrete-034.png",
"type": "hyperframes:asset"
},
{
"path": "masks/grass-001.png",
"target": "assets/texture-mask-text/masks/grass-001.png",
"type": "hyperframes:asset"
},
{
"path": "masks/wood-051.png",
"target": "assets/texture-mask-text/masks/wood-051.png",
"type": "hyperframes:asset"
},
{
"path": "masks/asphalt-031.png",
"target": "assets/texture-mask-text/masks/asphalt-031.png",
"type": "hyperframes:asset"
},
{
"path": "masks/wood-floor-064.png",
"target": "assets/texture-mask-text/masks/wood-floor-064.png",
"type": "hyperframes:asset"
},
{
"path": "masks/concrete-047-a.png",
"target": "assets/texture-mask-text/masks/concrete-047-a.png",
"type": "hyperframes:asset"
},
{
"path": "masks/metal-046-b.png",
"target": "assets/texture-mask-text/masks/metal-046-b.png",
"type": "hyperframes:asset"
},
{
"path": "masks/grass-004.png",
"target": "assets/texture-mask-text/masks/grass-004.png",
"type": "hyperframes:asset"
},
{
"path": "masks/marble-012.png",
"target": "assets/texture-mask-text/masks/marble-012.png",
"type": "hyperframes:asset"
},
{
"path": "masks/ground-037.png",
"target": "assets/texture-mask-text/masks/ground-037.png",
"type": "hyperframes:asset"
},
{
"path": "masks/tiles-138.png",
"target": "assets/texture-mask-text/masks/tiles-138.png",
"type": "hyperframes:asset"
},
{
"path": "masks/wood-066.png",
"target": "assets/texture-mask-text/masks/wood-066.png",
"type": "hyperframes:asset"
},
{
"path": "masks/concrete-046.png",
"target": "assets/texture-mask-text/masks/concrete-046.png",
"type": "hyperframes:asset"
},
{
"path": "masks/wood-049.png",
"target": "assets/texture-mask-text/masks/wood-049.png",
"type": "hyperframes:asset"
},
{
"path": "masks/road-013-a.png",
"target": "assets/texture-mask-text/masks/road-013-a.png",
"type": "hyperframes:asset"
},
{
"path": "masks/plaster-001.png",
"target": "assets/texture-mask-text/masks/plaster-001.png",
"type": "hyperframes:asset"
},
{
"path": "masks/snow-015.png",
"target": "assets/texture-mask-text/masks/snow-015.png",
"type": "hyperframes:asset"
},
{
"path": "masks/road-012-b.png",
"target": "assets/texture-mask-text/masks/road-012-b.png",
"type": "hyperframes:asset"
},
{
"path": "masks/concrete-042-a.png",
"target": "assets/texture-mask-text/masks/concrete-042-a.png",
"type": "hyperframes:asset"
},
{
"path": "masks/bricks-102.png",
"target": "assets/texture-mask-text/masks/bricks-102.png",
"type": "hyperframes:asset"
},
{
"path": "masks/rock-058.png",
"target": "assets/texture-mask-text/masks/rock-058.png",
"type": "hyperframes:asset"
},
{
"path": "masks/wood-floor-070.png",
"target": "assets/texture-mask-text/masks/wood-floor-070.png",
"type": "hyperframes:asset"
},
{
"path": "masks/bricks-101.png",
"target": "assets/texture-mask-text/masks/bricks-101.png",
"type": "hyperframes:asset"
},
{
"path": "masks/travertine-009.png",
"target": "assets/texture-mask-text/masks/travertine-009.png",
"type": "hyperframes:asset"
},
{
"path": "masks/ground-054.png",
"target": "assets/texture-mask-text/masks/ground-054.png",
"type": "hyperframes:asset"
},
{
"path": "masks/wood-058.png",
"target": "assets/texture-mask-text/masks/wood-058.png",
"type": "hyperframes:asset"
},
{
"path": "masks/ground-104.png",
"target": "assets/texture-mask-text/masks/ground-104.png",
"type": "hyperframes:asset"
},
{
"path": "masks/painted-plaster-017.png",
"target": "assets/texture-mask-text/masks/painted-plaster-017.png",
"type": "hyperframes:asset"
},
{
"path": "masks/paving-stones-138.png",
"target": "assets/texture-mask-text/masks/paving-stones-138.png",
"type": "hyperframes:asset"
},
{
"path": "masks/ground-068.png",
"target": "assets/texture-mask-text/masks/ground-068.png",
"type": "hyperframes:asset"
},
{
"path": "masks/bricks-075-a.png",
"target": "assets/texture-mask-text/masks/bricks-075-a.png",
"type": "hyperframes:asset"
},
{
"path": "masks/ground-080.png",
"target": "assets/texture-mask-text/masks/ground-080.png",
"type": "hyperframes:asset"
},
{
"path": "masks/road-009-c.png",
"target": "assets/texture-mask-text/masks/road-009-c.png",
"type": "hyperframes:asset"
},
{
"path": "masks/bark-014.png",
"target": "assets/texture-mask-text/masks/bark-014.png",
"type": "hyperframes:asset"
},
{
"path": "masks/marble-016.png",
"target": "assets/texture-mask-text/masks/marble-016.png",
"type": "hyperframes:asset"
},
{
"path": "masks/fabric-083.png",
"target": "assets/texture-mask-text/masks/fabric-083.png",
"type": "hyperframes:asset"
},
{
"path": "masks/metal-061-b.png",
"target": "assets/texture-mask-text/masks/metal-061-b.png",
"type": "hyperframes:asset"
},
{
"path": "masks/metal-048-a.png",
"target": "assets/texture-mask-text/masks/metal-048-a.png",
"type": "hyperframes:asset"
},
{
"path": "masks/metal-032.png",
"target": "assets/texture-mask-text/masks/metal-032.png",
"type": "hyperframes:asset"
},
{
"path": "masks/metal-041-a.png",
"target": "assets/texture-mask-text/masks/metal-041-a.png",
"type": "hyperframes:asset"
},
{
"path": "masks/metal-038.png",
"target": "assets/texture-mask-text/masks/metal-038.png",
"type": "hyperframes:asset"
},
{
"path": "masks/diamond-plate-009.png",
"target": "assets/texture-mask-text/masks/diamond-plate-009.png",
"type": "hyperframes:asset"
},
{
"path": "masks/leather-037.png",
"target": "assets/texture-mask-text/masks/leather-037.png",
"type": "hyperframes:asset"
},
{
"path": "masks/fabric-080.png",
"target": "assets/texture-mask-text/masks/fabric-080.png",
"type": "hyperframes:asset"
}
]
}
@@ -0,0 +1,379 @@
<!--
Texture Mask Text - CSS luminance masks that cut holes through letterforms.
Usage: add class="hf-texture-text hf-texture-{name}" to any text element.
Paste the real style element near the bottom of this file into your composition's HTML once.
Texture PNGs install to assets/texture-mask-text/masks; these CSS classes
reference them with project-root URLs so the CSS block can be pasted
into any composition.
Available textures (66 total):
Masonry:
hf-texture-brick - mortar line patterns
hf-texture-bricks-104 - ambientCG Bricks104
hf-texture-bricks-102 - ambientCG Bricks102
hf-texture-bricks-101 - ambientCG Bricks101
hf-texture-bricks-075-a - ambientCG Bricks075A
hf-texture-concrete - streaked channel cuts
hf-texture-concrete-034 - ambientCG Concrete034
hf-texture-concrete-047-a - ambientCG Concrete047A
hf-texture-concrete-046 - ambientCG Concrete046
hf-texture-concrete-042-a - ambientCG Concrete042A
hf-texture-plaster-001 - ambientCG Plaster001
hf-texture-painted-plaster-017 - ambientCG PaintedPlaster017
Stone:
hf-texture-rock - sharp hard cutouts
hf-texture-rock-063 - ambientCG Rock063
hf-texture-rock-058 - ambientCG Rock058
hf-texture-onyx - flowing marble veins
hf-texture-marble-012 - ambientCG Marble012
hf-texture-marble-016 - ambientCG Marble016
hf-texture-travertine-009 - ambientCG Travertine009
hf-texture-paving-stones-150 - ambientCG PavingStones150
hf-texture-paving-stones-138 - ambientCG PavingStones138
hf-texture-tiles-138 - ambientCG Tiles138
Ground / road:
hf-texture-ground-103 - ambientCG Ground103
hf-texture-ground-037 - ambientCG Ground037
hf-texture-ground-054 - ambientCG Ground054
hf-texture-ground-104 - ambientCG Ground104
hf-texture-ground-068 - ambientCG Ground068
hf-texture-ground-080 - ambientCG Ground080
hf-texture-road-012-a - ambientCG Road012A
hf-texture-road-008-a - ambientCG Road008A
hf-texture-road-007 - ambientCG Road007
hf-texture-road-013-a - ambientCG Road013A
hf-texture-road-012-b - ambientCG Road012B
hf-texture-road-009-c - ambientCG Road009C
hf-texture-asphalt-031 - ambientCG Asphalt031
Wood:
hf-texture-wood - horizontal grain slices
hf-texture-wood-094 - ambientCG Wood094
hf-texture-wood-092 - ambientCG Wood092
hf-texture-wood-051 - ambientCG Wood051
hf-texture-wood-066 - ambientCG Wood066
hf-texture-wood-049 - ambientCG Wood049
hf-texture-wood-058 - ambientCG Wood058
hf-texture-wood-floor-051 - ambientCG WoodFloor051
hf-texture-wood-floor-064 - ambientCG WoodFloor064
hf-texture-wood-floor-070 - ambientCG WoodFloor070
hf-texture-bark-014 - ambientCG Bark014
Metal:
hf-texture-metal - rust and pit erosion
hf-texture-metal-049-a - ambientCG Metal049A
hf-texture-metal-055-a - ambientCG Metal055A
hf-texture-metal-046-b - ambientCG Metal046B
hf-texture-metal-061-b - ambientCG Metal061B
hf-texture-metal-048-a - ambientCG Metal048A
hf-texture-metal-032 - ambientCG Metal032
hf-texture-metal-041-a - ambientCG Metal041A
hf-texture-metal-038 - ambientCG Metal038
hf-texture-diamond-plate-009 - ambientCG DiamondPlate009
Organic / soft:
hf-texture-lava - smooth organic erosion
hf-texture-grass-005 - ambientCG Grass005
hf-texture-grass-001 - ambientCG Grass001
hf-texture-grass-004 - ambientCG Grass004
hf-texture-carpet - fine repeating weave
hf-texture-fabric-083 - ambientCG Fabric083
hf-texture-snow - soft patchy dissolve
hf-texture-snow-015 - ambientCG Snow015
hf-texture-leather-037 - ambientCG Leather037
hf-texture-fabric-080 - ambientCG Fabric080
Custom properties:
--mask-size cover CSS mask-size
--mask-position center CSS mask-position
--mask-contrast 1 filter contrast to sharpen/soften edges
Custom texture (set mask-image directly, not via custom property):
<div class="hf-texture-text" style="-webkit-mask-image: url('my-texture.png'); mask-image: url('my-texture.png')">TEXT</div>
Drop shadow (follows masked contour, not bounding box):
<div style="filter: drop-shadow(2px 3px 4px rgba(0,0,0,0.3))">
<div class="hf-texture-text hf-texture-lava">TEXT</div>
</div>
-->
<style>
.hf-texture-text {
color: #fff;
-webkit-mask-size: var(--mask-size, cover);
mask-size: var(--mask-size, cover);
-webkit-mask-position: var(--mask-position, center);
mask-position: var(--mask-position, center);
-webkit-mask-mode: luminance;
mask-mode: luminance;
filter: contrast(var(--mask-contrast, 1));
}
.hf-texture-lava {
-webkit-mask-image: url("/assets/texture-mask-text/masks/lava.png");
mask-image: url("/assets/texture-mask-text/masks/lava.png");
}
.hf-texture-rock {
-webkit-mask-image: url("/assets/texture-mask-text/masks/rock.png");
mask-image: url("/assets/texture-mask-text/masks/rock.png");
}
.hf-texture-concrete {
-webkit-mask-image: url("/assets/texture-mask-text/masks/concrete.png");
mask-image: url("/assets/texture-mask-text/masks/concrete.png");
}
.hf-texture-brick {
-webkit-mask-image: url("/assets/texture-mask-text/masks/brick.png");
mask-image: url("/assets/texture-mask-text/masks/brick.png");
}
.hf-texture-wood {
-webkit-mask-image: url("/assets/texture-mask-text/masks/wood.png");
mask-image: url("/assets/texture-mask-text/masks/wood.png");
}
.hf-texture-onyx {
-webkit-mask-image: url("/assets/texture-mask-text/masks/onyx.png");
mask-image: url("/assets/texture-mask-text/masks/onyx.png");
}
.hf-texture-carpet {
-webkit-mask-image: url("/assets/texture-mask-text/masks/carpet.png");
mask-image: url("/assets/texture-mask-text/masks/carpet.png");
}
.hf-texture-metal {
-webkit-mask-image: url("/assets/texture-mask-text/masks/metal.png");
mask-image: url("/assets/texture-mask-text/masks/metal.png");
}
.hf-texture-snow {
-webkit-mask-image: url("/assets/texture-mask-text/masks/snow.png");
mask-image: url("/assets/texture-mask-text/masks/snow.png");
}
.hf-texture-bricks-104 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/bricks-104.png");
mask-image: url("/assets/texture-mask-text/masks/bricks-104.png");
}
.hf-texture-ground-103 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/ground-103.png");
mask-image: url("/assets/texture-mask-text/masks/ground-103.png");
}
.hf-texture-grass-005 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/grass-005.png");
mask-image: url("/assets/texture-mask-text/masks/grass-005.png");
}
.hf-texture-rock-063 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/rock-063.png");
mask-image: url("/assets/texture-mask-text/masks/rock-063.png");
}
.hf-texture-wood-094 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/wood-094.png");
mask-image: url("/assets/texture-mask-text/masks/wood-094.png");
}
.hf-texture-road-012-a {
-webkit-mask-image: url("/assets/texture-mask-text/masks/road-012-a.png");
mask-image: url("/assets/texture-mask-text/masks/road-012-a.png");
}
.hf-texture-wood-092 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/wood-092.png");
mask-image: url("/assets/texture-mask-text/masks/wood-092.png");
}
.hf-texture-metal-049-a {
-webkit-mask-image: url("/assets/texture-mask-text/masks/metal-049-a.png");
mask-image: url("/assets/texture-mask-text/masks/metal-049-a.png");
}
.hf-texture-wood-floor-051 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/wood-floor-051.png");
mask-image: url("/assets/texture-mask-text/masks/wood-floor-051.png");
}
.hf-texture-paving-stones-150 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/paving-stones-150.png");
mask-image: url("/assets/texture-mask-text/masks/paving-stones-150.png");
}
.hf-texture-road-008-a {
-webkit-mask-image: url("/assets/texture-mask-text/masks/road-008-a.png");
mask-image: url("/assets/texture-mask-text/masks/road-008-a.png");
}
.hf-texture-road-007 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/road-007.png");
mask-image: url("/assets/texture-mask-text/masks/road-007.png");
}
.hf-texture-metal-055-a {
-webkit-mask-image: url("/assets/texture-mask-text/masks/metal-055-a.png");
mask-image: url("/assets/texture-mask-text/masks/metal-055-a.png");
}
.hf-texture-concrete-034 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/concrete-034.png");
mask-image: url("/assets/texture-mask-text/masks/concrete-034.png");
}
.hf-texture-grass-001 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/grass-001.png");
mask-image: url("/assets/texture-mask-text/masks/grass-001.png");
}
.hf-texture-wood-051 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/wood-051.png");
mask-image: url("/assets/texture-mask-text/masks/wood-051.png");
}
.hf-texture-asphalt-031 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/asphalt-031.png");
mask-image: url("/assets/texture-mask-text/masks/asphalt-031.png");
}
.hf-texture-wood-floor-064 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/wood-floor-064.png");
mask-image: url("/assets/texture-mask-text/masks/wood-floor-064.png");
}
.hf-texture-concrete-047-a {
-webkit-mask-image: url("/assets/texture-mask-text/masks/concrete-047-a.png");
mask-image: url("/assets/texture-mask-text/masks/concrete-047-a.png");
}
.hf-texture-metal-046-b {
-webkit-mask-image: url("/assets/texture-mask-text/masks/metal-046-b.png");
mask-image: url("/assets/texture-mask-text/masks/metal-046-b.png");
}
.hf-texture-grass-004 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/grass-004.png");
mask-image: url("/assets/texture-mask-text/masks/grass-004.png");
}
.hf-texture-marble-012 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/marble-012.png");
mask-image: url("/assets/texture-mask-text/masks/marble-012.png");
}
.hf-texture-ground-037 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/ground-037.png");
mask-image: url("/assets/texture-mask-text/masks/ground-037.png");
}
.hf-texture-tiles-138 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/tiles-138.png");
mask-image: url("/assets/texture-mask-text/masks/tiles-138.png");
}
.hf-texture-wood-066 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/wood-066.png");
mask-image: url("/assets/texture-mask-text/masks/wood-066.png");
}
.hf-texture-concrete-046 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/concrete-046.png");
mask-image: url("/assets/texture-mask-text/masks/concrete-046.png");
}
.hf-texture-wood-049 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/wood-049.png");
mask-image: url("/assets/texture-mask-text/masks/wood-049.png");
}
.hf-texture-road-013-a {
-webkit-mask-image: url("/assets/texture-mask-text/masks/road-013-a.png");
mask-image: url("/assets/texture-mask-text/masks/road-013-a.png");
}
.hf-texture-plaster-001 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/plaster-001.png");
mask-image: url("/assets/texture-mask-text/masks/plaster-001.png");
}
.hf-texture-snow-015 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/snow-015.png");
mask-image: url("/assets/texture-mask-text/masks/snow-015.png");
}
.hf-texture-road-012-b {
-webkit-mask-image: url("/assets/texture-mask-text/masks/road-012-b.png");
mask-image: url("/assets/texture-mask-text/masks/road-012-b.png");
}
.hf-texture-concrete-042-a {
-webkit-mask-image: url("/assets/texture-mask-text/masks/concrete-042-a.png");
mask-image: url("/assets/texture-mask-text/masks/concrete-042-a.png");
}
.hf-texture-bricks-102 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/bricks-102.png");
mask-image: url("/assets/texture-mask-text/masks/bricks-102.png");
}
.hf-texture-rock-058 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/rock-058.png");
mask-image: url("/assets/texture-mask-text/masks/rock-058.png");
}
.hf-texture-wood-floor-070 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/wood-floor-070.png");
mask-image: url("/assets/texture-mask-text/masks/wood-floor-070.png");
}
.hf-texture-bricks-101 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/bricks-101.png");
mask-image: url("/assets/texture-mask-text/masks/bricks-101.png");
}
.hf-texture-travertine-009 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/travertine-009.png");
mask-image: url("/assets/texture-mask-text/masks/travertine-009.png");
}
.hf-texture-ground-054 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/ground-054.png");
mask-image: url("/assets/texture-mask-text/masks/ground-054.png");
}
.hf-texture-wood-058 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/wood-058.png");
mask-image: url("/assets/texture-mask-text/masks/wood-058.png");
}
.hf-texture-ground-104 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/ground-104.png");
mask-image: url("/assets/texture-mask-text/masks/ground-104.png");
}
.hf-texture-painted-plaster-017 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/painted-plaster-017.png");
mask-image: url("/assets/texture-mask-text/masks/painted-plaster-017.png");
}
.hf-texture-paving-stones-138 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/paving-stones-138.png");
mask-image: url("/assets/texture-mask-text/masks/paving-stones-138.png");
}
.hf-texture-ground-068 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/ground-068.png");
mask-image: url("/assets/texture-mask-text/masks/ground-068.png");
}
.hf-texture-bricks-075-a {
-webkit-mask-image: url("/assets/texture-mask-text/masks/bricks-075-a.png");
mask-image: url("/assets/texture-mask-text/masks/bricks-075-a.png");
}
.hf-texture-ground-080 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/ground-080.png");
mask-image: url("/assets/texture-mask-text/masks/ground-080.png");
}
.hf-texture-road-009-c {
-webkit-mask-image: url("/assets/texture-mask-text/masks/road-009-c.png");
mask-image: url("/assets/texture-mask-text/masks/road-009-c.png");
}
.hf-texture-bark-014 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/bark-014.png");
mask-image: url("/assets/texture-mask-text/masks/bark-014.png");
}
.hf-texture-marble-016 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/marble-016.png");
mask-image: url("/assets/texture-mask-text/masks/marble-016.png");
}
.hf-texture-fabric-083 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/fabric-083.png");
mask-image: url("/assets/texture-mask-text/masks/fabric-083.png");
}
.hf-texture-metal-061-b {
-webkit-mask-image: url("/assets/texture-mask-text/masks/metal-061-b.png");
mask-image: url("/assets/texture-mask-text/masks/metal-061-b.png");
}
.hf-texture-metal-048-a {
-webkit-mask-image: url("/assets/texture-mask-text/masks/metal-048-a.png");
mask-image: url("/assets/texture-mask-text/masks/metal-048-a.png");
}
.hf-texture-metal-032 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/metal-032.png");
mask-image: url("/assets/texture-mask-text/masks/metal-032.png");
}
.hf-texture-metal-041-a {
-webkit-mask-image: url("/assets/texture-mask-text/masks/metal-041-a.png");
mask-image: url("/assets/texture-mask-text/masks/metal-041-a.png");
}
.hf-texture-metal-038 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/metal-038.png");
mask-image: url("/assets/texture-mask-text/masks/metal-038.png");
}
.hf-texture-diamond-plate-009 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/diamond-plate-009.png");
mask-image: url("/assets/texture-mask-text/masks/diamond-plate-009.png");
}
.hf-texture-leather-037 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/leather-037.png");
mask-image: url("/assets/texture-mask-text/masks/leather-037.png");
}
.hf-texture-fabric-080 {
-webkit-mask-image: url("/assets/texture-mask-text/masks/fabric-080.png");
mask-image: url("/assets/texture-mask-text/masks/fabric-080.png");
}
</style>
+4
View File
@@ -59,6 +59,10 @@
"name": "grid-pixelate-wipe",
"type": "hyperframes:component"
},
{
"name": "texture-mask-text",
"type": "hyperframes:component"
},
{
"name": "instagram-follow",
"type": "hyperframes:block"
+248 -12
View File
@@ -38,6 +38,11 @@ interface SourceMetadata {
sourcePrompt?: string;
}
interface TextureGroup {
title: string;
items: string[];
}
interface CatalogEntry {
name: string;
type: ItemKind;
@@ -97,23 +102,233 @@ function typeDir(kind: ItemKind): string {
return ITEM_TYPE_DIRS[kind === "block" ? "hyperframes:block" : "hyperframes:component"];
}
function textureGroupsFor(manifest: RegistryItem): TextureGroup[] {
if (!("textureGroups" in manifest)) return [];
const value = manifest.textureGroups;
if (!Array.isArray(value)) return [];
return value.filter((group): group is TextureGroup => {
if (!group || typeof group !== "object") return false;
if (!("title" in group) || typeof group.title !== "string") return false;
if (!("items" in group) || !Array.isArray(group.items)) return false;
return group.items.every((item) => typeof item === "string");
});
}
function textureLabel(slug: string): string {
return slug
.split("-")
.map((part) =>
part.length === 1 ? part.toUpperCase() : part[0]!.toUpperCase() + part.slice(1),
)
.join(" ");
}
function textureSampleWord(slug: string): string {
if (slug.includes("brick")) return "BRICK";
if (slug.includes("concrete")) return "CONCRETE";
if (slug.includes("plaster")) return "PLASTER";
if (slug.includes("rock")) return "ROCK";
if (slug.includes("onyx")) return "ONYX";
if (slug.includes("marble")) return "MARBLE";
if (slug.includes("travertine")) return "STONE";
if (slug.includes("paving")) return "STONE";
if (slug.includes("tiles")) return "TILE";
if (slug.includes("ground")) return "GROUND";
if (slug.includes("road")) return "ROAD";
if (slug.includes("asphalt")) return "ASPHALT";
if (slug.includes("wood-floor")) return "FLOOR";
if (slug.includes("wood")) return "WOOD";
if (slug.includes("bark")) return "BARK";
if (slug.includes("diamond")) return "PLATE";
if (slug.includes("metal")) return "METAL";
if (slug.includes("lava")) return "LAVA";
if (slug.includes("grass")) return "GRASS";
if (slug.includes("carpet")) return "WOVEN";
if (slug.includes("fabric")) return "FABRIC";
if (slug.includes("snow")) return "SNOW";
if (slug.includes("leather")) return "LEATHER";
return slug.toUpperCase();
}
function textureMaskUrlFor(manifest: RegistryItem, texture: string): string {
return `${catalogImageBase}/components/${manifest.name}/masks/${texture}.png`;
}
function generateTextureExamples(manifest: RegistryItem, textureGroups: TextureGroup[]): string[] {
const lines: string[] = [
"## Texture Examples",
"",
'<div className="hf-texture-example-groups">',
];
for (const group of textureGroups) {
lines.push(
" <div>",
` <h3 className="hf-texture-example-title">${group.title}</h3>`,
' <div className="hf-texture-example-grid">',
);
for (const item of group.items) {
const maskPath = textureMaskUrlFor(manifest, item);
const textureClass = `hf-texture-${item}`;
lines.push(
` <div className="hf-texture-example-card" style={{ "--mask-url": "url('${maskPath}')" }}>`,
` <div className="hf-texture-example-meta"><div className="hf-texture-example-label">${textureLabel(item)}</div><code className="hf-texture-example-class">${textureClass}</code></div>`,
` <div className="hf-texture-example-shadow"><div className="hf-texture-example-word">${textureSampleWord(item)}</div></div>`,
` <div className="hf-texture-example-usage">Use <code>hf-texture-text ${textureClass}</code></div>`,
" </div>",
);
}
lines.push(" </div>", " </div>");
}
lines.push("</div>", "");
return lines;
}
function generateTextureAgentUsage(
manifest: RegistryItem,
textureGroups: TextureGroup[],
): string[] {
const firstTexture = textureGroups[0]?.items[0] ?? "brick";
const firstClass = `hf-texture-${firstTexture}`;
const installedSnippet = `compositions/components/${manifest.name}/${manifest.name}.html`;
return [
"## Agent Usage",
"",
"Use this wording when asking an agent to apply a texture:",
"",
"```text",
`Use the ${manifest.title} catalog component.`,
"",
"1. From the project root, run:",
` npx hyperframes add ${manifest.name}`,
"2. That command creates this installed snippet:",
` ${installedSnippet}`,
"3. Open that file and paste the real <style> block",
" near the bottom into the composition once. That CSS defines",
" hf-texture-text and every hf-texture-* class.",
"4. Apply this class to the target text:",
` class="hf-texture-text ${firstClass}"`,
"5. For another material, copy one hf-texture-* class",
" from the Texture Examples cards.",
"6. This is the proper way to apply drop shadow",
" to textured text: wrap the text and put",
" filter on the wrapper, not on the text.",
" Use this markup:",
` <div style="filter: drop-shadow(1px 2px 1px rgba(0,0,0,0.48))">`,
` <div class="hf-texture-text ${firstClass}">TEXT</div>`,
" </div>",
"```",
"",
`After install, the snippet lives at \`${installedSnippet}\` inside the project where you ran \`npx hyperframes add ${manifest.name}\`. The part to paste is the real \`<style>\` element near the bottom of that file; the texture PNGs install to \`assets/${manifest.name}/masks/\` and are referenced by project-root URLs in that CSS.`,
"",
`Swap \`${firstClass}\` for the class shown on any texture card below. The base class \`hf-texture-text\` is always required.`,
"",
];
}
function generateTextureAnimationExample(
manifest: RegistryItem,
textureGroups: TextureGroup[],
): string[] {
const texture =
textureGroups.flatMap((group) => group.items).find((item) => item === "lava") ??
textureGroups[0]?.items[0] ??
"brick";
const textureClass = `hf-texture-${texture}`;
const maskPath = textureMaskUrlFor(manifest, texture);
return [
"## Animated Texture",
"",
"Animate the texture by moving the mask position on the text element. Keep drop shadow on a wrapper so the shadow follows the textured contour.",
"",
`<div className="hf-texture-animate-demo" style={{ "--mask-url": "url('${maskPath}')" }}>`,
' <div className="hf-texture-animate-meta">',
' <div className="hf-texture-animate-label">Animated mask position</div>',
` <code className="hf-texture-animate-class">hf-texture-text ${textureClass}</code>`,
" </div>",
' <div className="hf-texture-animate-shadow">',
' <div className="hf-texture-animate-word">MOTION</div>',
" </div>",
"</div>",
"",
"```html",
'<div class="texture-shadow">',
` <div class="hf-texture-text ${textureClass} animated-texture">MOTION</div>`,
"</div>",
"```",
"",
"```css",
".animated-texture {",
" --mask-size: 180% 180%;",
" --mask-position: 0% 50%;",
"}",
"```",
"",
"```js",
"const tl = gsap.timeline({ paused: true });",
'tl.to(".animated-texture", {',
' "--mask-position": "100% 50%",',
" duration: 1.2,",
' ease: "sine.inOut",',
" yoyo: true,",
" repeat: 1,",
"}, 0);",
'window.__timelines["my-composition"] = tl;',
"```",
"",
];
}
function generateTexturePreview(manifest: RegistryItem, textureGroups: TextureGroup[]): string[] {
const sampleItems = textureGroups
.map((group) => group.items[0])
.filter(Boolean)
.slice(0, 6);
const lines: string[] = ['<div className="hf-texture-preview-panel">'];
for (const item of sampleItems) {
const maskPath = textureMaskUrlFor(manifest, item);
lines.push(
` <div className="hf-texture-preview-card" style={{ "--mask-url": "url('${maskPath}')" }}>`,
` <div className="hf-texture-preview-label">${textureLabel(item!)}</div>`,
` <div className="hf-texture-preview-shadow"><div className="hf-texture-preview-word">${textureSampleWord(item!)}</div></div>`,
" </div>",
);
}
lines.push("</div>", "");
return lines;
}
function catalogPreviewFor(kind: ItemKind, manifest: RegistryItem): string {
const dir = typeDir(kind);
return `${catalogImageBase}/${dir}/${manifest.name}.png`;
}
function yamlString(value: string): string {
return JSON.stringify(value);
}
function generateItemMdx(kind: ItemKind, manifest: RegistryItem): string {
const tags = manifest.tags ?? [];
const tagBadges = tags.map((t) => `\`${t}\``).join(" ");
const installCmd = `npx hyperframes add ${manifest.name}`;
const source = manifest as RegistryItem & SourceMetadata;
const textureGroups = textureGroupsFor(manifest);
const lines: string[] = [
"---",
`title: "${manifest.title.replace(/"/g, '\\"')}"`,
`description: "${manifest.description.replace(/"/g, '\\"')}"`,
"---",
"",
`# ${manifest.title}`,
"",
manifest.description,
"",
];
const lines: string[] = ["---", `title: ${yamlString(manifest.title)}`];
if (textureGroups.length === 0) {
lines.push(`description: ${yamlString(manifest.description)}`);
}
lines.push("---", "");
if (textureGroups.length === 0) {
lines.push(`# ${manifest.title}`, "", manifest.description, "");
}
if (tagBadges) {
lines.push(tagBadges, "");
@@ -137,12 +352,16 @@ function generateItemMdx(kind: ItemKind, manifest: RegistryItem): string {
lines.push("## Source Prompt", "", "```text", source.sourcePrompt, "```", "");
}
if (textureGroups.length > 0) {
lines.push(...generateTexturePreview(manifest, textureGroups));
} else {
// Preview video with poster — muted loop, no autoPlay (matches examples page).
const previewPath = `${catalogImageBase}/${typeDir(kind)}/${manifest.name}`;
lines.push(
`<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="${previewPath}.mp4" poster="${previewPath}.png" autoPlay muted loop playsInline />`,
"",
);
}
// Install command
lines.push(
@@ -181,12 +400,20 @@ function generateItemMdx(kind: ItemKind, manifest: RegistryItem): string {
);
}
if (textureGroups.length > 0) {
lines.push(...generateTextureAgentUsage(manifest, textureGroups));
lines.push(...generateTextureAnimationExample(manifest, textureGroups));
lines.push(...generateTextureExamples(manifest, textureGroups));
}
// Files
if (textureGroups.length === 0) {
lines.push("## Files", "", "| File | Target | Type |", "| --- | --- | --- |");
for (const f of manifest.files) {
lines.push(`| \`${f.path}\` | \`${f.target}\` | ${f.type} |`);
}
lines.push("");
}
// Usage hint — find the primary file by type, not array position.
const primaryFile =
@@ -208,6 +435,14 @@ function generateItemMdx(kind: ItemKind, manifest: RegistryItem): string {
"```",
"",
);
} else {
if (textureGroups.length > 0) {
lines.push(
"## Usage",
"",
`After \`${installCmd}\`, the installed snippet lives at \`${primaryTarget}\` inside your current HyperFrames project. Open that file and paste the real \`<style>\` element near the bottom into your composition once; it defines \`hf-texture-text\` and every \`hf-texture-*\` class used by the examples above. Keep the installed texture PNGs in \`assets/${manifest.name}/masks/\`; the CSS references them with project-root URLs.`,
"",
);
} else {
lines.push(
"## Usage",
@@ -216,6 +451,7 @@ function generateItemMdx(kind: ItemKind, manifest: RegistryItem): string {
"",
);
}
}
// Related skill
if (manifest.relatedSkill) {
@@ -258,7 +494,7 @@ function main(): void {
description: manifest.description,
tags: manifest.tags ?? [],
href: `/catalog/${dir}/${manifest.name}`,
preview: `${catalogImageBase}/${dir}/${manifest.name}.png`,
preview: catalogPreviewFor(kind, manifest),
});
}