npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

h-marquee-x

v0.1.1

Published

High-performance canvas marquee component for Vue 3.

Readme

HMarqueeX

HMarqueeX 是一个基于 Canvas 的 Vue 3 无限滚动走马灯组件。核心动画引擎不依赖 Vue,可以单独使用;Vue 组件负责生命周期、props、事件和插件注册。

特性

  • Canvas 渲染,适合 1000+ 条数据的长列表滚动场景。
  • 使用 requestAnimationFrame 驱动动画。
  • 只绘制可视区域内的条目,避免每帧遍历完整数据。
  • 支持水平和垂直滚动。
  • 支持 hover 暂停、触摸暂停、点击条目回调。
  • 支持 appendreplace 两种数据更新模式。
  • 支持 ESM 和 UMD 打包格式。

安装

npm install h-marquee-x

Vue 3 是 peer dependency,项目中需要已有 Vue 3。

Vue 3 使用

<template>
  <HMarqueeX
    ref="marqueeRef"
    class="marquee"
    :data="items"
    :speed="1.2"
    :item-width="170"
    :item-height="76"
    :gap="12"
    :render-item="renderItem"
    @item-click="handleClick"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { HMarqueeX, type RenderItem, type MarqueeClickPayload } from 'h-marquee-x';
import 'h-marquee-x/style.css';

interface Item {
  title: string;
  value: number;
}

const marqueeRef = ref<InstanceType<typeof HMarqueeX> | null>(null);
const items = ref<Item[]>([
  { title: 'A', value: 100 },
  { title: 'B', value: 200 },
]);

const renderItem: RenderItem<Item> = (ctx, item, x, y, width, height) => {
  ctx.fillStyle = '#ffffff';
  ctx.strokeStyle = '#e2e8f0';
  ctx.beginPath();
  ctx.rect(x, y, width, height);
  ctx.fill();
  ctx.stroke();

  ctx.fillStyle = '#0f172a';
  ctx.font = '14px system-ui';
  ctx.textBaseline = 'middle';
  ctx.fillText(`${item.title}: ${item.value}`, x + 12, y + height / 2, width - 24);
};

function handleClick(payload: MarqueeClickPayload<Item>) {
  console.log(payload.item, payload.index);
}

function appendItems(next: Item[]) {
  marqueeRef.value?.updateData(next, 'append');
}
</script>

<style scoped>
.marquee {
  width: 100%;
  height: 120px;
}
</style>

插槽自定义内容

如果需要让业务方完全自定义内部 DOM 和 CSS,可以直接使用默认插槽:

<template>
  <HMarqueeX
    class="marquee"
    :data="items"
    :speed="1"
    :item-width="180"
    :item-height="82"
    :gap="12"
    @item-click="handleClick"
  >
    <template #default="{ item, index }">
      <div class="marquee-card">
        <strong>{{ item.title }}</strong>
        <span>{{ item.value }}</span>
        <small>#{{ index }}</small>
      </div>
    </template>
  </HMarqueeX>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { HMarqueeX } from 'h-marquee-x';
import 'h-marquee-x/style.css';

const items = ref([
  { title: '商品 A', value: 100 },
  { title: '商品 B', value: 200 },
]);

function handleClick(payload) {
  console.log(payload.item, payload.index);
}
</script>

<style scoped>
.marquee {
  width: 100%;
  height: 120px;
}

.marquee-card {
  display: grid;
  align-content: center;
  width: 100%;
  height: 100%;
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  background: #ffffff;
  padding: 12px;
}
</style>

默认 renderModeauto:没有插槽时使用 Canvas 模式,有默认插槽时自动使用 DOM 模式。Canvas 不能直接渲染 Vue 插槽里的 HTML/CSS,因此插槽模式会用 DOM 节点承载内容,仍由组件内部用 requestAnimationFrame 控制滚动、暂停和点击。

也可以注册为全局插件:

import { createApp } from 'vue';
import HMarqueeX from 'h-marquee-x';
import 'h-marquee-x/style.css';

createApp(App).use(HMarqueeX).mount('#app');

纯 JavaScript 使用

import { CanvasMarquee } from 'h-marquee-x';

const canvas = document.querySelector<HTMLCanvasElement>('#marquee')!;

const marquee = new CanvasMarquee(canvas, {
  data: ['A', 'B', 'C'],
  speed: 1,
  itemWidth: 150,
  itemHeight: 80,
  gap: 10,
});

marquee.updateData(['D', 'E'], 'append');

Props

| 名称 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | data | T[] | [] | 初始/响应式数据 | | direction | 'horizontal' \| 'vertical' | 'horizontal' | 滚动方向 | | speed | number | 1 | 每帧移动像素数 | | itemWidth | number | 150 | 条目宽度 | | itemHeight | number | 80 | 条目高度 | | gap | number | 10 | 条目间距 | | autoStart | boolean | true | 挂载后自动开始 | | pauseOnHover | boolean | true | 鼠标悬停暂停 | | touchPause | boolean | true | 触摸/指针按下暂停 | | forceScroll | boolean | true | 是否强制滚动;为 false 时仅内容主轴长度超过容器才滚动 | | backgroundColor | string | undefined | Canvas 背景色 | | renderItem | RenderItem<T> | 内置文本渲染 | 自定义条目绘制函数 | | updateMode | 'append' \| 'replace' | 'replace' | props 数据变化时的更新策略 | | renderMode | 'auto' \| 'canvas' \| 'dom' | 'auto' | 渲染模式;插槽内容建议使用 autodom |

数据不足和是否滚动

组件不会修改用户传入的 data。为了做到无缝循环,内部只在渲染层做虚拟重复:

  • forceScroll: true:只要有数据就滚动。数据量不足以填满容器时,内部会重复映射原始数据进行绘制或渲染,但点击事件返回的仍然是原始 item/index
  • forceScroll: false:组件会计算内容主轴长度是否超过容器。没有超过时静态展示;超过时才开启循环滚动。

判断方式:

const itemStep = direction === 'horizontal'
  ? itemWidth + gap
  : itemHeight + gap

const contentSize = data.length * itemStep
const viewportSize = direction === 'horizontal'
  ? containerWidth
  : containerHeight

const shouldScroll = forceScroll || contentSize > viewportSize

当数据从很多更新成很少时,如果新的内容长度不再超过容器,并且 forceScrollfalse,组件会把当前滚动偏移归零并切换为静态展示,避免空白、跳动或停在错误位置。

事件

| 事件名 | 参数 | 说明 | | --- | --- | --- | | item-click | { item, index, event } | 点击可视条目时触发 |

暴露方法

组件通过 defineExpose 暴露以下方法:

| 方法 | 说明 | | --- | --- | | updateData(newData, mode) | 更新数据,modeappendreplace | | start() | 启动动画 | | stop() | 停止动画 | | pause() | 暂停滚动,不销毁动画循环 | | resume() | 恢复滚动 | | getSnapshot() | 获取当前 offset、运行状态和数据长度 |

无感更新原理

组件内部把动画状态和数据状态解耦:

  1. offset 只由动画循环维护,不跟随数据变化重置。
  2. updateData 只替换或追加数据数组,然后把当前 offset 归一化到新的循环长度内。
  3. 每帧根据 offset / (itemSize + gap) 计算可视区起始槽位,再用取模映射到真实数据下标。
  4. 绘制时只渲染可视区数量加少量缓冲项,因此数据总量增加不会线性增加每帧绘制成本。

追加模式适合长轮询、WebSocket 等增量数据场景。替换模式适合全量刷新,组件会保留当前偏移量,尽量避免视觉跳动。

本地开发

npm install
npm run dev
npm run build