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

prophetk-3d-viewer

v0.1.8

Published

A Three.js r128 based GLB/GLTF model viewer for WeChat Mini Program and Web.

Downloads

479

Readme

prophetk-3d-viewer

Three.js r128 based GLB/GLTF model viewer for WeChat Mini Program and Web.

The package bundles the same Three.js r128 build for both platforms. It does not rely on the host project's Three.js version, which keeps material, lighting, GLTF parsing, and decal shader behavior consistent.

Features

  • WeChat Mini Program component.
  • Web viewer helper for browser projects.
  • Built-in Three.js r128 and patched GLTFLoader.
  • GLB/GLTF loading.
  • Scene JSON loading.
  • Orbit touch controls.
  • Canvas-rendered button UI for Mini Program/Web overlays.
  • Cylindrical decal projection on PBR materials.
  • WeChat local file cache.

WeChat Mini Program

Install the npm package, run "构建 npm" in WeChat DevTools, then register the component:

{
  "usingComponents": {
    "model-viewer": "prophetk-3d-viewer/wechat/model-viewer"
  }
}

Use it in WXML:

<model-viewer
  id="viewer"
  canvas-id="modelCanvas"
  bind:uievent="onViewerUiEvent"
/>

Call the component API:

const viewer = this.selectComponent('#viewer');

await viewer.loadScene(sceneConfig);
await viewer.loadModel('https://cdn.example.com/model.glb');
viewer.updateDecal(Math.PI / 4, 0, 1, 1);

Handle canvas-rendered UI events with bind:uievent:

Page({
  onViewerUiEvent(e) {
    const { event } = e.detail;
    if (event === 'event_btn_reload_model') {
      this.selectComponent('#viewer').loadScene();
    }
  }
});

Remote model, texture, and scene URLs must be added to the Mini Program legal domain allowlist.

Web

Use the bundled Web entry:

const { createWebModelViewer } = require('prophetk-3d-viewer/web');

const viewer = createWebModelViewer({
  container: document.querySelector('#viewer'),
  bgColor: '#000000',
  onUiEvent(detail) {
    console.log(detail.event);
  },
});

await viewer.loadScene(sceneConfig);

The Web helper also dispatches a uiEvent DOM event on the canvas:

viewer.canvas.addEventListener('uiEvent', (e) => {
  console.log(e.detail.event);
});

The container must have a stable width and height:

#viewer {
  width: 100%;
  height: 480px;
}

Core API

const { ModelRenderer, WebPlatformAdapter } = require('prophetk-3d-viewer');

const renderer = new ModelRenderer(canvas, {
  platform: new WebPlatformAdapter(),
  renderMode: 'continuous',
});

Main methods:

  • loadModel(url, options)
  • switchModel(url, options)
  • loadScene(configOrUrl)
  • clearModel(target)
  • clearAllModels()
  • applyDecal(url, options)
  • updateDecal(options) or updateDecal(angle, height, scaleX, scaleY)
  • updateProjectorDecal(options)
  • removeDecal()
  • setUiEventHandler(handler)
  • setUiLabelText(id, text)
  • dispose()

Decal API

applyDecal(url, options) loads a decal texture and applies it to the current model. PNG textures with alpha are recommended.

Cylindrical decals wrap around the model on the world Y axis:

await renderer.applyDecal('https://cdn.example.com/decal.png', {
  projectionMode: 'cylindrical',
  angle: Math.PI / 4,
  height: 0,
  scaleX: 1,
  scaleY: 1,
  arcWidth: Math.PI / 3,
  heightRange: 1,
  opacity: 1
});

Projector decals use an orthographic projector:

await renderer.applyDecal('https://cdn.example.com/decal.png', {
  projectionMode: 'projector',
  projectorPosition: [0, 0.5, 2],
  projectorTarget: [0, 0.5, 0],
  projectorRollDeg: 0,
  projectorWidth: 1,
  projectorHeight: 1,
  projectorNormalThreshold: 0,
  scaleX: 1,
  scaleY: 1,
  opacity: 1
});

