@rlawncks125/component-render-ani
v0.1.11
Published
Reusable Vue canvas animation component.
Downloads
1,756
Readme
Component Render Ani
Reusable Vue canvas animation component.
@rlawncks125/component-render-ani exposes RenderAniCanvas, a canvas-like
component that watches the #render slot and animates newly added Vue components
onto the screen.
Install
npm install @rlawncks125/component-render-aniNuxt Usage
Add the Nuxt module to nuxt.config.ts.
export default defineNuxtConfig({
modules: ["@rlawncks125/component-render-ani/nuxt"],
});Nuxt can then auto-import both component names:
<RenderAniCanvas />
<RenderAiCanvas />The Nuxt module also registers the package stylesheet automatically.
Vue Usage
Register the plugin once in your Vue app entry.
import { createApp } from "vue";
import ComponentRenderAni from "@rlawncks125/component-render-ani";
import App from "./App.vue";
createApp(App).use(ComponentRenderAni).mount("#app");After registration, Vue/Volar can discover these global components:
<RenderAniCanvas />
<RenderAiCanvas />If you import the component directly instead of using app.use, import it like this:
import { RenderAniCanvas } from "@rlawncks125/component-render-ani";
import type { RenderAniCanvasItem } from "@rlawncks125/component-render-ani";Basic Example
Add components to renderLists. When the list changes, RenderAniCanvas reads
the #render slot and draws the new component.
<script setup lang="ts">
import { ref } from "vue";
import { RenderAniCanvas, type RenderAniCanvasItem } from "@rlawncks125/component-render-ani";
import MyCard from "./MyCard.vue";
const renderLists = ref<RenderAniCanvasItem[]>([
{
id: "first-card",
slotId: "first-card",
span: "half",
component: MyCard,
componentName: "MyCard",
attrs: {
class: "custom-card",
style: { borderColor: "#4285f4" },
},
props: {
title: "First rendered card",
description: "This component is rendered through the canvas animation.",
},
},
]);
const addCard = () => {
const id = `card-${renderLists.value.length + 1}`;
renderLists.value.push({
id,
slotId: id,
span: "half",
component: MyCard,
componentName: "MyCard",
props: {
title: `Rendered card ${renderLists.value.length + 1}`,
description: "Pushing to renderLists triggers the canvas draw sequence.",
},
});
};
</script>
<template>
<RenderAniCanvas>
<template #controls="{ running, queue }">
<button type="button" :disabled="running" @click="addCard">
Add card
</button>
<span v-if="running || queue.length">
{{ running ? "Drawing" : "" }}
{{ queue.length ? `${queue.length} waiting` : "" }}
</span>
</template>
<template #render>
<template v-for="item in renderLists" :key="item.id">
<component
:is="item.component"
:component-name="item.componentName"
:slot-id="item.slotId"
:span="item.span"
v-bind="{ ...(item.attrs || {}), ...(item.props || {}) }"
/>
</template>
</template>
</RenderAniCanvas>
</template>Headless Styling
The package exports CSS at @rlawncks125/component-render-ani/style.css.
If your bundler does not automatically include CSS from the package entry, import it once in your app entry file.
import "@rlawncks125/component-render-ani/style.css";RenderAniCanvas is mostly headless. It keeps only the classes needed for
measurement, scrolling, column span, and animation. Build the visible layout with
classes.wrap and classes.render. The internal canvas owns scrolling, so
you usually do not need to set overflow classes yourself.
<RenderAniCanvas
:classes="{
wrap: 'h-screen w-screen bg-white',
header: 'px-5 pt-5',
render: 'mx-auto grid w-[min(960px,calc(100vw-32px))] grid-cols-12 gap-4 py-8',
item: 'rounded-lg',
}"
/>Cursor
Use the cursor prop to change the built-in cursor. Supported types are
pointer, dot, ring, image, and none.
<RenderAniCanvas
:cursor="{
type: 'ring',
size: 28,
class: 'text-blue-500',
}"
/>Use an image cursor with imageSrc.
<RenderAniCanvas
:cursor="{
type: 'image',
imageSrc: '/cursor.png',
size: 36,
offsetX: -4,
offsetY: -4,
}"
/>For a fully custom cursor, use the #cursor slot.
<RenderAniCanvas>
<template #cursor="{ clicking }">
<div
class="grid size-8 place-items-center rounded-full bg-black text-white"
:class="{ 'scale-90': clicking }"
>
AI
</div>
</template>
</RenderAniCanvas>Animation Types
Use animation.type to choose how a component is drawn. Supported types are
draw, instant, spotlight, scan, and typewriter.
<RenderAniCanvas
:animation="{
type: 'spotlight',
speed: 1.4,
}"
/><RenderAniCanvas :animation="{ type: 'scan' }" />
<RenderAniCanvas :animation="{ type: 'typewriter' }" />Internal Structure
RenderAniCanvas renders the user-provided #render components into this
internal structure:
wrap
├─ header
│ └─ #controls slot
└─ canvas frame
├─ canvas
│ └─ render
│ └─ item
│ └─ rendered component
└─ overlay
├─ selection
└─ cursorThe same names are used by the classes and styles props. In most cases,
customizing wrap and render is enough.
<RenderAniCanvas
:classes="{
wrap: 'h-screen w-screen bg-white',
header: 'px-5 pt-5',
render: 'mx-auto grid w-[min(960px,calc(100vw-32px))] grid-cols-12 gap-4 py-8',
item: 'rounded-lg',
}"
/>wrap: root wrapper for the whole canvas component.header: wrapper around the#controlsslot.canvas: internal scroll container.render: render area and layout container.item: required render item section for span, measurement, ref, and animation.
Every internal element also includes a debug attribute so it is easy to inspect in DevTools:
[data-render-ani-el="wrap"]
[data-render-ani-el="header"]
[data-render-ani-el="canvas"]
[data-render-ani-el="render"]
[data-render-ani-el="item"]
[data-render-ani-el="overlay"]
[data-render-ani-el="selection"]
[data-render-ani-el="spotlight"]
[data-render-ani-el="scan"]
[data-render-ani-el="typewriter"]
[data-render-ani-el="cursor"]
[data-render-ani-el="measure"]Rendered items also include data-render-ani-slot-id,
data-render-ani-span, and data-render-ani-component.
Render Item
interface RenderAniCanvasItem {
attrs?: Record<string, unknown>;
id: string;
slotId?: string;
span?: "half" | "full";
component: string | Component;
componentName?: string;
props?: Record<string, unknown>;
}id: stable key for Vuev-for.attrs: pass-through attrs such asclass,style,data-*, andaria-*.slotId: canvas slot identity. If omitted, the VNode key or fallback order is used.span:"half"renders in a 6-column slot,"full"renders full-width.component: Vue component object or registered component name.componentName: optional name used for default height lookup.props: props passed to the rendered component.
Props
<RenderAniCanvas
:animation="{ speed: 1.6 }"
:auto-run="true"
:classes="{
wrap: 'h-screen w-screen',
header: 'px-5 pt-5',
render: 'grid grid-cols-12 gap-4 p-6',
item: 'rounded-lg',
}"
:default-heights="{ MyCard: 240 }"
:fallback-height="180"
:styles="{
wrap: { minHeight: '100vh' },
header: { pointerEvents: 'auto' },
render: { paddingTop: '120px' },
}"
/>animation: controls animation speed and behavior.autoRun: watches#renderslot changes and draws automatically. Default:true.defaultHeights: initial reserved height bycomponentName.fallbackHeight: reserved height when no default height is found. Default:180.rootAttrs: pass-through attrs for the root wrapper.canvasAttrs: pass-through attrs for the scroll canvas.renderAreaAttrs: pass-through attrs for the render container.renderItemAttrs: pass-through attrs for each rendered item.classes: Tailwind/class customization map forwrap,header,canvas,render, anditem.styles: style customization map forwrap,header,canvas,render, anditem.
Default utility classes are intentionally minimal and are merged with
tailwind-merge, so your custom classes win when they conflict with built-in
classes.
Animation
Use animation.speed to make every built-in animation faster or slower. The
default speed is 1. Higher values are faster.
<RenderAniCanvas :animation="{ speed: 2 }" />Use animation.type to choose the animation behavior. Current options are
"draw" and "instant". The "draw" type is the default canvas drawing
animation. The "instant" type keeps the same render flow, but skips motion
durations.
<RenderAniCanvas :animation="{ type: 'instant' }" />You can also override individual durations in milliseconds. These values are
still divided by speed.
<RenderAniCanvas
:animation="{
type: 'draw',
speed: 1.4,
durations: {
cursorMove: 260,
cursorClick: 80,
draw: 420,
mountDelay: 80,
reveal: 180,
scroll: 220,
},
}"
/>Slots
#render
Required for list-driven rendering. Put the components that should be drawn here.
<template #render>
<template v-for="item in renderLists" :key="item.id">
<component
:is="item.component"
:component-name="item.componentName"
:slot-id="item.slotId"
:span="item.span"
v-bind="{ ...(item.attrs || {}), ...(item.props || {}) }"
/>
</template>
</template>#controls
Optional. Use this slot for buttons, status labels, or custom controls.
<template #controls="{ running, queue, resetAll, syncSlottedComponents }">
<button type="button" :disabled="running" @click="resetAll">
Reset
</button>
<button type="button" :disabled="running" @click="syncSlottedComponents">
Sync
</button>
<span>{{ queue.length }} waiting</span>
</template>Slot props:
interface RenderAniCanvasSlotProps {
addComponent: (definition: ComponentDefinition) => Promise<void>;
queue: DrawItem[];
resetAll: () => void;
runComponents: (definitions: ComponentDefinition[]) => Promise<void>;
runSlottedComponents: () => Promise<void>;
running: boolean;
slots: DrawSlot[];
syncSlottedComponents: () => Promise<void>;
}Build And Publish
bun install
bun run build:lib
npm publish --access publicSee USAGE.md for the longer local development notes.
