About this animation
A laser engraving preview with traced letters, sparks, and smoke, with configurable product image, glyph sets, fonts, and accent color.
Why this one
A flat text overlay rarely feels like engraving. Here, each letter follows its actual font outline. The laser tip and trimmed stroke share the same contour progress; the remaining silver mark has a clipped, one-point inset shadow. Per-letter cylindrical projection compresses characters toward the edges, and rotation changes the projected placement. The product fills a 48%-width hero frame, with a warm spotlight behind the original image, an eight-point laser halo with ten-point blur, and sparks that reach 20–40 points. No rectangular reflection layer covers the bottle.
How it works
- The input waits 600 ms after the last keystroke, or starts immediately with Preview engraving.
- Bundled outlines extracted from the actual Geist and Instrument Serif fonts become Skia paths. Skia 2.14.0 has no
font.getPath, and itsMakeFromTextis unavailable on web; local outlines avoid those unsupported calls. - Each path is sampled once by contour arc length. A single UI-thread progress value drives glyph traversal, stroke
end, fill, laser location, sparks, and smoke. No React state changes per animation frame. - Letters project onto a cylinder using sine for position and cosine for horizontal compression. Rotation uses a separate continuous, interruptible shared value, bounded to ±25°.
- Style changes erase the previous mark with a 220 ms clipping sweep before mounting and engraving the new outlines. Touch gestures use Gesture Handler; independent 44-point buttons provide equivalent rotation.
- The demo writes “Alex” at 110 ms per letter, starts engraving by 1.17 seconds, switches to Script at 3.18 seconds, and repeats every 6.1 seconds. Touching stops every pending autoplay timer.
Install
npx expo install expo-image @shopify/react-native-skia react-native-gesture-handler react-native-reanimated react-native-workletsCopy this folder without demo/. Install SDK-compatible versions with Expo. Mount your app inside GestureHandlerRootView and configure Reanimated/worklets for your Expo project. For web, load CanvasKit before importing this component with Skia's WithSkiaWeb or an equivalent lazy-loading boundary. No additional runtime font package is required: outlines ship locally.
Usage
import { LaserEngravePreview } from './laser-engrave-preview';
<LaserEngravePreview
productImage={require('./assets/bottle.png')}
defaultText="Alex"
style={{ flex: 1 }}
/>;import { useRef } from 'react';
import {
LaserEngravePreview,
type LaserEngravePreviewRef,
} from './laser-engrave-preview';
const engraving = useRef<LaserEngravePreviewRef>(null);
<LaserEngravePreview
ref={engraving}
productImage={require('./assets/bottle.png')}
accentColor="#FBBF24"
engravingDuration={1400}
particleCount={8}
fontFamily="YourLoadedInterfaceFont"
onStatusChange={(status) => updateCheckoutStatus(status)}
/>;
// From an event handler:
engraving.current?.setText('Made for you');
engraving.current?.setStyle('Script');
engraving.current?.preview();The ref exposes setText(text), setStyle(style), preview(), and reset(). Imperative calls do not report human interaction. defaultText prepopulates the field; use the preview button or ref to engrave it.
Props
| Prop | Type | Default | Purpose |
|---|---|---|---|
productImage | ImageSourcePropType | Required | Local transparent, tightly cropped product image |
productHeight | number | Available stage height | Display height in points; the demo uses 42% of its measured screen height |
glyphSets | Record<EngravingStyle, EngravingGlyphSet> | Bundled outlines | Override the actual glyph contours for all three styles |
defaultText | string | '' | Initial input value |
defaultStyle | 'Serif' | 'Sans' | 'Script' | 'Serif' | Initial engraving style |
fontFamily | string | System | Interface font, loaded by the host |
styleFontFamilies | Partial style-to-font map | Interface font | Selector labels |
monoFontFamily | string | System | Character counter font |
accentColor | string | '#FBBF24' | Six-digit hex laser and control accent |
engravingDuration | number | 1600 | Full contour traversal in milliseconds, minimum 200 |
particleCount | number | 10 | Spark count, limited to 8–12 |
maxLength | number | 14 | Maximum code points, limited to 1–14 |
colorScheme | 'light' | 'dark' | 'auto' | 'dark' | Interface theme |
placeholder | string | 'Your name or a short line' | Input hint |
accessibilityLabel | string | 'Engraving preview' | Product description |
onInteraction | () => void | — | Human input callback |
onTextChange | (text: string) => void | — | Input changes, including ref calls |
onStyleChange | (style: EngravingStyle) => void | — | Style changes |
onStatusChange | (status: 'engraving' | 'ready') => void | — | Animation lifecycle |
style | StyleProp<ViewStyle> | — | Root layout overrides |
testID | string | — | Root test identifier |
Custom outlines use a 1,000-unit em, a baseline at zero, downward-positive Y coordinates, and an advance width. Bundled glyphs cover printable Latin-1 characters; unsupported characters are omitted rather than producing missing-glyph boxes. Script intentionally uses Instrument Serif Italic. The supplied image fills a responsive 48%-width product frame. The demo sets the display height to 42% of its measured screen height and centers the image around 34% of screen height. The original 188×608 PNG is fitted to this wider display frame without cropping or adding a replacement silhouette; this intentionally changes its displayed aspect ratio. Set productHeight to containerWidth * 0.48 * imageHeight / imageWidth to retain the aspect ratio of your own artwork when enough vertical space is available. Very long lines scale down to fit; short names give the clearest engraving.
Compatibility
Targets Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, worklets 0.13.0, Gesture Handler 3.3, and Skia 2.14.0. Uses modules bundled with the matching Expo Go. iOS, Android, and web are supported in code.
Accessibility
All controls have labels, roles, and at least 44-point hit areas. Style selection announces checked state. Status changes use a polite live region; completion also announces to screen readers. Rotation has dedicated left, front, and right buttons. Reduced motion replaces engraving and style transitions with 200 ms fades, removes rotation, particles, smoke, and moving light, and keeps counts static. Keyboard submission starts the preview.
Assets and licenses
The demo uses the local Tidewell graphite bottle, the Tidewell tile and wordmark from the project brand kit, and a Lucide droplet. Icons: Lucide (ISC), with its license included. Letter outlines are derived from Geist and Instrument Serif under the SIL Open Font License 1.1; license files are included. The image and demo brand are demonstration content and are excluded from the reusable component.