projectionMode can be cylindrical, projector, or orthographic. mode is also accepted as an alias. Projector options automatically switch the decal to projector mode when projectionMode is omitted.

Common decal options:

  • scaleX, scaleY: texture scale inside the projected region.
  • opacity: decal blend opacity.
  • angle: cylindrical center angle in radians.
  • height: cylindrical center height in world units.
  • arcWidth: cylindrical angular width in radians.
  • heightRange: cylindrical vertical span in world units.
  • projectorPosition or position: projector origin.
  • projectorTarget or targetPoint: world point the projector aims at.
  • projectorDirection or direction: projector direction when no target is provided.
  • projectorRotation or rotation: Euler XYZ rotation in radians when no target is provided.
  • projectorRoll or roll: rotation around the projector direction in radians.
  • projectorRollDeg or rollDeg: rotation around the projector direction in degrees.
  • projectorUp or up: projector up direction.
  • projectorWidth or width: projector width.
  • projectorHeight: projector height. height is also accepted in projector mode, but projectorHeight avoids ambiguity with cylindrical height.
  • projectorNormalThreshold or normalThreshold: surface normal visibility threshold.

updateDecal(options) updates the active decal without reloading the texture:

renderer.updateDecal({
  angle: Math.PI / 2,
  height: 0.3,
  scaleX: 1.2,
  scaleY: 1,
  arcWidth: Math.PI / 4,
  heightRange: 0.8,
  opacity: 0.9
});

The legacy positional form is still supported for cylindrical decals:

renderer.updateDecal(Math.PI / 2, 0.3, 1.2, 1);

For projector decals, either pass projector options to updateDecal(options) or use updateProjectorDecal(options):

renderer.updateProjectorDecal({
  projectorPosition: [0, 0.5, 2],
  projectorTarget: [0, 0.5, 0],
  projectorRollDeg: 0,
  projectorWidth: 1,
  projectorHeight: 1
});

Canvas UI

The ui field in loadScene() renders interactive controls inside the same Three.js canvas. This is useful on WeChat Mini Program because ordinary Mini Program views and cover-view are not reliable overlays above a WebGL canvas.

Currently supported UI types:

  • button
  • slider
  • label

Example:

await viewer.loadScene({
  models: [
    {
      id: 'main',
      type: 'gltf',
      url: 'https://cdn.example.com/model.glb'
    }
  ],
  ui: [
    {
      type: 'button',
      position: [0.04, 0.04],
      scale: [0.24, 0.08],
      text: 'Reload',
      img: '',
      alpha: 1,
      event: 'event_btn_reload_model'
    },
    {
      type: 'slider',
      position: [0.04, 0.16],
      scale: [0.42, 0.08],
      text: 'decal x',
      min: 0.2,
      max: 3,
      value: 1,
      step: 0.01,
      event: 'event_slider_decal_scale_x',
      target: 'decal.scaleX'
    },
    {
      type: 'label',
      id: 'status_label',
      position: [0.04, 0.88],
      scale: [0.5, 0.06],
      text: 'Ready',
      color: '#ffffff',
      backgroundColor: 'rgba(0, 0, 0, 0.35)'
    }
  ]
});

position and scale use normalized canvas coordinates. The origin is the canvas bottom-left corner:

  • position: [0, 0] places the button at the bottom-left of the canvas.
  • position: [1, 1] starts at the top-right of the canvas.
  • X grows to the right.
  • Y grows upward.
  • position means the button's bottom-left corner, not its center.
  • scale: [0.24, 0.08] means 24% of the canvas width and 8% of the canvas height.
  • Normalized values keep button size and placement broadly consistent across different phones and canvas resolutions.

