@heyramzi/ui 0.0.0-stage → 1.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COMPONENTS.md +133 -0
- package/README.md +136 -2
- package/package.json +217 -3
- package/scripts/lib/sync-core.mjs +113 -0
- package/scripts/relative-imports.mjs +39 -0
- package/scripts/sf-symbol.mjs +93 -0
- package/scripts/sync-shadcn.mjs +354 -0
- package/scripts/sync-transitions.mjs +146 -0
- package/scripts/validate-apple-symbol-exports.mjs +45 -0
- package/src/agents/agent-avatar.svelte +592 -0
- package/src/agents/agent-cast.svelte +197 -0
- package/src/agents/demo.svelte +125 -0
- package/src/agents/helpers.ts +58 -0
- package/src/agents/index.ts +11 -0
- package/src/agents/page-state.ts +70 -0
- package/src/agents/types.ts +74 -0
- package/src/agents/vendor/LICENSE +21 -0
- package/src/agents/vendor/color.ts +155 -0
- package/src/agents/vendor/draw.ts +840 -0
- package/src/agents/vendor/engine.ts +845 -0
- package/src/agents/vendor/mesh.ts +838 -0
- package/src/agents/vendor/plastic.ts +4958 -0
- package/src/agents/vendor/presets.ts +103 -0
- package/src/agents/vendor/shapes.ts +41 -0
- package/src/agents/vendor/ticker.ts +67 -0
- package/src/agents/vendor/types.ts +261 -0
- package/src/agents/vendor/wear.ts +1699 -0
- package/src/ai/ai-button/AiButton.svelte +86 -0
- package/src/ai/ai-button/index.ts +4 -0
- package/src/ai/confirmation/Confirmation.svelte +54 -0
- package/src/ai/confirmation/ConfirmationAccepted.svelte +23 -0
- package/src/ai/confirmation/ConfirmationAction.svelte +53 -0
- package/src/ai/confirmation/ConfirmationActions.svelte +24 -0
- package/src/ai/confirmation/ConfirmationFailed.svelte +23 -0
- package/src/ai/confirmation/ConfirmationRejected.svelte +23 -0
- package/src/ai/confirmation/ConfirmationRequest.svelte +25 -0
- package/src/ai/confirmation/ConfirmationTitle.svelte +25 -0
- package/src/ai/confirmation/confirmation-context.svelte.ts +22 -0
- package/src/ai/confirmation/index.ts +50 -0
- package/src/ai/generation-progress/GenerationProgress.svelte +354 -0
- package/src/ai/generation-progress/index.ts +7 -0
- package/src/ai/index.ts +111 -0
- package/src/ai/loading-state/PixelLoader.svelte +138 -0
- package/src/ai/loading-state/index.ts +5 -0
- package/src/ai/prompt-input/PromptInput.svelte +56 -0
- package/src/ai/prompt-input/PromptInputBody.svelte +19 -0
- package/src/ai/prompt-input/PromptInputSubmit.svelte +79 -0
- package/src/ai/prompt-input/PromptInputTextarea.svelte +74 -0
- package/src/ai/prompt-input/PromptInputToolbar.svelte +19 -0
- package/src/ai/prompt-input/index.ts +34 -0
- package/src/ai/reasoning/Reasoning.svelte +50 -0
- package/src/ai/reasoning/ReasoningContent.svelte +27 -0
- package/src/ai/reasoning/ReasoningTrigger.svelte +48 -0
- package/src/ai/reasoning/index.ts +22 -0
- package/src/ai/reasoning/reasoning-context.svelte.ts +20 -0
- package/src/ai/response/Response.css +32 -0
- package/src/ai/response/Response.svelte +130 -0
- package/src/ai/response/index.ts +5 -0
- package/src/ai/suggestion/Suggestion.svelte +43 -0
- package/src/ai/suggestion/Suggestions.svelte +25 -0
- package/src/ai/suggestion/index.ts +14 -0
- package/src/ai/thinking-dots/ThinkingDots.svelte +42 -0
- package/src/ai/thinking-dots/index.ts +5 -0
- package/src/ai/typing-dots/TypingDots.css +39 -0
- package/src/ai/typing-dots/TypingDots.svelte +27 -0
- package/src/ai/typing-dots/index.ts +10 -0
- package/src/ai/voice-waveform/VoiceWaveform.css +40 -0
- package/src/ai/voice-waveform/VoiceWaveform.svelte +46 -0
- package/src/ai/voice-waveform/index.ts +5 -0
- package/src/ambient.d.ts +4 -0
- package/src/animations/README.md +84 -0
- package/src/animations/avatar-group-hover.css +47 -0
- package/src/animations/card-tilt.css +74 -0
- package/src/animations/digit-pop-in.css +78 -0
- package/src/animations/dropdown.css +121 -0
- package/src/animations/index.css +23 -0
- package/src/animations/input-clear-dissolve.css +86 -0
- package/src/animations/input-shake.css +114 -0
- package/src/animations/learn-more-hover.css +48 -0
- package/src/animations/like-button.css +93 -0
- package/src/animations/modal.css +75 -0
- package/src/animations/page-side-by-side.css +63 -0
- package/src/animations/plus-menu-morph.css +97 -0
- package/src/animations/resize.css +20 -0
- package/src/animations/shimmer-text.css +72 -0
- package/src/animations/spinning-counter.css +52 -0
- package/src/animations/success-check.css +120 -0
- package/src/animations/text-swap.css +40 -0
- package/src/animations/text-swap.ts +40 -0
- package/src/animations/thinking-states.css +65 -0
- package/src/animations/upstream-coverage.test.ts +65 -0
- package/src/components/copy/clipboard.test.ts +206 -0
- package/src/components/copy/clipboard.ts +49 -0
- package/src/components/copy/copied.svelte.ts +39 -0
- package/src/components/copy/copy-button.svelte +175 -0
- package/src/components/copy/copy-button.test.ts +110 -0
- package/src/components/copy/index.ts +3 -0
- package/src/components/data-table/data-table.test.ts +134 -0
- package/src/components/data-table/data-table.ts +77 -0
- package/src/components/data-table/index.ts +20 -0
- package/src/components/data-table/infinite-scroll.svelte +69 -0
- package/src/components/data-table/pagination.svelte +62 -0
- package/src/components/data-table/row-arrow.svelte +28 -0
- package/src/components/data-table/row-context-menu.svelte +106 -0
- package/src/components/data-table/row-context-menu.test.ts +18 -0
- package/src/components/data-table/sortable-head.svelte +56 -0
- package/src/components/data-table/table-skeleton.svelte +83 -0
- package/src/components/hold-to-confirm/hold-to-confirm.svelte +232 -0
- package/src/components/hold-to-confirm/index.ts +1 -0
- package/src/components/icon/icon.svelte +23 -0
- package/src/components/icon/index.ts +1 -0
- package/src/components/language-flag/index.ts +1 -0
- package/src/components/language-flag/language-flag.svelte +60 -0
- package/src/components/list-page/detail-empty.svelte +50 -0
- package/src/components/list-page/detail-header.svelte +107 -0
- package/src/components/list-page/detail-panel.svelte +90 -0
- package/src/components/list-page/detail-row.svelte +36 -0
- package/src/components/list-page/detail-stack.svelte +33 -0
- package/src/components/list-page/empty-state.svelte +91 -0
- package/src/components/list-page/filter-item.svelte +49 -0
- package/src/components/list-page/filter-popover.svelte +88 -0
- package/src/components/list-page/filter-section.ts +12 -0
- package/src/components/list-page/grouped-list.svelte +116 -0
- package/src/components/list-page/index.ts +28 -0
- package/src/components/list-page/list-page-shell.svelte +195 -0
- package/src/components/list-page/list-table.ts +19 -0
- package/src/components/list-page/master-detail.svelte +206 -0
- package/src/components/list-page/master-detail.test.ts +37 -0
- package/src/components/list-page/notice-bar.svelte +68 -0
- package/src/components/list-page/prev-next.svelte +104 -0
- package/src/components/list-page/queue-rail.ts +25 -0
- package/src/components/list-page/rail-body.svelte +29 -0
- package/src/components/list-page/rail-empty.svelte +51 -0
- package/src/components/list-page/rail-head.svelte +49 -0
- package/src/components/list-page/rail-row.svelte +163 -0
- package/src/components/list-page/section-host.ts +24 -0
- package/src/components/list-page/section-label.svelte +39 -0
- package/src/components/list-page/step.test.ts +21 -0
- package/src/components/list-page/step.ts +10 -0
- package/src/components/list-page/toolbar-search.svelte +63 -0
- package/src/components/list-page/toolbar-trigger.ts +23 -0
- package/src/components/list-page/types.ts +1 -0
- package/src/components/locale-switch/index.ts +1 -0
- package/src/components/locale-switch/locale-switch.svelte +59 -0
- package/src/components/mark/index.ts +1 -0
- package/src/components/mark/mark.svelte +70 -0
- package/src/components/open-in/index.ts +6 -0
- package/src/components/open-in/open-in.svelte +106 -0
- package/src/components/picker/index.ts +4 -0
- package/src/components/picker/mark-picker.svelte +84 -0
- package/src/components/picker/picker.svelte +143 -0
- package/src/components/picker/trigger.ts +42 -0
- package/src/components/picker/types.ts +26 -0
- package/src/components/segmented/animated-tabs.svelte +175 -0
- package/src/components/segmented/collapse.test.ts +49 -0
- package/src/components/segmented/collapse.ts +39 -0
- package/src/components/segmented/index.ts +41 -0
- package/src/components/segmented/section-runs.svelte +79 -0
- package/src/components/segmented/segment.svelte +106 -0
- package/src/components/segmented/segmented-filter.svelte +125 -0
- package/src/components/segmented/segmented-nav.svelte +82 -0
- package/src/components/segmented/segmented-overflow.svelte +85 -0
- package/src/components/segmented/segmented-track.svelte +181 -0
- package/src/components/segmented/segmented.test.ts +20 -0
- package/src/components/segmented/segmented.ts +80 -0
- package/src/components/star-rating-input/index.ts +9 -0
- package/src/components/star-rating-input/star-rating-input.svelte +256 -0
- package/src/components/svg/index.ts +1 -0
- package/src/components/svg/svg.svelte +9 -0
- package/src/design/index.css +270 -0
- package/src/design/polish.css +159 -0
- package/src/effects/animated-beam/AnimatedBeam.svelte +132 -0
- package/src/effects/animated-beam/diagrams/BeamSvg.svelte +91 -0
- package/src/effects/animated-beam/index.ts +2 -0
- package/src/effects/animated-beam/path-calculator.svelte.ts +70 -0
- package/src/effects/animated-beam/types.ts +23 -0
- package/src/effects/border-beam/BorderBeam.svelte +223 -0
- package/src/effects/border-beam/css.test.ts +46 -0
- package/src/effects/border-beam/css.ts +574 -0
- package/src/effects/border-beam/presets.ts +699 -0
- package/src/effects/border-beam/register-properties.test.ts +74 -0
- package/src/effects/border-beam/register-properties.ts +73 -0
- package/src/effects/border-beam/types.ts +38 -0
- package/src/effects/image-comparison/ImageComparison.css +100 -0
- package/src/effects/image-comparison/ImageComparison.svelte +209 -0
- package/src/effects/image-comparison/compare.test.ts +17 -0
- package/src/effects/image-comparison/compare.ts +5 -0
- package/src/effects/image-reveal/ImageReveal.css +10 -0
- package/src/effects/image-reveal/ImageReveal.svelte +357 -0
- package/src/effects/image-reveal/index.ts +2 -0
- package/src/effects/image-reveal/types.ts +35 -0
- package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +338 -0
- package/src/effects/magic-card/MagicCard.css +47 -0
- package/src/effects/magic-card/MagicCard.svelte +57 -0
- package/src/effects/magnetic/magnetic.ts +81 -0
- package/src/effects/marquee/Marquee.css +78 -0
- package/src/effects/marquee/Marquee.svelte +160 -0
- package/src/effects/progressive-blur/ProgressiveBlur.css +9 -0
- package/src/effects/progressive-blur/ProgressiveBlur.svelte +63 -0
- package/src/effects/reveal/AnimatedGroup.svelte +101 -0
- package/src/effects/reveal/InView.svelte +77 -0
- package/src/effects/reveal/TextEffect.svelte +142 -0
- package/src/effects/reveal/index.ts +11 -0
- package/src/effects/reveal/observe.test.ts +60 -0
- package/src/effects/reveal/observe.ts +65 -0
- package/src/effects/reveal/reveal.css +141 -0
- package/src/effects/reveal/types.ts +32 -0
- package/src/effects/shader-gradient/ShaderGradient.svelte +227 -0
- package/src/effects/shader-gradient/color.ts +44 -0
- package/src/effects/shader-gradient/index.ts +5 -0
- package/src/effects/shader-gradient/renderer.ts +456 -0
- package/src/effects/shader-gradient/shaders.ts +324 -0
- package/src/effects/shader-gradient/types.ts +68 -0
- package/src/effects/spotlight/Spotlight.css +14 -0
- package/src/effects/spotlight/Spotlight.svelte +61 -0
- package/src/effects/theme-toggle/ThemeToggle.css +214 -0
- package/src/effects/theme-toggle/ThemeToggle.svelte +126 -0
- package/src/gallery.test.ts +52 -0
- package/src/icons/TradeIcon.svelte +16 -0
- package/src/icons/apple-symbol-exports.ts +135 -0
- package/src/icons/sf-symbols.ts +527 -0
- package/src/icons/trades.ts +89 -0
- package/src/index.ts +53 -0
- package/src/lib/arrive.ts +26 -0
- package/src/lib/icon-types.ts +17 -0
- package/src/lib/layout.ts +8 -0
- package/src/lib/panel.ts +21 -0
- package/src/lib/sliding-pill.ts +68 -0
- package/src/lib/theme-transition.ts +79 -0
- package/src/lib/tone.ts +48 -0
- package/src/shadcn/accordion/accordion-content.svelte +22 -0
- package/src/shadcn/accordion/accordion-item.svelte +17 -0
- package/src/shadcn/accordion/accordion-trigger.svelte +44 -0
- package/src/shadcn/accordion/accordion.svelte +19 -0
- package/src/shadcn/accordion/index.ts +16 -0
- package/src/shadcn/alert/alert-description.svelte +23 -0
- package/src/shadcn/alert/alert-title.svelte +20 -0
- package/src/shadcn/alert/alert.svelte +46 -0
- package/src/shadcn/alert/index.ts +14 -0
- package/src/shadcn/alert-dialog/alert-dialog-action.svelte +18 -0
- package/src/shadcn/alert-dialog/alert-dialog-cancel.svelte +18 -0
- package/src/shadcn/alert-dialog/alert-dialog-content.svelte +29 -0
- package/src/shadcn/alert-dialog/alert-dialog-description.svelte +17 -0
- package/src/shadcn/alert-dialog/alert-dialog-footer.svelte +20 -0
- package/src/shadcn/alert-dialog/alert-dialog-header.svelte +20 -0
- package/src/shadcn/alert-dialog/alert-dialog-overlay.svelte +17 -0
- package/src/shadcn/alert-dialog/alert-dialog-portal.svelte +7 -0
- package/src/shadcn/alert-dialog/alert-dialog-title.svelte +17 -0
- package/src/shadcn/alert-dialog/alert-dialog-trigger.svelte +7 -0
- package/src/shadcn/alert-dialog/alert-dialog.svelte +7 -0
- package/src/shadcn/alert-dialog/index.ts +37 -0
- package/src/shadcn/attachment/attachment-action.svelte +23 -0
- package/src/shadcn/attachment/attachment-actions.svelte +23 -0
- package/src/shadcn/attachment/attachment-content.svelte +23 -0
- package/src/shadcn/attachment/attachment-description.svelte +23 -0
- package/src/shadcn/attachment/attachment-group.svelte +23 -0
- package/src/shadcn/attachment/attachment-media.svelte +44 -0
- package/src/shadcn/attachment/attachment-title.svelte +23 -0
- package/src/shadcn/attachment/attachment-trigger.svelte +29 -0
- package/src/shadcn/attachment/attachment.svelte +55 -0
- package/src/shadcn/attachment/index.ts +40 -0
- package/src/shadcn/avatar/avatar-badge.svelte +26 -0
- package/src/shadcn/avatar/avatar-fallback.svelte +20 -0
- package/src/shadcn/avatar/avatar-group-count.svelte +23 -0
- package/src/shadcn/avatar/avatar-group.svelte +23 -0
- package/src/shadcn/avatar/avatar-image.svelte +17 -0
- package/src/shadcn/avatar/avatar.svelte +26 -0
- package/src/shadcn/avatar/index.ts +22 -0
- package/src/shadcn/badge/badge.svelte +54 -0
- package/src/shadcn/badge/index.ts +2 -0
- package/src/shadcn/breadcrumb/breadcrumb-ellipsis.svelte +23 -0
- package/src/shadcn/breadcrumb/breadcrumb-item.svelte +20 -0
- package/src/shadcn/breadcrumb/breadcrumb-link.svelte +31 -0
- package/src/shadcn/breadcrumb/breadcrumb-list.svelte +23 -0
- package/src/shadcn/breadcrumb/breadcrumb-page.svelte +23 -0
- package/src/shadcn/breadcrumb/breadcrumb-separator.svelte +27 -0
- package/src/shadcn/breadcrumb/breadcrumb.svelte +21 -0
- package/src/shadcn/breadcrumb/index.ts +25 -0
- package/src/shadcn/bubble/bubble-content.svelte +31 -0
- package/src/shadcn/bubble/bubble-group.svelte +20 -0
- package/src/shadcn/bubble/bubble-reactions.svelte +52 -0
- package/src/shadcn/bubble/bubble.svelte +58 -0
- package/src/shadcn/bubble/index.ts +25 -0
- package/src/shadcn/button/button.svelte +121 -0
- package/src/shadcn/button/index.ts +17 -0
- package/src/shadcn/calendar/calendar-caption.svelte +76 -0
- package/src/shadcn/calendar/calendar-cell.svelte +19 -0
- package/src/shadcn/calendar/calendar-day.svelte +35 -0
- package/src/shadcn/calendar/calendar-grid-body.svelte +12 -0
- package/src/shadcn/calendar/calendar-grid-head.svelte +12 -0
- package/src/shadcn/calendar/calendar-grid-row.svelte +12 -0
- package/src/shadcn/calendar/calendar-grid.svelte +16 -0
- package/src/shadcn/calendar/calendar-head-cell.svelte +19 -0
- package/src/shadcn/calendar/calendar-header.svelte +19 -0
- package/src/shadcn/calendar/calendar-heading.svelte +16 -0
- package/src/shadcn/calendar/calendar-month-select.svelte +44 -0
- package/src/shadcn/calendar/calendar-month.svelte +15 -0
- package/src/shadcn/calendar/calendar-months.svelte +19 -0
- package/src/shadcn/calendar/calendar-nav.svelte +19 -0
- package/src/shadcn/calendar/calendar-next-button.svelte +31 -0
- package/src/shadcn/calendar/calendar-prev-button.svelte +31 -0
- package/src/shadcn/calendar/calendar-year-select.svelte +43 -0
- package/src/shadcn/calendar/calendar.svelte +124 -0
- package/src/shadcn/calendar/index.ts +40 -0
- package/src/shadcn/card/card-action.svelte +23 -0
- package/src/shadcn/card/card-content.svelte +20 -0
- package/src/shadcn/card/card-description.svelte +20 -0
- package/src/shadcn/card/card-footer.svelte +23 -0
- package/src/shadcn/card/card-header.svelte +23 -0
- package/src/shadcn/card/card-title.svelte +29 -0
- package/src/shadcn/card/card.svelte +25 -0
- package/src/shadcn/card/index.ts +25 -0
- package/src/shadcn/checkbox/checkbox.svelte +38 -0
- package/src/shadcn/checkbox/index.ts +6 -0
- package/src/shadcn/code-block/code-block-actions.svelte +23 -0
- package/src/shadcn/code-block/code-block-content.svelte +55 -0
- package/src/shadcn/code-block/code-block-copy-button.svelte +10 -0
- package/src/shadcn/code-block/code-block-footer.svelte +20 -0
- package/src/shadcn/code-block/code-block-header.svelte +23 -0
- package/src/shadcn/code-block/code-block-line.svelte +71 -0
- package/src/shadcn/code-block/code-block-title.svelte +23 -0
- package/src/shadcn/code-block/code-block.svelte +47 -0
- package/src/shadcn/code-block/index.ts +42 -0
- package/src/shadcn/collapsible/collapsible-content.svelte +7 -0
- package/src/shadcn/collapsible/collapsible-trigger.svelte +7 -0
- package/src/shadcn/collapsible/collapsible.svelte +11 -0
- package/src/shadcn/collapsible/index.ts +13 -0
- package/src/shadcn/command/command-dialog.svelte +66 -0
- package/src/shadcn/command/command-empty.svelte +17 -0
- package/src/shadcn/command/command-group.svelte +40 -0
- package/src/shadcn/command/command-input.svelte +64 -0
- package/src/shadcn/command/command-item.svelte +37 -0
- package/src/shadcn/command/command-link-item.svelte +20 -0
- package/src/shadcn/command/command-list.svelte +20 -0
- package/src/shadcn/command/command-loading.svelte +7 -0
- package/src/shadcn/command/command-separator.svelte +17 -0
- package/src/shadcn/command/command-shortcut.svelte +23 -0
- package/src/shadcn/command/command.svelte +28 -0
- package/src/shadcn/command/index.ts +40 -0
- package/src/shadcn/context-menu/context-menu-checkbox-item.svelte +41 -0
- package/src/shadcn/context-menu/context-menu-content.svelte +28 -0
- package/src/shadcn/context-menu/context-menu-group-heading.svelte +21 -0
- package/src/shadcn/context-menu/context-menu-group.svelte +7 -0
- package/src/shadcn/context-menu/context-menu-item.svelte +27 -0
- package/src/shadcn/context-menu/context-menu-label.svelte +27 -0
- package/src/shadcn/context-menu/context-menu-portal.svelte +7 -0
- package/src/shadcn/context-menu/context-menu-radio-group.svelte +16 -0
- package/src/shadcn/context-menu/context-menu-radio-item.svelte +35 -0
- package/src/shadcn/context-menu/context-menu-separator.svelte +17 -0
- package/src/shadcn/context-menu/context-menu-shortcut.svelte +23 -0
- package/src/shadcn/context-menu/context-menu-sub-content.svelte +25 -0
- package/src/shadcn/context-menu/context-menu-sub-trigger.svelte +29 -0
- package/src/shadcn/context-menu/context-menu-sub.svelte +7 -0
- package/src/shadcn/context-menu/context-menu-trigger.svelte +17 -0
- package/src/shadcn/context-menu/context-menu.svelte +7 -0
- package/src/shadcn/context-menu/index.ts +52 -0
- package/src/shadcn/dialog/dialog-close.svelte +11 -0
- package/src/shadcn/dialog/dialog-content.svelte +57 -0
- package/src/shadcn/dialog/dialog-description.svelte +20 -0
- package/src/shadcn/dialog/dialog-footer.svelte +32 -0
- package/src/shadcn/dialog/dialog-header.svelte +20 -0
- package/src/shadcn/dialog/dialog-overlay.svelte +20 -0
- package/src/shadcn/dialog/dialog-portal.svelte +7 -0
- package/src/shadcn/dialog/dialog-title.svelte +17 -0
- package/src/shadcn/dialog/dialog-trigger.svelte +11 -0
- package/src/shadcn/dialog/dialog.svelte +7 -0
- package/src/shadcn/dialog/index.ts +34 -0
- package/src/shadcn/dropdown-menu/dropdown-menu-checkbox-group.svelte +16 -0
- package/src/shadcn/dropdown-menu/dropdown-menu-checkbox-item.svelte +43 -0
- package/src/shadcn/dropdown-menu/dropdown-menu-content.svelte +31 -0
- package/src/shadcn/dropdown-menu/dropdown-menu-group-heading.svelte +22 -0
- package/src/shadcn/dropdown-menu/dropdown-menu-group.svelte +7 -0
- package/src/shadcn/dropdown-menu/dropdown-menu-item.svelte +29 -0
- package/src/shadcn/dropdown-menu/dropdown-menu-label.svelte +27 -0
- package/src/shadcn/dropdown-menu/dropdown-menu-portal.svelte +7 -0
- package/src/shadcn/dropdown-menu/dropdown-menu-radio-group.svelte +16 -0
- package/src/shadcn/dropdown-menu/dropdown-menu-radio-item.svelte +34 -0
- package/src/shadcn/dropdown-menu/dropdown-menu-separator.svelte +17 -0
- package/src/shadcn/dropdown-menu/dropdown-menu-shortcut.svelte +23 -0
- package/src/shadcn/dropdown-menu/dropdown-menu-sub-content.svelte +20 -0
- package/src/shadcn/dropdown-menu/dropdown-menu-sub-trigger.svelte +29 -0
- package/src/shadcn/dropdown-menu/dropdown-menu-sub.svelte +7 -0
- package/src/shadcn/dropdown-menu/dropdown-menu-trigger.svelte +7 -0
- package/src/shadcn/dropdown-menu/dropdown-menu.svelte +7 -0
- package/src/shadcn/dropdown-menu/index.ts +56 -0
- package/src/shadcn/dropdown-menu/menu-item.ts +43 -0
- package/src/shadcn/empty/empty-content.svelte +23 -0
- package/src/shadcn/empty/empty-description.svelte +23 -0
- package/src/shadcn/empty/empty-header.svelte +20 -0
- package/src/shadcn/empty/empty-media.svelte +41 -0
- package/src/shadcn/empty/empty-title.svelte +20 -0
- package/src/shadcn/empty/empty.svelte +23 -0
- package/src/shadcn/empty/index.ts +22 -0
- package/src/shadcn/field/field-content.svelte +20 -0
- package/src/shadcn/field/field-description.svelte +25 -0
- package/src/shadcn/field/field-error.svelte +58 -0
- package/src/shadcn/field/field-group.svelte +23 -0
- package/src/shadcn/field/field-label.svelte +26 -0
- package/src/shadcn/field/field-legend.svelte +29 -0
- package/src/shadcn/field/field-separator.svelte +35 -0
- package/src/shadcn/field/field-set.svelte +24 -0
- package/src/shadcn/field/field-title.svelte +23 -0
- package/src/shadcn/field/field.svelte +53 -0
- package/src/shadcn/field/index.ts +36 -0
- package/src/shadcn/field/simple-field.svelte +71 -0
- package/src/shadcn/field/simple-field.test.ts +47 -0
- package/src/shadcn/hooks/highlighter.ts +132 -0
- package/src/shadcn/hooks/is-mobile.svelte.ts +9 -0
- package/src/shadcn/hooks/use-highlighted-code.svelte.ts +98 -0
- package/src/shadcn/hover-card/hover-card-content.svelte +31 -0
- package/src/shadcn/hover-card/hover-card-portal.svelte +7 -0
- package/src/shadcn/hover-card/hover-card-trigger.svelte +7 -0
- package/src/shadcn/hover-card/hover-card.svelte +7 -0
- package/src/shadcn/hover-card/index.ts +15 -0
- package/src/shadcn/input/index.ts +9 -0
- package/src/shadcn/input/input.svelte +48 -0
- package/src/shadcn/input/secret-input.svelte +75 -0
- package/src/shadcn/input-otp/index.ts +15 -0
- package/src/shadcn/input-otp/input-otp-group.svelte +20 -0
- package/src/shadcn/input-otp/input-otp-separator.svelte +25 -0
- package/src/shadcn/input-otp/input-otp-slot.svelte +26 -0
- package/src/shadcn/input-otp/input-otp.svelte +22 -0
- package/src/shadcn/kbd/index.ts +10 -0
- package/src/shadcn/kbd/kbd-group.svelte +20 -0
- package/src/shadcn/kbd/kbd.svelte +23 -0
- package/src/shadcn/label/index.ts +7 -0
- package/src/shadcn/label/label.svelte +20 -0
- package/src/shadcn/marker/index.ts +15 -0
- package/src/shadcn/marker/marker-content.svelte +23 -0
- package/src/shadcn/marker/marker-icon.svelte +21 -0
- package/src/shadcn/marker/marker.svelte +52 -0
- package/src/shadcn/message/index.ts +22 -0
- package/src/shadcn/message/message-avatar.svelte +23 -0
- package/src/shadcn/message/message-content.svelte +23 -0
- package/src/shadcn/message/message-footer.svelte +23 -0
- package/src/shadcn/message/message-group.svelte +20 -0
- package/src/shadcn/message/message-header.svelte +23 -0
- package/src/shadcn/message/message.svelte +27 -0
- package/src/shadcn/message-scroller/index.ts +28 -0
- package/src/shadcn/message-scroller/message-scroller-button.svelte +52 -0
- package/src/shadcn/message-scroller/message-scroller-content.svelte +17 -0
- package/src/shadcn/message-scroller/message-scroller-item.svelte +22 -0
- package/src/shadcn/message-scroller/message-scroller-provider.svelte +7 -0
- package/src/shadcn/message-scroller/message-scroller-viewport.svelte +20 -0
- package/src/shadcn/message-scroller/message-scroller.svelte +20 -0
- package/src/shadcn/popover/index.ts +28 -0
- package/src/shadcn/popover/popover-close.svelte +7 -0
- package/src/shadcn/popover/popover-content.svelte +38 -0
- package/src/shadcn/popover/popover-description.svelte +20 -0
- package/src/shadcn/popover/popover-header.svelte +20 -0
- package/src/shadcn/popover/popover-portal.svelte +7 -0
- package/src/shadcn/popover/popover-title.svelte +15 -0
- package/src/shadcn/popover/popover-trigger.svelte +17 -0
- package/src/shadcn/popover/popover.svelte +7 -0
- package/src/shadcn/progress/index.ts +7 -0
- package/src/shadcn/progress/progress.svelte +30 -0
- package/src/shadcn/radio-group/index.ts +10 -0
- package/src/shadcn/radio-group/radio-group-item.svelte +32 -0
- package/src/shadcn/radio-group/radio-group.svelte +19 -0
- package/src/shadcn/scroll-area/index.ts +10 -0
- package/src/shadcn/scroll-area/scroll-area-scrollbar.svelte +30 -0
- package/src/shadcn/scroll-area/scroll-area.svelte +43 -0
- package/src/shadcn/select/index.ts +37 -0
- package/src/shadcn/select/select-content.svelte +45 -0
- package/src/shadcn/select/select-group-heading.svelte +21 -0
- package/src/shadcn/select/select-group.svelte +17 -0
- package/src/shadcn/select/select-item.svelte +38 -0
- package/src/shadcn/select/select-label.svelte +20 -0
- package/src/shadcn/select/select-portal.svelte +7 -0
- package/src/shadcn/select/select-scroll-down-button.svelte +23 -0
- package/src/shadcn/select/select-scroll-up-button.svelte +23 -0
- package/src/shadcn/select/select-separator.svelte +18 -0
- package/src/shadcn/select/select-trigger.svelte +33 -0
- package/src/shadcn/select/select.svelte +11 -0
- package/src/shadcn/separator/index.ts +7 -0
- package/src/shadcn/separator/separator.svelte +23 -0
- package/src/shadcn/sheet/index.ts +34 -0
- package/src/shadcn/sheet/sheet-close.svelte +7 -0
- package/src/shadcn/sheet/sheet-content.svelte +73 -0
- package/src/shadcn/sheet/sheet-description.svelte +17 -0
- package/src/shadcn/sheet/sheet-footer.svelte +20 -0
- package/src/shadcn/sheet/sheet-header.svelte +20 -0
- package/src/shadcn/sheet/sheet-overlay.svelte +20 -0
- package/src/shadcn/sheet/sheet-portal.svelte +7 -0
- package/src/shadcn/sheet/sheet-title.svelte +17 -0
- package/src/shadcn/sheet/sheet-trigger.svelte +7 -0
- package/src/shadcn/sheet/sheet.css +48 -0
- package/src/shadcn/sheet/sheet.svelte +7 -0
- package/src/shadcn/sidebar/constants.ts +6 -0
- package/src/shadcn/sidebar/context.svelte.ts +81 -0
- package/src/shadcn/sidebar/index.ts +75 -0
- package/src/shadcn/sidebar/sidebar-content.svelte +24 -0
- package/src/shadcn/sidebar/sidebar-footer.svelte +21 -0
- package/src/shadcn/sidebar/sidebar-group-action.svelte +33 -0
- package/src/shadcn/sidebar/sidebar-group-content.svelte +21 -0
- package/src/shadcn/sidebar/sidebar-group-label.svelte +33 -0
- package/src/shadcn/sidebar/sidebar-group.svelte +21 -0
- package/src/shadcn/sidebar/sidebar-header.svelte +21 -0
- package/src/shadcn/sidebar/sidebar-input.svelte +21 -0
- package/src/shadcn/sidebar/sidebar-inset.svelte +23 -0
- package/src/shadcn/sidebar/sidebar-menu-action.svelte +37 -0
- package/src/shadcn/sidebar/sidebar-menu-badge.svelte +24 -0
- package/src/shadcn/sidebar/sidebar-menu-button.svelte +103 -0
- package/src/shadcn/sidebar/sidebar-menu-item.svelte +21 -0
- package/src/shadcn/sidebar/sidebar-menu-skeleton.svelte +36 -0
- package/src/shadcn/sidebar/sidebar-menu-sub-button.svelte +39 -0
- package/src/shadcn/sidebar/sidebar-menu-sub-item.svelte +21 -0
- package/src/shadcn/sidebar/sidebar-menu-sub.svelte +24 -0
- package/src/shadcn/sidebar/sidebar-menu.svelte +21 -0
- package/src/shadcn/sidebar/sidebar-provider.svelte +62 -0
- package/src/shadcn/sidebar/sidebar-rail.svelte +36 -0
- package/src/shadcn/sidebar/sidebar-separator.svelte +19 -0
- package/src/shadcn/sidebar/sidebar-trigger.svelte +37 -0
- package/src/shadcn/sidebar/sidebar.svelte +109 -0
- package/src/shadcn/skeleton/index.ts +7 -0
- package/src/shadcn/skeleton/skeleton.svelte +17 -0
- package/src/shadcn/slider/index.ts +7 -0
- package/src/shadcn/slider/slider.svelte +52 -0
- package/src/shadcn/sonner/index.ts +1 -0
- package/src/shadcn/sonner/sonner.svelte +31 -0
- package/src/shadcn/spinner/index.ts +1 -0
- package/src/shadcn/spinner/spinner.svelte +26 -0
- package/src/shadcn/switch/index.ts +7 -0
- package/src/shadcn/switch/switch.svelte +31 -0
- package/src/shadcn/table/index.ts +28 -0
- package/src/shadcn/table/table-body.svelte +20 -0
- package/src/shadcn/table/table-caption.svelte +20 -0
- package/src/shadcn/table/table-cell.svelte +20 -0
- package/src/shadcn/table/table-footer.svelte +20 -0
- package/src/shadcn/table/table-head.svelte +23 -0
- package/src/shadcn/table/table-header.svelte +20 -0
- package/src/shadcn/table/table-row.svelte +23 -0
- package/src/shadcn/table/table.svelte +25 -0
- package/src/shadcn/tabs/index.ts +16 -0
- package/src/shadcn/tabs/tabs-content.svelte +17 -0
- package/src/shadcn/tabs/tabs-list.svelte +36 -0
- package/src/shadcn/tabs/tabs-trigger.svelte +20 -0
- package/src/shadcn/tabs/tabs.svelte +19 -0
- package/src/shadcn/textarea/index.ts +7 -0
- package/src/shadcn/textarea/textarea.svelte +23 -0
- package/src/shadcn/toggle/index.ts +13 -0
- package/src/shadcn/toggle/toggle.svelte +53 -0
- package/src/shadcn/toggle-group/index.ts +10 -0
- package/src/shadcn/toggle-group/toggle-group-item.svelte +35 -0
- package/src/shadcn/toggle-group/toggle-group.svelte +66 -0
- package/src/shadcn/tooltip/context.ts +16 -0
- package/src/shadcn/tooltip/index.ts +19 -0
- package/src/shadcn/tooltip/tooltip-content.svelte +38 -0
- package/src/shadcn/tooltip/tooltip-portal.svelte +7 -0
- package/src/shadcn/tooltip/tooltip-provider.svelte +16 -0
- package/src/shadcn/tooltip/tooltip-trigger.svelte +7 -0
- package/src/shadcn/tooltip/tooltip.svelte +19 -0
- package/src/shadcn/trace/index.ts +29 -0
- package/src/shadcn/trace/trace-collapsible-content.svelte +29 -0
- package/src/shadcn/trace/trace-collapsible-trigger.svelte +29 -0
- package/src/shadcn/trace/trace-collapsible.svelte +20 -0
- package/src/shadcn/trace/trace-description.svelte +20 -0
- package/src/shadcn/trace/trace-icon.svelte +24 -0
- package/src/shadcn/trace/trace-line.svelte +31 -0
- package/src/shadcn/trace/trace-title.svelte +20 -0
- package/src/shadcn/trace/trace.svelte +23 -0
- package/src/shadcn/utilities.css +549 -0
- package/src/test-setup.ts +34 -0
- package/src/tokens.css +993 -0
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
// @ts-nocheck WHY: vendored engine, kept as upstream wrote it so a re-sync stays a clean diff. Strict flags in consumers (review) would flag ~870 spots.
|
|
2
|
+
/* The renderer. The body is a stack of copies of its outline, spaced along
|
|
3
|
+
a depth axis with a pillow profile (smaller at the caps); each copy is
|
|
4
|
+
projected with the head's yaw and pitch, so the stack reads as a rounded
|
|
5
|
+
extruded solid that turns, flips and shows its side. Nearest copies are
|
|
6
|
+
lit with a directional gradient, the far side sits in shade. The face
|
|
7
|
+
lives on the front cap and follows it. */
|
|
8
|
+
|
|
9
|
+
import type { Pose } from "./engine";
|
|
10
|
+
import type { BotAvatarFace, BotAvatarShading } from "./types";
|
|
11
|
+
import { shade } from "./color";
|
|
12
|
+
import { drawPlasticCap, mulAffine, surfaceAt, type FurStyle } from "./plastic";
|
|
13
|
+
import {
|
|
14
|
+
drawGlasses,
|
|
15
|
+
drawWearBehind,
|
|
16
|
+
drawWearFront,
|
|
17
|
+
type EyeSpot,
|
|
18
|
+
type Wear,
|
|
19
|
+
type WearRig,
|
|
20
|
+
} from "./wear";
|
|
21
|
+
|
|
22
|
+
export interface DrawConfig {
|
|
23
|
+
path: Path2D;
|
|
24
|
+
face: BotAvatarFace;
|
|
25
|
+
faceX: number;
|
|
26
|
+
faceY: number;
|
|
27
|
+
faceScale: number;
|
|
28
|
+
color: string;
|
|
29
|
+
ink: string;
|
|
30
|
+
shading: BotAvatarShading;
|
|
31
|
+
/** intensities and geometry of the lighting; omitted means the stock look */
|
|
32
|
+
shadow?: number;
|
|
33
|
+
highlight?: number;
|
|
34
|
+
depth?: number;
|
|
35
|
+
/** degrees clockwise from the top, where the light comes from */
|
|
36
|
+
light?: number;
|
|
37
|
+
rim?: number;
|
|
38
|
+
spread?: number;
|
|
39
|
+
/** plastic and fabric: the profile through the depth — 1 (the default) a
|
|
40
|
+
cushion, thickest in the middle and rounding off to nothing at the
|
|
41
|
+
outline all round; toward 0 a slab with softened edges */
|
|
42
|
+
roundness?: number;
|
|
43
|
+
/** fabric: the pile's style — length, density, edge fuzz, tufts, curl */
|
|
44
|
+
fur?: FurStyle;
|
|
45
|
+
/** plastic and fabric: 0–1, how much colour the light keeps past the
|
|
46
|
+
palette's full saturation — highlights, sheen and rim stay coloured
|
|
47
|
+
rather than paling toward white */
|
|
48
|
+
vivid?: number;
|
|
49
|
+
/** fabric: where the back light comes from, degrees clockwise from the
|
|
50
|
+
top; by default above the toy, toward the side away from the key */
|
|
51
|
+
backLight?: number;
|
|
52
|
+
/** fabric: how far round to the front the key sits, degrees — 0 grazes
|
|
53
|
+
the toy from the side, 90 lights it from the camera (default 32) */
|
|
54
|
+
lightFront?: number;
|
|
55
|
+
/** fabric: the strength of the highlights along single fibres (default 0) */
|
|
56
|
+
shine?: number;
|
|
57
|
+
/** fabric: 0–1, how soft the back light is — 0 a small source caught on
|
|
58
|
+
single fibres, 1 a large one, an even glow (default 1) */
|
|
59
|
+
backSoftness?: number;
|
|
60
|
+
/** fabric: the smooth, satin sheen over the body's edge — the key's
|
|
61
|
+
sheen and the back light's band as even gradients (default 0) */
|
|
62
|
+
sheen?: number;
|
|
63
|
+
/** identifies the outline for the material caches (the type name) */
|
|
64
|
+
typeKey?: string;
|
|
65
|
+
/** no animation loop follows this draw (reduced motion, paused): build
|
|
66
|
+
* materials now instead of on idle time */
|
|
67
|
+
still?: boolean;
|
|
68
|
+
/** with `still`, bake fabric's pile on idle time all the same; the caller
|
|
69
|
+
* draws again when it lands (`takeMissed` and `onLanded` in plastic.ts),
|
|
70
|
+
* as the component does */
|
|
71
|
+
furOnIdle?: boolean;
|
|
72
|
+
/** thin parts (antennae) drawn behind the body with `partsDepth` of its
|
|
73
|
+
depth; a list draws each piece as its own solid */
|
|
74
|
+
parts?: Path2D | Path2D[];
|
|
75
|
+
partsDepth?: number;
|
|
76
|
+
/** the resolved surface: the whirl is white on dark, black on light */
|
|
77
|
+
theme?: "dark" | "light";
|
|
78
|
+
/** the device pixel ratio the context is scaled by: with it given the
|
|
79
|
+
context's transform is taken as that scale and never read back */
|
|
80
|
+
dpr?: number;
|
|
81
|
+
/** plastic's side slices: filled as vectors, or blitted from sprites of
|
|
82
|
+
the outline. `auto` (the default) blits on WebKit, where a
|
|
83
|
+
conic-gradient fill costs thirty times a flat one. */
|
|
84
|
+
sides?: "auto" | "vector" | "sprite";
|
|
85
|
+
/** the whirl's knobs; 1 everywhere is the stock look */
|
|
86
|
+
whirl?: { strength: number; size: number; width: number; length: number; tilt: number };
|
|
87
|
+
/** what the bot wears: a hat, glasses, headphones, a bow tie */
|
|
88
|
+
wear?: Wear;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* The canvas is drawn larger than the avatar's layout box, so a hop or a
|
|
92
|
+
flip can leave the box without being clipped. */
|
|
93
|
+
export const OVERSCAN = 1.5;
|
|
94
|
+
/** the body's centre sits this fraction of the box below the canvas
|
|
95
|
+
centre: hops and flips need the room above, not below */
|
|
96
|
+
export const RISE = 0.1;
|
|
97
|
+
|
|
98
|
+
/* Copies through the depth, and the stock half-depth in body units. */
|
|
99
|
+
const SLICES = 17;
|
|
100
|
+
const HALF_DEPTH = 15;
|
|
101
|
+
/* Cap scale at the ends of the pillow, at the stock rim width. */
|
|
102
|
+
const CAP = 0.9;
|
|
103
|
+
const profile = (z: number, cap: number) => cap + (1 - cap) * Math.sqrt(Math.max(0, 1 - z * z));
|
|
104
|
+
|
|
105
|
+
/* Big, plain dark eyes: they carry the face at 24px. */
|
|
106
|
+
const EYE_GAP = 25;
|
|
107
|
+
const EYE_RX = 6.3;
|
|
108
|
+
const EYE_Y = { eyes: 1, mouth: -3.5 } as const;
|
|
109
|
+
|
|
110
|
+
interface Palette {
|
|
111
|
+
base: string;
|
|
112
|
+
far: string;
|
|
113
|
+
near: string;
|
|
114
|
+
light: string;
|
|
115
|
+
dark: string;
|
|
116
|
+
capTop: string;
|
|
117
|
+
capBottom: string;
|
|
118
|
+
/** the slice colours by draw order (far → near), crisp and smooth */
|
|
119
|
+
crispMix: string[];
|
|
120
|
+
smoothMix: string[];
|
|
121
|
+
/** crisp: the lit side and cap gradients, for a light direction */
|
|
122
|
+
grad: { lx: number; ly: number; lit: CanvasGradient; cap: CanvasGradient } | null;
|
|
123
|
+
}
|
|
124
|
+
const paletteCache = new Map<string, Palette>();
|
|
125
|
+
function palette(color: string, shadow: number, highlight: number): Palette {
|
|
126
|
+
const key = `${color}|${shadow}|${highlight}`;
|
|
127
|
+
let p = paletteCache.get(key);
|
|
128
|
+
if (!p) {
|
|
129
|
+
const far = shade(color, -0.3 * shadow, 0.05 * shadow);
|
|
130
|
+
const near = shade(color, -0.12 * shadow, 0.03 * shadow);
|
|
131
|
+
const crispMix: string[] = [],
|
|
132
|
+
smoothMix: string[] = [];
|
|
133
|
+
for (let j = 0; j < SLICES; j++) {
|
|
134
|
+
const t = j / (SLICES - 1);
|
|
135
|
+
crispMix.push(t > 0.6 ? "" : mixCss(far, near, t / 0.6));
|
|
136
|
+
smoothMix.push(t >= 0.5 ? color : mixCss(far, color, t / 0.5));
|
|
137
|
+
}
|
|
138
|
+
p = {
|
|
139
|
+
base: color,
|
|
140
|
+
far,
|
|
141
|
+
near,
|
|
142
|
+
light: shade(color, 0.04 * highlight),
|
|
143
|
+
dark: shade(color, -0.3 * shadow, 0.05 * shadow),
|
|
144
|
+
capTop: shade(color, 0.035 * highlight),
|
|
145
|
+
capBottom: shade(color, -0.035 * shadow),
|
|
146
|
+
crispMix,
|
|
147
|
+
smoothMix,
|
|
148
|
+
grad: null,
|
|
149
|
+
};
|
|
150
|
+
if (paletteCache.size > 200) paletteCache.clear();
|
|
151
|
+
paletteCache.set(key, p);
|
|
152
|
+
}
|
|
153
|
+
return p;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* the numbers of an hsl() string; any other colour is normalised through shade() first */
|
|
157
|
+
const hslNums = (c: string) =>
|
|
158
|
+
(c.startsWith("hsl(") ? c : shade(c, 0)).match(/[\d.]+/g)!.map(Number);
|
|
159
|
+
function mixCss(a: string, b: string, t: number): string {
|
|
160
|
+
/* interpolate the hsl numbers */
|
|
161
|
+
const pa = hslNums(a);
|
|
162
|
+
const pb = hslNums(b);
|
|
163
|
+
const m = pa.map((v, i) => v + (pb[i] - v) * t);
|
|
164
|
+
return `hsl(${m[0].toFixed(1)} ${m[1].toFixed(1)}% ${m[2].toFixed(1)}%)`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* The whirl: the cartoon motion round a spinning body — one tapered
|
|
168
|
+
trail on a tilted ring, made of the body's own material: a translucent
|
|
169
|
+
plastic tube in the body colour, lit from the same light — a lighter
|
|
170
|
+
flank toward it, a darker underside, a white specular ridge along the
|
|
171
|
+
top — with a soft halo so it reads as a puff of plastic cloud. The
|
|
172
|
+
ring lies in the body's equatorial plane seen a little from above,
|
|
173
|
+
with a touch of perspective: the near half (sin > 0) is larger,
|
|
174
|
+
thicker and stronger and is drawn over the body and face, casting a
|
|
175
|
+
soft shadow on them; the far half is smaller and fainter and goes
|
|
176
|
+
behind. */
|
|
177
|
+
const WHIRL_SEGMENTS = 34;
|
|
178
|
+
const WHIRL_SPAN = Math.PI * 1.55;
|
|
179
|
+
const WHIRL_RX = 57;
|
|
180
|
+
const WHIRL_RATIO = 0.4;
|
|
181
|
+
const WHIRL_TILT = -0.28;
|
|
182
|
+
interface WhirlInk {
|
|
183
|
+
base: string;
|
|
184
|
+
light: string;
|
|
185
|
+
dark: string;
|
|
186
|
+
halo: string;
|
|
187
|
+
}
|
|
188
|
+
const whirlInkCache = new Map<string, WhirlInk>();
|
|
189
|
+
function whirlInk(color: string): WhirlInk {
|
|
190
|
+
let w = whirlInkCache.get(color);
|
|
191
|
+
if (!w) {
|
|
192
|
+
w = {
|
|
193
|
+
base: shade(color, 0.1, 0.02),
|
|
194
|
+
light: shade(color, 0.3, 0.04),
|
|
195
|
+
dark: shade(color, -0.22, 0.08),
|
|
196
|
+
halo: shade(color, 0.2),
|
|
197
|
+
};
|
|
198
|
+
if (whirlInkCache.size > 200) whirlInkCache.clear();
|
|
199
|
+
whirlInkCache.set(color, w);
|
|
200
|
+
}
|
|
201
|
+
return w;
|
|
202
|
+
}
|
|
203
|
+
/* an hsl() from shade() with an alpha */
|
|
204
|
+
const withAlpha = (hsl: string, a: number) =>
|
|
205
|
+
hsl.replace(")", ` / ${Math.max(0, Math.min(1, a)).toFixed(3)})`);
|
|
206
|
+
|
|
207
|
+
function drawWhirl(
|
|
208
|
+
ctx: CanvasRenderingContext2D,
|
|
209
|
+
pose: Pose,
|
|
210
|
+
color: string,
|
|
211
|
+
lx: number,
|
|
212
|
+
ly: number,
|
|
213
|
+
near: boolean,
|
|
214
|
+
knobs?: DrawConfig["whirl"],
|
|
215
|
+
) {
|
|
216
|
+
const strength = knobs?.strength ?? 0;
|
|
217
|
+
const k = Math.min(1, pose.whirl * strength);
|
|
218
|
+
if (k <= 0.01) return;
|
|
219
|
+
const sizeK = knobs?.size ?? 1,
|
|
220
|
+
widthK = knobs?.width ?? 1,
|
|
221
|
+
lengthK = knobs?.length ?? 1,
|
|
222
|
+
tiltK = knobs?.tilt ?? 1;
|
|
223
|
+
const span = WHIRL_SPAN * lengthK;
|
|
224
|
+
const ink = whirlInk(color);
|
|
225
|
+
/* the ring runs the way the body's near face moves: to the right */
|
|
226
|
+
const head = -pose.whirlAngle;
|
|
227
|
+
const rx = WHIRL_RX * sizeK;
|
|
228
|
+
const ry = rx * WHIRL_RATIO * tiltK * (near ? 1.14 : 0.86);
|
|
229
|
+
/* where round the ring the light falls, in the ring's own frame */
|
|
230
|
+
const lightA = Math.atan2(ly, lx) - WHIRL_TILT;
|
|
231
|
+
ctx.save();
|
|
232
|
+
ctx.rotate(WHIRL_TILT);
|
|
233
|
+
ctx.translate(0, 5);
|
|
234
|
+
ctx.lineCap = "butt";
|
|
235
|
+
const seg = (a0: number, a1: number, width: number, style: string, dy: number) => {
|
|
236
|
+
ctx.strokeStyle = style;
|
|
237
|
+
ctx.lineWidth = width;
|
|
238
|
+
ctx.beginPath();
|
|
239
|
+
ctx.ellipse(0, dy, rx, ry, 0, a0, a1, false);
|
|
240
|
+
ctx.stroke();
|
|
241
|
+
};
|
|
242
|
+
/* the near half casts a soft shadow on the body it crosses */
|
|
243
|
+
if (near) {
|
|
244
|
+
for (let i = 0; i < WHIRL_SEGMENTS; i++) {
|
|
245
|
+
const f = i / WHIRL_SEGMENTS;
|
|
246
|
+
const a1 = head + f * span,
|
|
247
|
+
a0 = a1 + span / WHIRL_SEGMENTS + 0.012;
|
|
248
|
+
if (Math.sin((a0 + a1) / 2) <= 0) continue;
|
|
249
|
+
const fade = Math.pow(1 - f, 1.3);
|
|
250
|
+
seg(a1, a0, (2 + 8 * fade) * 1.5 * widthK, `rgba(0,0,0,${(0.2 * k * fade).toFixed(3)})`, 3.5);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
for (let i = 0; i < WHIRL_SEGMENTS; i++) {
|
|
254
|
+
const f = i / WHIRL_SEGMENTS;
|
|
255
|
+
/* the trail lies at the angles the head has already passed */
|
|
256
|
+
const a1 = head + f * span,
|
|
257
|
+
a0 = a1 + span / WHIRL_SEGMENTS + 0.012;
|
|
258
|
+
const mid = (a0 + a1) / 2;
|
|
259
|
+
if (Math.sin(mid) > 0 !== near) continue;
|
|
260
|
+
/* perspective and depth: the nearest point of the ring is fullest */
|
|
261
|
+
const depth = 0.6 + 0.4 * Math.sin(mid);
|
|
262
|
+
const fade = Math.pow(1 - f, 1.3);
|
|
263
|
+
/* a gentle puff along the trail */
|
|
264
|
+
const puff = 1 + 0.18 * Math.sin(f * 9 + 1.2);
|
|
265
|
+
const width = (2 + 8 * fade) * depth * widthK * puff;
|
|
266
|
+
const a = k * (0.3 + 0.7 * fade) * depth;
|
|
267
|
+
/* how much this stretch of the ring faces the light */
|
|
268
|
+
const facing = 0.5 + 0.5 * Math.cos(mid - lightA);
|
|
269
|
+
/* halo, underside, body, lit flank, specular ridge: a plastic tube */
|
|
270
|
+
seg(a1, a0, width * 2.6, withAlpha(ink.halo, a * 0.2), 0);
|
|
271
|
+
seg(a1, a0, width * 0.8, withAlpha(ink.dark, a * 0.45), width * 0.32);
|
|
272
|
+
seg(a1, a0, width, withAlpha(ink.base, a * 0.72), 0);
|
|
273
|
+
seg(a1, a0, width * 0.62, withAlpha(ink.light, a * 0.78 * (0.4 + 0.6 * facing)), -width * 0.16);
|
|
274
|
+
seg(
|
|
275
|
+
a1,
|
|
276
|
+
a0,
|
|
277
|
+
width * 0.24,
|
|
278
|
+
`rgba(255,255,255,${(a * 0.9 * (0.15 + 0.85 * facing * facing)).toFixed(3)})`,
|
|
279
|
+
-width * 0.3,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
ctx.restore();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Draw one frame. `box` is the avatar's layout size in CSS px; the canvas
|
|
287
|
+
* is `box * OVERSCAN` square with the body's centre `RISE * box` below
|
|
288
|
+
* its middle, and the context already scaled for the device pixel ratio.
|
|
289
|
+
*/
|
|
290
|
+
/* the back light's direction on screen from its angle, when one is given */
|
|
291
|
+
const backOf = (deg: number | undefined): { bx?: number; by?: number } => {
|
|
292
|
+
if (deg === undefined || !Number.isFinite(deg)) return {};
|
|
293
|
+
const a = (deg * Math.PI) / 180;
|
|
294
|
+
return { bx: Math.sin(a), by: -Math.cos(a) };
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
/** The light each material looks its best in where the config leaves it:
|
|
298
|
+
a plush pile wants a broad, low studio light that shows its lobes and
|
|
299
|
+
grain, a clear coat a brighter one for its gloss. */
|
|
300
|
+
export const LIGHT_DEFAULTS = {
|
|
301
|
+
fabric: { shadow: 1.15, highlight: 1.45, light: 295, rim: 0.6, spread: 1.6 },
|
|
302
|
+
other: { shadow: 0.35, highlight: 1.3, light: 300, rim: 0.5, spread: 1.55 },
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
export function draw(ctx: CanvasRenderingContext2D, box: number, pose: Pose, cfg: DrawConfig) {
|
|
306
|
+
const full = box * OVERSCAN;
|
|
307
|
+
ctx.clearRect(0, 0, full, full);
|
|
308
|
+
const S = box / 100;
|
|
309
|
+
/* the context's transform as given (the device scale) is the base of
|
|
310
|
+
every transform set here; known from `dpr`, else read once */
|
|
311
|
+
let dpr: number, base: readonly number[];
|
|
312
|
+
if (cfg.dpr !== undefined) {
|
|
313
|
+
dpr = cfg.dpr;
|
|
314
|
+
base = [dpr, 0, 0, dpr, 0, 0];
|
|
315
|
+
} else if (ctx.getTransform) {
|
|
316
|
+
const t = ctx.getTransform();
|
|
317
|
+
base = [t.a, t.b, t.c, t.d, t.e, t.f];
|
|
318
|
+
dpr = t.a || 1;
|
|
319
|
+
} else {
|
|
320
|
+
dpr = 1;
|
|
321
|
+
base = [1, 0, 0, 1, 0, 0];
|
|
322
|
+
}
|
|
323
|
+
const lit = LIGHT_DEFAULTS[cfg.shading === "fabric" ? "fabric" : "other"];
|
|
324
|
+
const shadow = cfg.shadow ?? lit.shadow,
|
|
325
|
+
highlight = cfg.highlight ?? lit.highlight;
|
|
326
|
+
const halfDepth = HALF_DEPTH * (cfg.depth ?? 0.65);
|
|
327
|
+
const rimK = cfg.rim ?? lit.rim;
|
|
328
|
+
const cap = 1 - (1 - CAP) * rimK;
|
|
329
|
+
const spread = cfg.spread ?? lit.spread;
|
|
330
|
+
/* the light's direction on screen: a unit vector toward the source */
|
|
331
|
+
const la = ((cfg.light ?? lit.light) * Math.PI) / 180;
|
|
332
|
+
const lx = Math.sin(la),
|
|
333
|
+
ly = -Math.cos(la);
|
|
334
|
+
const pal = palette(cfg.color, shadow, highlight);
|
|
335
|
+
|
|
336
|
+
const cy0 = Math.cos(pose.yaw),
|
|
337
|
+
sy = Math.sin(pose.yaw);
|
|
338
|
+
const cp0 = Math.cos(pose.pitch),
|
|
339
|
+
sp = Math.sin(pose.pitch);
|
|
340
|
+
/* which cap faces the viewer: the front while this is positive */
|
|
341
|
+
const facing = cy0 * cp0;
|
|
342
|
+
/* edge-on, every slice would thin to a line and the stack would show
|
|
343
|
+
gaps; a floor on the foreshortening keeps it a solid */
|
|
344
|
+
const floor = (v: number) => (Math.abs(v) < 0.22 ? (v < 0 ? -0.22 : 0.22) : v);
|
|
345
|
+
const cy = floor(cy0),
|
|
346
|
+
cp = floor(cp0);
|
|
347
|
+
|
|
348
|
+
/* body space: the box centre plus the pose's offset, its roll and
|
|
349
|
+
squash. The squash and stretch scale about the body's base (y = 50),
|
|
350
|
+
so a landing keeps the feet on the ground and presses the top down,
|
|
351
|
+
and a stretch rises from the base. */
|
|
352
|
+
const cr = Math.cos(pose.roll),
|
|
353
|
+
sr = Math.sin(pose.roll),
|
|
354
|
+
kx = pose.sx * S,
|
|
355
|
+
ky = pose.sy * S;
|
|
356
|
+
const lift = 50 * (1 - pose.sy) * S;
|
|
357
|
+
const body = mulAffine(base, [
|
|
358
|
+
cr * kx,
|
|
359
|
+
sr * kx,
|
|
360
|
+
-sr * ky,
|
|
361
|
+
cr * ky,
|
|
362
|
+
full / 2 + pose.x * S - sr * lift,
|
|
363
|
+
full / 2 + RISE * box + pose.y * S + cr * lift,
|
|
364
|
+
]);
|
|
365
|
+
ctx.save();
|
|
366
|
+
ctx.setTransform(body[0], body[1], body[2], body[3], body[4], body[5]);
|
|
367
|
+
ctx.lineCap = "round";
|
|
368
|
+
ctx.lineJoin = "round";
|
|
369
|
+
|
|
370
|
+
const mode = cfg.shading;
|
|
371
|
+
/* one solid: the slice stack (or the plastic material) for an outline
|
|
372
|
+
at a depth; the thin parts come first with a fraction of the depth,
|
|
373
|
+
then the body over them. A plush toy's antennae are moulded plastic:
|
|
374
|
+
a pile on a rod that thin is all fringe, so parts take the plastic
|
|
375
|
+
when the body is fabric (`plush` false) */
|
|
376
|
+
const drawSolid = (path: Path2D, key: string, halfDepth: number, plush = true): boolean => {
|
|
377
|
+
/* the lit gradient, in the body's own space: light from the upper left */
|
|
378
|
+
let lit: CanvasGradient | string = pal.near;
|
|
379
|
+
let capFill: CanvasGradient | string = pal.base;
|
|
380
|
+
if (mode === "crisp") {
|
|
381
|
+
if (!pal.grad || pal.grad.lx !== lx || pal.grad.ly !== ly) {
|
|
382
|
+
const g = ctx.createLinearGradient(lx * 56, ly * 56, -lx * 56, -ly * 56);
|
|
383
|
+
g.addColorStop(0, pal.light);
|
|
384
|
+
g.addColorStop(0.45, pal.near);
|
|
385
|
+
g.addColorStop(1, pal.dark);
|
|
386
|
+
const c = ctx.createLinearGradient(lx * 46, ly * 46, -lx * 46, -ly * 46);
|
|
387
|
+
c.addColorStop(0, pal.capTop);
|
|
388
|
+
c.addColorStop(1, pal.capBottom);
|
|
389
|
+
pal.grad = { lx, ly, lit: g, cap: c };
|
|
390
|
+
}
|
|
391
|
+
lit = pal.grad.lit;
|
|
392
|
+
capFill = pal.grad.cap;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/* plastic and fabric: the material module draws the whole body, an
|
|
396
|
+
inflated relief of the lit texture. While a form is still baking on
|
|
397
|
+
idle time it declines, and the stock slices with the smooth overlay
|
|
398
|
+
stand in for that frame. */
|
|
399
|
+
let plasticDone = false;
|
|
400
|
+
if (mode === "plastic" || mode === "fabric") {
|
|
401
|
+
plasticDone = drawPlasticCap(
|
|
402
|
+
ctx,
|
|
403
|
+
{ ...cfg, path, typeKey: key, shading: plush ? mode : "plastic" },
|
|
404
|
+
{
|
|
405
|
+
cy,
|
|
406
|
+
sy,
|
|
407
|
+
cp,
|
|
408
|
+
sp,
|
|
409
|
+
facing,
|
|
410
|
+
roll: pose.roll,
|
|
411
|
+
halfDepth,
|
|
412
|
+
cap,
|
|
413
|
+
lx,
|
|
414
|
+
ly,
|
|
415
|
+
...backOf(cfg.backLight),
|
|
416
|
+
dev: box * dpr,
|
|
417
|
+
ctm: body,
|
|
418
|
+
still: cfg.still,
|
|
419
|
+
round: cfg.roundness ?? 1,
|
|
420
|
+
},
|
|
421
|
+
pal,
|
|
422
|
+
null,
|
|
423
|
+
{
|
|
424
|
+
shadow,
|
|
425
|
+
highlight,
|
|
426
|
+
spread,
|
|
427
|
+
rim: rimK,
|
|
428
|
+
vivid: cfg.vivid ?? 0,
|
|
429
|
+
front: (Math.min(85, Math.max(0, cfg.lightFront ?? 32)) * Math.PI) / 180,
|
|
430
|
+
shine: cfg.shine ?? 0,
|
|
431
|
+
sheen: cfg.sheen ?? 0,
|
|
432
|
+
backSoft: cfg.backSoftness ?? 1,
|
|
433
|
+
},
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
const mode2: BotAvatarShading =
|
|
437
|
+
(mode === "plastic" || mode === "fabric") && !plasticDone ? "smooth" : mode;
|
|
438
|
+
const soft = mode2 === "smooth";
|
|
439
|
+
const union = soft && typeof Path2D === "function" ? new Path2D() : null;
|
|
440
|
+
/* slices, far to near; each sets its transform outright from the
|
|
441
|
+
body's, no save/restore */
|
|
442
|
+
const order = facing >= 0 ? 1 : -1;
|
|
443
|
+
const [ca, cb, cc, cd, ce, cf] = body;
|
|
444
|
+
let fill: CanvasGradient | string | null = null;
|
|
445
|
+
/* each slice's affine is applied relative to the previous slice's: one
|
|
446
|
+
transform() per slice, no save/restore */
|
|
447
|
+
let pa = 1,
|
|
448
|
+
pb = 0,
|
|
449
|
+
pc = 0,
|
|
450
|
+
pd = 1,
|
|
451
|
+
pe = 0,
|
|
452
|
+
pf = 0;
|
|
453
|
+
for (let j = 0; j < SLICES && !plasticDone; j++) {
|
|
454
|
+
const k = order > 0 ? j : SLICES - 1 - j;
|
|
455
|
+
const z = -1 + (2 * k) / (SLICES - 1);
|
|
456
|
+
const s = profile(z, cap);
|
|
457
|
+
const near = j / (SLICES - 1);
|
|
458
|
+
/* yaw about Y then pitch about X, orthographic: an affine per slice,
|
|
459
|
+
then the path's own origin at its centre */
|
|
460
|
+
const m0 = cy * s,
|
|
461
|
+
m1 = sy * sp * s,
|
|
462
|
+
m3 = cp * s;
|
|
463
|
+
const e = z * sy * halfDepth - 50 * m0,
|
|
464
|
+
fo = -z * cy * sp * halfDepth - 50 * m1 - 50 * m3;
|
|
465
|
+
const det = pa * pd - pb * pc;
|
|
466
|
+
const ia = pd / det,
|
|
467
|
+
ib = -pb / det,
|
|
468
|
+
ic = -pc / det,
|
|
469
|
+
id = pa / det,
|
|
470
|
+
ie = (pc * pf - pd * pe) / det,
|
|
471
|
+
jf = (pb * pe - pa * pf) / det;
|
|
472
|
+
ctx.transform(
|
|
473
|
+
ia * m0 + ic * m1,
|
|
474
|
+
ib * m0 + id * m1,
|
|
475
|
+
ic * m3,
|
|
476
|
+
id * m3,
|
|
477
|
+
ia * e + ic * fo + ie,
|
|
478
|
+
ib * e + id * fo + jf,
|
|
479
|
+
);
|
|
480
|
+
pa = m0;
|
|
481
|
+
pb = m1;
|
|
482
|
+
pc = 0;
|
|
483
|
+
pd = m3;
|
|
484
|
+
pe = e;
|
|
485
|
+
pf = fo;
|
|
486
|
+
let style: CanvasGradient | string;
|
|
487
|
+
/* smooth: one colour ramp through the depth to the front, no edge at the cap */
|
|
488
|
+
if (soft) style = pal.smoothMix[j];
|
|
489
|
+
else if (j === SLICES - 1) style = capFill;
|
|
490
|
+
else if (near > 0.6) style = lit;
|
|
491
|
+
else style = pal.crispMix[j];
|
|
492
|
+
if (style !== fill) ctx.fillStyle = fill = style;
|
|
493
|
+
ctx.fill(path);
|
|
494
|
+
if (union) union.addPath(path, { a: m0, b: m1, c: 0, d: m3, e, f: fo });
|
|
495
|
+
}
|
|
496
|
+
if (!plasticDone) ctx.setTransform(ca, cb, cc, cd, ce, cf);
|
|
497
|
+
|
|
498
|
+
/* smooth: a soft shadow from the lower right and a light from the upper
|
|
499
|
+
left, laid over the whole form so nothing has an edge */
|
|
500
|
+
if (union && mode2 === "smooth") {
|
|
501
|
+
/* the two gradients are made per frame on purpose: a kept one is
|
|
502
|
+
slower to use in Safari than a fresh one */
|
|
503
|
+
ctx.save();
|
|
504
|
+
ctx.clip(union);
|
|
505
|
+
const sa = Math.min(1, 0.34 * shadow);
|
|
506
|
+
const sg = ctx.createRadialGradient(
|
|
507
|
+
-lx * 45,
|
|
508
|
+
-ly * 45,
|
|
509
|
+
4 * spread,
|
|
510
|
+
-lx * 45,
|
|
511
|
+
-ly * 45,
|
|
512
|
+
84 * spread,
|
|
513
|
+
);
|
|
514
|
+
sg.addColorStop(0, `rgba(0,0,0,${sa})`);
|
|
515
|
+
sg.addColorStop(0.5, `rgba(0,0,0,${sa * 0.35})`);
|
|
516
|
+
sg.addColorStop(1, "rgba(0,0,0,0)");
|
|
517
|
+
ctx.globalCompositeOperation = "multiply";
|
|
518
|
+
ctx.fillStyle = sg;
|
|
519
|
+
ctx.fillRect(-120, -120, 240, 240);
|
|
520
|
+
ctx.globalCompositeOperation = "source-over";
|
|
521
|
+
const ha = Math.min(1, 0.22 * highlight);
|
|
522
|
+
const hg = ctx.createRadialGradient(lx * 37, ly * 37, 0, lx * 37, ly * 37, 62 * spread);
|
|
523
|
+
hg.addColorStop(0, `rgba(255,255,255,${ha})`);
|
|
524
|
+
hg.addColorStop(0.6, `rgba(255,255,255,${ha * 0.23})`);
|
|
525
|
+
hg.addColorStop(1, "rgba(255,255,255,0)");
|
|
526
|
+
ctx.fillStyle = hg;
|
|
527
|
+
ctx.fillRect(-120, -120, 240, 240);
|
|
528
|
+
ctx.restore();
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return plasticDone;
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
/* the far half of the whirl sits behind everything */
|
|
535
|
+
drawWhirl(ctx, pose, cfg.color, lx, ly, false, cfg.whirl);
|
|
536
|
+
|
|
537
|
+
/* what the bot wears, placed on its own geometry and turned with it */
|
|
538
|
+
const wear = cfg.wear;
|
|
539
|
+
const worn =
|
|
540
|
+
!!wear && (wear.hat !== "none" || wear.glasses !== "none" || wear.headphones || wear.bowTie);
|
|
541
|
+
const wr: WearRig = { cy, sy, cp, sp, facing, halfDepth, lx, ly };
|
|
542
|
+
if (worn) drawWearBehind(ctx, cfg.path, wr, wear!);
|
|
543
|
+
|
|
544
|
+
const key = cfg.typeKey ?? "custom";
|
|
545
|
+
/* each piece rounds on its own: two antennae inflated as one shape
|
|
546
|
+
would be scaled toward the middle between them, slice by slice */
|
|
547
|
+
const parts = cfg.parts ? (Array.isArray(cfg.parts) ? cfg.parts : [cfg.parts]) : [];
|
|
548
|
+
for (let i = 0; i < parts.length; i++)
|
|
549
|
+
drawSolid(
|
|
550
|
+
parts[i],
|
|
551
|
+
Array.isArray(cfg.parts) ? `${key}:parts${i}/${parts.length}` : `${key}:parts`,
|
|
552
|
+
halfDepth * (cfg.partsDepth ?? 0.4),
|
|
553
|
+
false,
|
|
554
|
+
);
|
|
555
|
+
const plasticDone = drawSolid(cfg.path, key, halfDepth);
|
|
556
|
+
|
|
557
|
+
/* the face: each feature sits on a sphere behind the front cap, so a
|
|
558
|
+
turn slides it round the head — the eye moving toward the edge
|
|
559
|
+
narrows, the other comes to the front, and past the side they go */
|
|
560
|
+
/* the front's outline in body space: the face's clip, and where worn
|
|
561
|
+
things cast their shadows. The extruded modes have a front cap; the
|
|
562
|
+
cushion of plastic and fabric has none, so its outline is taken a
|
|
563
|
+
little over halfway up the face's own height */
|
|
564
|
+
const zf = facing >= 0 ? 1 : -1;
|
|
565
|
+
const round = cfg.roundness ?? 1;
|
|
566
|
+
const cushion = plasticDone;
|
|
567
|
+
const sf = cushion ? 1 : profile(zf, cap);
|
|
568
|
+
const surface = (x: number, y: number) =>
|
|
569
|
+
cushion ? (surfaceAt(key, halfDepth, x, y, round) ?? 0.8 * halfDepth) : halfDepth * sf;
|
|
570
|
+
const zFace = cushion ? 0.55 * surface(cfg.faceX, cfg.faceY) : halfDepth;
|
|
571
|
+
const fm0 = cy * sf,
|
|
572
|
+
fm1 = sy * sp * sf,
|
|
573
|
+
fm3 = cp * sf;
|
|
574
|
+
const fe = zf * sy * zFace - 50 * fm0;
|
|
575
|
+
const ffo = -zf * cy * sp * zFace - 50 * fm1 - 50 * fm3;
|
|
576
|
+
let frontClip: Path2D | null = null;
|
|
577
|
+
if (worn && typeof Path2D === "function") {
|
|
578
|
+
frontClip = new Path2D();
|
|
579
|
+
frontClip.addPath(cfg.path, { a: fm0, b: fm1, c: 0, d: fm3, e: fe, f: ffo });
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (facing > -0.2) {
|
|
583
|
+
ctx.save();
|
|
584
|
+
/* The face is printed on the front of the body, so it cannot leave
|
|
585
|
+
it: clip to the front slice's own outline first. On a wide turn a
|
|
586
|
+
feature's place on the sphere can reach past the body's foreshortened
|
|
587
|
+
silhouette, and without this it floats off the side. */
|
|
588
|
+
{
|
|
589
|
+
const [ca, cb, cc, cd, ce, cf] = body;
|
|
590
|
+
ctx.setTransform(
|
|
591
|
+
ca * fm0 + cc * fm1,
|
|
592
|
+
cb * fm0 + cd * fm1,
|
|
593
|
+
cc * fm3,
|
|
594
|
+
cd * fm3,
|
|
595
|
+
ca * fe + cc * ffo + ce,
|
|
596
|
+
cb * fe + cd * ffo + cf,
|
|
597
|
+
);
|
|
598
|
+
ctx.clip(cfg.path);
|
|
599
|
+
ctx.setTransform(ca, cb, cc, cd, ce, cf);
|
|
600
|
+
}
|
|
601
|
+
ctx.translate(cfg.faceX - 50, cfg.faceY - 50);
|
|
602
|
+
ctx.scale(cfg.faceScale, cfg.faceScale);
|
|
603
|
+
/* under a clear coat the print shows the gloss faintly through it */
|
|
604
|
+
if (mode === "plastic" && plasticDone) ctx.globalAlpha = 0.93;
|
|
605
|
+
const eyes = drawFace(ctx, pose, cfg);
|
|
606
|
+
ctx.globalAlpha = 1;
|
|
607
|
+
/* glasses: their shadow falls on the face (inside its clip), the frames
|
|
608
|
+
themselves may reach past the body's edge */
|
|
609
|
+
const glasses = wear && wear.glasses !== "none" ? wear.glasses : null;
|
|
610
|
+
const turn = { yaw: pose.yaw, pitch: pose.pitch };
|
|
611
|
+
if (glasses) drawGlasses(ctx, glasses, eyes, { lx, ly }, "shadow", turn);
|
|
612
|
+
ctx.restore();
|
|
613
|
+
if (glasses) {
|
|
614
|
+
ctx.save();
|
|
615
|
+
ctx.translate(cfg.faceX - 50, cfg.faceY - 50);
|
|
616
|
+
ctx.scale(cfg.faceScale, cfg.faceScale);
|
|
617
|
+
drawGlasses(ctx, glasses, eyes, { lx, ly }, "frame", turn);
|
|
618
|
+
ctx.restore();
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if (worn)
|
|
622
|
+
drawWearFront(ctx, cfg.path, wr, wear!, frontClip, surface, {
|
|
623
|
+
y: cfg.faceY,
|
|
624
|
+
scale: cfg.faceScale,
|
|
625
|
+
});
|
|
626
|
+
/* the near half of the whirl passes in front of the face */
|
|
627
|
+
drawWhirl(ctx, pose, cfg.color, lx, ly, true, cfg.whirl);
|
|
628
|
+
ctx.restore();
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/* Radius of the sphere the face is drawn on, in body units. */
|
|
632
|
+
const FACE_R = 30;
|
|
633
|
+
|
|
634
|
+
/* A feature's place on the sphere: longitude and latitude from its
|
|
635
|
+
design position, turned by the head. Returns its screen offset, its
|
|
636
|
+
foreshortening, and how much it faces the viewer. */
|
|
637
|
+
function onSphere(x: number, y: number, yaw: number, pitch: number) {
|
|
638
|
+
const lon = Math.asin(Math.max(-1, Math.min(1, x / FACE_R))) + yaw;
|
|
639
|
+
const lat = Math.asin(Math.max(-1, Math.min(1, -y / FACE_R))) + pitch;
|
|
640
|
+
const cl = Math.cos(lat);
|
|
641
|
+
return {
|
|
642
|
+
x: FACE_R * Math.sin(lon) * cl,
|
|
643
|
+
y: -FACE_R * Math.sin(lat),
|
|
644
|
+
sx: Math.cos(lon),
|
|
645
|
+
sy: cl,
|
|
646
|
+
z: Math.cos(lon) * cl,
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/* An eye's curve as a short polyline with round joins, cached by its
|
|
651
|
+
numbers. Stroking the open eye's hairpin curve directly gives a pill in
|
|
652
|
+
Chrome but a teardrop in Safari, whose stroker does not round a cusp;
|
|
653
|
+
both stroke a polyline the same way. */
|
|
654
|
+
const EYE_STEPS = 8;
|
|
655
|
+
const eyePaths = new Map<number, Path2D>();
|
|
656
|
+
function eyePath(x0: number, y0: number, cy: number): Path2D {
|
|
657
|
+
const qx = Math.round(x0 * 50),
|
|
658
|
+
qy = Math.round(y0 * 50),
|
|
659
|
+
qc = Math.round(cy * 50);
|
|
660
|
+
const key = qx + 2000 * qy + 4e6 * qc;
|
|
661
|
+
let p = eyePaths.get(key);
|
|
662
|
+
if (!p) {
|
|
663
|
+
const ax = qx / 50,
|
|
664
|
+
ay = qy / 50,
|
|
665
|
+
ac = qc / 50;
|
|
666
|
+
let d = `M${-ax} ${ay}`;
|
|
667
|
+
for (let i = 1; i <= EYE_STEPS; i++) {
|
|
668
|
+
const t = i / EYE_STEPS,
|
|
669
|
+
mt = 1 - t;
|
|
670
|
+
d += ` L${(mt * mt * -ax + t * t * ax).toFixed(3)} ${((mt * mt + t * t) * ay + 2 * mt * t * ac).toFixed(3)}`;
|
|
671
|
+
}
|
|
672
|
+
p = new Path2D(d);
|
|
673
|
+
if (eyePaths.size > 256) eyePaths.clear();
|
|
674
|
+
eyePaths.set(key, p);
|
|
675
|
+
}
|
|
676
|
+
return p;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/* The mouth's outline as a path, cached by its numbers: corners at ±hw on
|
|
680
|
+
y = 0, a top edge from the left corner to the right with its controls
|
|
681
|
+
a·hw in from the corners at depth yt, a bottom edge back at depth yb
|
|
682
|
+
with controls ab·hw in, and round caps of radius t0 round each corner,
|
|
683
|
+
set square to the smile's end slope. */
|
|
684
|
+
interface Mouth {
|
|
685
|
+
hw: number;
|
|
686
|
+
t0: number;
|
|
687
|
+
a: number;
|
|
688
|
+
yt: number;
|
|
689
|
+
ab: number;
|
|
690
|
+
yb: number;
|
|
691
|
+
}
|
|
692
|
+
const MOUTH_SIN = Math.sin(0.684),
|
|
693
|
+
MOUTH_COS = Math.cos(0.684);
|
|
694
|
+
const mouthPaths = new Map<string, Path2D>();
|
|
695
|
+
function mouthPath(m: Mouth): Path2D {
|
|
696
|
+
const q = (v: number) => Math.round(v * 50) / 50;
|
|
697
|
+
const hw = q(m.hw),
|
|
698
|
+
t0 = q(m.t0),
|
|
699
|
+
a = q(m.a),
|
|
700
|
+
yt = q(m.yt),
|
|
701
|
+
ab = q(m.ab),
|
|
702
|
+
yb = q(m.yb);
|
|
703
|
+
const key = `${hw},${t0},${a},${yt},${ab},${yb}`;
|
|
704
|
+
let p = mouthPaths.get(key);
|
|
705
|
+
if (p) return p;
|
|
706
|
+
const f = (v: number) => v.toFixed(3);
|
|
707
|
+
/* the corners, offset along the end normal for the caps */
|
|
708
|
+
const nx = t0 * MOUTH_SIN,
|
|
709
|
+
ny = t0 * MOUTH_COS;
|
|
710
|
+
const ltx = -hw + nx,
|
|
711
|
+
lty = -ny,
|
|
712
|
+
rtx = hw - nx,
|
|
713
|
+
rty = -ny;
|
|
714
|
+
const lbx = -hw - nx,
|
|
715
|
+
lby = ny,
|
|
716
|
+
rbx = hw + nx,
|
|
717
|
+
rby = ny;
|
|
718
|
+
/* the caps bulge outward along the end tangents */
|
|
719
|
+
const cx = (4 / 3) * t0 * MOUTH_COS,
|
|
720
|
+
cy = (4 / 3) * t0 * MOUTH_SIN;
|
|
721
|
+
const d =
|
|
722
|
+
`M${f(ltx)} ${f(lty)}` +
|
|
723
|
+
`C${f(-hw + a * hw)} ${f(yt - t0)} ${f(hw - a * hw)} ${f(yt - t0)} ${f(rtx)} ${f(rty)}` +
|
|
724
|
+
`C${f(rtx + cx)} ${f(rty - cy)} ${f(rbx + cx)} ${f(rby - cy)} ${f(rbx)} ${f(rby)}` +
|
|
725
|
+
`C${f(hw - ab * hw)} ${f(yb + t0)} ${f(-hw + ab * hw)} ${f(yb + t0)} ${f(lbx)} ${f(lby)}` +
|
|
726
|
+
`C${f(lbx - cx)} ${f(lby - cy)} ${f(ltx - cx)} ${f(lty - cy)} ${f(ltx)} ${f(lty)}Z`;
|
|
727
|
+
p = new Path2D(d);
|
|
728
|
+
if (mouthPaths.size > 256) mouthPaths.clear();
|
|
729
|
+
mouthPaths.set(key, p);
|
|
730
|
+
return p;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function drawFace(ctx: CanvasRenderingContext2D, pose: Pose, cfg: DrawConfig): [EyeSpot, EyeSpot] {
|
|
734
|
+
const [wd, ww, ws] = pose.w;
|
|
735
|
+
const ink = cfg.ink;
|
|
736
|
+
const ey = EYE_Y[cfg.face];
|
|
737
|
+
const half = EYE_GAP / 2;
|
|
738
|
+
const lx = pose.lookX,
|
|
739
|
+
ly = pose.lookY;
|
|
740
|
+
const { yaw, pitch } = pose;
|
|
741
|
+
|
|
742
|
+
/* place a feature: skip it once it has gone round the side */
|
|
743
|
+
const at = (x: number, y: number, fn: () => void, alpha = 1) => {
|
|
744
|
+
const q = onSphere(x, y, yaw, pitch);
|
|
745
|
+
if (q.z <= 0.02 || alpha <= 0.01) return;
|
|
746
|
+
ctx.save();
|
|
747
|
+
ctx.globalAlpha = alpha * Math.min(1, q.z * 5);
|
|
748
|
+
ctx.translate(q.x, q.y);
|
|
749
|
+
ctx.scale(Math.max(0.02, q.sx), Math.max(0.02, q.sy));
|
|
750
|
+
fn();
|
|
751
|
+
ctx.restore();
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
/* Each eye is one stroked curve — endpoints at ±x0,y0, a control point
|
|
755
|
+
at 0,cy, width w, round caps — so every look is the same shape with
|
|
756
|
+
different numbers, and a blend of the numbers is a real morph: the
|
|
757
|
+
upright pill of an open eye squashes into a shut line, swings up into
|
|
758
|
+
a laughing arc, or droops into a sleeping lid. */
|
|
759
|
+
/* the eyes take the head's aim, but only once it really aims
|
|
760
|
+
somewhere: at rest and through the small drift of a breath they keep
|
|
761
|
+
their own shape, and past that they draw taller looking up, shorter
|
|
762
|
+
looking down, and a little wider from the corner of a sideways
|
|
763
|
+
glance — the mimic that makes a turn read as a look */
|
|
764
|
+
const past = (v: number, d: number) =>
|
|
765
|
+
Math.abs(v) <= d ? 0 : (Math.sign(v) * (Math.abs(v) - d)) / (1 - d);
|
|
766
|
+
const clamp1 = (v: number) => Math.max(-1, Math.min(1, v));
|
|
767
|
+
const up = past(clamp1(-pose.pitch / 0.26 - pose.lookY / 7), 0.34);
|
|
768
|
+
const side = Math.abs(past(clamp1(pose.lookX / 4.5), 0.4));
|
|
769
|
+
const tall = Math.max(0.3, 1 + 0.55 * up - 0.1 * side);
|
|
770
|
+
const wide = 1 - 0.05 * up + 0.12 * side;
|
|
771
|
+
const open = wd + ww * (1 - pose.laugh);
|
|
772
|
+
const laugh = ww * pose.laugh;
|
|
773
|
+
const lift = Math.max(0, -pose.y) / 26;
|
|
774
|
+
const sag = 0.5 + 0.5 * pose.breath;
|
|
775
|
+
for (const side of [-1, 1] as const) {
|
|
776
|
+
const lid = side < 0 ? pose.blinkL : pose.blinkR;
|
|
777
|
+
const e = Math.max(0, Math.min(1, pose.eyeOpen * (1 - lid)));
|
|
778
|
+
const kOpen = open * e,
|
|
779
|
+
kShut = open * (1 - e),
|
|
780
|
+
kLaugh = laugh,
|
|
781
|
+
kSleep = ws;
|
|
782
|
+
const x0 = kOpen * 0.01 + kShut * 5.4 + kLaugh * 6.2 + kSleep * 6;
|
|
783
|
+
const y0 =
|
|
784
|
+
kOpen * 1.1 * tall + kShut * 0.6 + kLaugh * (2.2 - lift * 1.5) + kSleep * (-1.4 + sag);
|
|
785
|
+
const cy =
|
|
786
|
+
kOpen * -3.3 * tall + kShut * 0.6 + kLaugh * (-11.4 - 4 * lift) + kSleep * (5.4 + 2 * sag);
|
|
787
|
+
const w = kOpen * EYE_RX * 2 * wide + kShut * 2.8 + kLaugh * 4.4 + kSleep * 4;
|
|
788
|
+
/* the eyes drift toward the look when open, less so when shut */
|
|
789
|
+
const dx = lx * (kOpen + 0.5 * (kShut + kLaugh)),
|
|
790
|
+
dy = ly * (kOpen + 0.5 * kShut);
|
|
791
|
+
at(side * half + dx, ey + dy, () => {
|
|
792
|
+
ctx.strokeStyle = ink;
|
|
793
|
+
ctx.lineWidth = w;
|
|
794
|
+
ctx.stroke(eyePath(x0, y0, cy));
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if (cfg.face === "mouth") {
|
|
799
|
+
const mx = lx * 0.35;
|
|
800
|
+
/* One mouth for every state, so a switch morphs it rather than fading
|
|
801
|
+
one shape into another: two cubic edges between the corners, round
|
|
802
|
+
caps of radius t0 at the corners (the smile is a thick line; the
|
|
803
|
+
open mouth and the sleeping "o" have sharp corners), every number a
|
|
804
|
+
blend of the three states'. The smile widens on the in-breath, the
|
|
805
|
+
open mouth at the top of a hop, the "o" swells with each breath. */
|
|
806
|
+
const kd = (0.6 + 0.4 * wd) * (1 + 0.06 * pose.breath);
|
|
807
|
+
const kw = (0.6 + 0.4 * ww) * (1 + (0.25 * Math.max(0, -pose.y)) / 26);
|
|
808
|
+
const r = 2.7 * ws * (1 + 0.25 * pose.breath);
|
|
809
|
+
const b = (d: number, w: number, s: number) => wd * d + ww * w + ws * s;
|
|
810
|
+
const m = {
|
|
811
|
+
hw: b(6.5 * kd, 9.5 * kw, r),
|
|
812
|
+
t0: b(1.9, 0, 0),
|
|
813
|
+
a: b(2 / 3, 2 / 3, 0),
|
|
814
|
+
yt: b(3.53 * kd, 1.6 * kw, (-4 * r) / 3),
|
|
815
|
+
ab: b(2 / 3, 0, 0),
|
|
816
|
+
yb: b(3.53 * kd, 17.3 * kw, (4 * r) / 3),
|
|
817
|
+
};
|
|
818
|
+
at(mx, b(12.5, 11.6, 15.5), () => {
|
|
819
|
+
ctx.fillStyle = ink;
|
|
820
|
+
ctx.fill(mouthPath(m));
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/* where the eyes sit on the face, for anything worn over them: the
|
|
825
|
+
design spots on the sphere, without the look's drift */
|
|
826
|
+
const spot = (x: number): EyeSpot => {
|
|
827
|
+
const q = onSphere(x, ey, yaw, pitch);
|
|
828
|
+
return {
|
|
829
|
+
x: q.x,
|
|
830
|
+
y: q.y,
|
|
831
|
+
sx: q.sx,
|
|
832
|
+
sy: q.sy,
|
|
833
|
+
z: q.z,
|
|
834
|
+
ox: x,
|
|
835
|
+
oy: ey,
|
|
836
|
+
fz: Math.sqrt(Math.max(0, FACE_R * FACE_R - x * x - ey * ey)),
|
|
837
|
+
};
|
|
838
|
+
};
|
|
839
|
+
return [spot(-half), spot(half)];
|
|
840
|
+
}
|