Button fields:

  • type: must be 'button'.
  • position: [x, y] normalized to the canvas, bottom-left origin.
  • scale: [width, height] normalized to the canvas.
  • text: optional text drawn into the button texture.
  • img: optional image URL used as the button background. If empty, a default rounded dark button is drawn.
  • alpha: button opacity from 0 to 1.
  • event: string key emitted when the user taps/clicks the button.
  • backgroundColor: optional fallback background color when img is empty.
  • borderColor: optional fallback border color when img is empty.
  • color: optional text color.
  • fontSize: optional text size in pixels.

When the user presses a button, it renders a built-in pressed state by scaling down slightly and lowering opacity. If the pointer moves outside the button before release, the pressed state is cancelled and no event is emitted.

Slider fields:

  • type: 'slider'. The alias 'slide' is also accepted.
  • position: [x, y] normalized to the canvas, bottom-left origin.
  • scale: [width, height] normalized to the canvas.
  • text: optional label shown above the track.
  • min: minimum value. Default is 0.
  • max: maximum value. Default is 1.
  • value: initial value.
  • step: optional step size.
  • alpha: slider opacity from 0 to 1.
  • event: string key emitted when the slider changes.
  • target: optional built-in target. Currently decal.scaleX and decal.scaleY update the active decal directly.
  • trackColor: optional track color.
  • fillColor: optional filled track color.
  • thumbColor: optional thumb color.
  • thumbBorderColor: optional thumb border color.
  • color: optional label text color.
  • fontSize: optional label text size in pixels.

Example decal scale controls:

ui: [
  {
    type: 'slider',
    position: [0.04, 0.16],
    scale: [0.42, 0.08],
    text: 'decal x',
    min: 0.2,
    max: 3,
    value: 1,
    step: 0.01,
    event: 'event_slider_decal_scale_x',
    target: 'decal.scaleX'
  },
  {
    type: 'slider',
    position: [0.04, 0.27],
    scale: [0.42, 0.08],
    text: 'decal y',
    min: 0.2,
    max: 3,
    value: 1,
    step: 0.01,
    event: 'event_slider_decal_scale_y',
    target: 'decal.scaleY'
  }
]

Slider events include the current value:

{
  type: 'slider',
  event: 'event_slider_decal_scale_x',
  text: 'decal x',
  value: 1.35
}

Label fields:

  • type: must be 'label'.
  • id: required if the label text will be updated from code.
  • position: [x, y] normalized to the canvas, bottom-left origin.
  • scale: [width, height] normalized to the canvas.
  • text: initial display text.
  • alpha: label opacity from 0 to 1.
  • backgroundColor: optional label background.
  • borderColor: optional border color.
  • borderWidth: optional border width in pixels.
  • radius: optional corner radius in pixels.
  • padding: optional horizontal text padding in pixels.
  • color: optional text color.
  • fontSize: optional text size in pixels.
  • align: optional text alignment, one of 'left', 'center', or 'right'.

Labels do not consume touch input. They are rendered in the canvas like buttons and sliders, but pointer/touch events pass through to model orbit controls.

Update a label after the scene is loaded:

const viewer = this.selectComponent('#viewer');
viewer.setUiLabelText('status_label', 'Loading model...');

Web usage:

viewer.setUiLabelText('status_label', 'Scale X: 1.25');

WeChat UI Events

Use lowercase bind:uievent in WXML:

<model-viewer
  id="viewer"
  canvas-id="modelCanvas"
  bind:uievent="onViewerUiEvent"
/>

Then route by e.detail.event:

Page({
  onViewerUiEvent(e) {
    if (e.detail.event === 'event_btn_reload_model') {
      this.selectComponent('#viewer').loadScene();
    }
  }
});

Event detail:

{
  type: 'button',
  event: 'event_btn_reload_model',
  text: 'Reload',
  x: 24,
  y: 438
}

x and y are the raw touch position in canvas CSS pixels with top-left browser/touch coordinates. Use event as the stable business key.

Web UI Events

For Web projects, pass onUiEvent to createWebModelViewer():

const viewer = createWebModelViewer({
  container: document.querySelector('#viewer'),
  onUiEvent(detail) {
    if (detail.event === 'event_btn_reload_model') {
      viewer.loadScene(sceneConfig);
    }
  }
});

The canvas also emits a uiEvent DOM CustomEvent:

viewer.canvas.addEventListener('uiEvent', (e) => {
  console.log(e.detail);
});

UI Notes

  • UI is rendered after the 3D scene with a separate orthographic camera.
  • UI config uses normalized coordinates, then the renderer converts them to current canvas CSS pixels.
  • When the Web helper resizes, the active UI config is rebuilt against the new canvas size.
  • UI does not use Mini Program layout, so it will not be affected by z-index or native component layer limits.
  • UI textures are generated with an offscreen 2D canvas. On WeChat, this requires wx.createOffscreenCanvas.
  • Image URLs in img use the same download/cache path as model and decal assets, so remote domains must be allowlisted.
  • Buttons and sliders consume touch input while active, so UI interaction will not rotate the model. Labels are non-interactive.

Scene Config

{
  renderer: {
    clearColor: '#000000',
    clearAlpha: 1,
    outputEncoding: 'sRGB',
    toneMapping: 'ACESFilmic'
  },
  camera: {
    position: [0, 1.5, 3],
    lookAt: [0, 0, 0],
    fov: 60
  },
  models: [
    {
      id: 'main',
      type: 'gltf',
      url: 'https://cdn.example.com/model.glb',
      rotation: [0, 90, 0],
      scale: [1, 1, 1]
    }
  ],
  lights: [
    { type: 'ambient', color: '#ffffff', intensity: 0.5 },
    { type: 'directional', color: '#ffffff', intensity: 0.8, position: [5, 10, 7] }
  ],
  ui: [
    {
      type: 'button',
      position: [0.04, 0.04],
      scale: [0.24, 0.08],
      text: 'Reload',
      img: '',
      alpha: 1,
      event: 'event_btn_reload_model'
    },
    {
      type: 'slider',
      position: [0.04, 0.16],
      scale: [0.42, 0.08],
      text: 'decal x',
      min: 0.2,
      max: 3,
      value: 1,
      step: 0.01,
      event: 'event_slider_decal_scale_x',
      target: 'decal.scaleX'
    },
    {
      type: 'slider',
      position: [0.04, 0.27],
      scale: [0.42, 0.08],
      text: 'decal y',
      min: 0.2,
      max: 3,
      value: 1,
      step: 0.01,
      event: 'event_slider_decal_scale_y',
      target: 'decal.scaleY'
    },
    {
      type: 'label',
      id: 'status_label',
      position: [0.04, 0.88],
      scale: [0.5, 0.06],
      text: 'Ready',
      color: '#ffffff',
      backgroundColor: 'rgba(0, 0, 0, 0.35)'
    }
  ],
  decal_projector: {
    target: 'main',
    url: 'https://cdn.example.com/decal.png',
    scaleX: 1,
    scaleY: 1,
    opacity: 1,
    projectorPosition: [0, 0.5, 2],
    projectorTarget: [0, 0.5, 0],
    projectorRollDeg: 0,
    projectorWidth: 1,
    projectorHeight: 1,
    projectorNormalThreshold: 0,
    debug: true
  }
}

For cylindrical decals, use the decal block:

{
  decal: {
    target: 'main',
    url: 'https://cdn.example.com/decal.png',
    decalAngleDeg: 45,
    decalHeight: 0,
    decalScaleX: 1,
    decalScaleY: 1,
    arcWidth: Math.PI / 3,
    heightRange: 1,
    opacity: 1
  }
}

For projector decals, use the top-level decal_projector block. decalProjector is also accepted. In both blocks, target is the model id; projector aim uses projectorTarget.

Model rotation values are Euler XYZ degrees.

Local Development

npm run check

The check verifies that all package entries use bundled Three.js r128.