tegaki 0.7.0 → 0.8.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/CHANGELOG.md +10 -0
- package/README.md +2 -1
- package/dist/solid/index.mjs +5 -1
- package/dist/solid/index.mjs.map +1 -1
- package/dist/wc/index.d.mts +52 -0
- package/dist/wc/index.mjs +162 -0
- package/dist/wc/index.mjs.map +1 -0
- package/package.json +11 -3
- package/src/astro/TegakiRenderer.astro +4 -2
- package/src/solid/TegakiRenderer.tsx +3 -1
- package/src/svelte/TegakiRenderer.svelte +2 -2
- package/src/wc/TegakiElement.ts +207 -0
- package/src/wc/index.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# tegaki
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`b0dabe4`](https://github.com/KurtGokhan/tegaki/commit/b0dabe4ede42564ca2fadf68a3db23a94c55d163) Thanks [@KurtGokhan](https://github.com/KurtGokhan)! - Add Web Components adapter (`tegaki/wc`) with `<tegaki-renderer>` custom element and docs page.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- [`4068d1c`](https://github.com/KurtGokhan/tegaki/commit/4068d1c74413e302b73375897aa9377c215a087a) Thanks [@KurtGokhan](https://github.com/KurtGokhan)! - Fix user-provided inline styles being overridden by engine root styles in Astro, Svelte, and Solid adapters.
|
|
12
|
+
|
|
3
13
|
## 0.7.0
|
|
4
14
|
|
|
5
15
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -60,6 +60,7 @@ import TegakiRenderer from 'tegaki/astro'; // Astro
|
|
|
60
60
|
|
|
61
61
|
```ts
|
|
62
62
|
import { TegakiEngine } from 'tegaki/core'; // Vanilla JS
|
|
63
|
+
import { registerTegakiElement } from 'tegaki/wc'; // Web Components
|
|
63
64
|
```
|
|
64
65
|
|
|
65
66
|
## Built-in Fonts
|
|
@@ -78,7 +79,7 @@ For other Google Fonts, use the [interactive generator](https://gkurt.com/tegaki
|
|
|
78
79
|
Visit **[gkurt.com/tegaki](https://gkurt.com/tegaki)** for full documentation:
|
|
79
80
|
|
|
80
81
|
- [Getting Started](https://gkurt.com/tegaki/getting-started/)
|
|
81
|
-
- [Framework Guides](https://gkurt.com/tegaki/frameworks/react/) (React, Svelte, Vue, SolidJS, Astro, Vanilla)
|
|
82
|
+
- [Framework Guides](https://gkurt.com/tegaki/frameworks/react/) (React, Svelte, Vue, SolidJS, Astro, Web Components, Vanilla)
|
|
82
83
|
- [Generating Fonts](https://gkurt.com/tegaki/guides/generating-fonts/)
|
|
83
84
|
- [API Reference](https://gkurt.com/tegaki/api/tegaki-renderer/)
|
|
84
85
|
|
package/dist/solid/index.mjs
CHANGED
|
@@ -66,11 +66,15 @@ function TegakiRenderer(props) {
|
|
|
66
66
|
createEffect(on(engineOptions, (options) => {
|
|
67
67
|
engine?.update(options);
|
|
68
68
|
}));
|
|
69
|
+
const mergedStyle = {
|
|
70
|
+
...rootProps.style,
|
|
71
|
+
...typeof divProps.style === "object" ? divProps.style : {}
|
|
72
|
+
};
|
|
69
73
|
return /* @__PURE__ */ jsx("div", {
|
|
70
74
|
ref: container,
|
|
71
75
|
"data-tegaki": "root",
|
|
72
|
-
style: rootProps.style,
|
|
73
76
|
...divProps,
|
|
77
|
+
style: mergedStyle,
|
|
74
78
|
innerHTML
|
|
75
79
|
});
|
|
76
80
|
}
|
package/dist/solid/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/solid/TegakiRenderer.tsx"],"sourcesContent":["/** @jsxImportSource solid-js */\nimport { createEffect, createMemo, type JSX, on, onCleanup, onMount, splitProps } from 'solid-js';\nimport { TegakiEngine, type TegakiEngineOptions } from '../core/engine.ts';\nimport type { TegakiEffects } from '../types.ts';\n\nexport interface TegakiRendererProps extends Omit<TegakiEngineOptions, 'effects'> {\n /** Visual effects applied during canvas rendering. */\n effects?: TegakiEffects<Record<string, any>>;\n class?: string;\n ref?: (handle: TegakiRendererHandle) => void;\n [key: string]: any;\n}\n\nexport interface TegakiRendererHandle {\n readonly engine: TegakiEngine | null;\n readonly element: HTMLDivElement | null;\n}\n\nfunction solidCreateElement(tag: string, props: Record<string, any>, ...children: (JSX.Element | string)[]): JSX.Element {\n const parts: string[] = [];\n for (const [key, value] of Object.entries(props)) {\n if (value == null || value === false) continue;\n if (key === 'style' && typeof value === 'object') {\n const css = Object.entries(value)\n .filter(([, v]) => v != null)\n .map(([k, v]) => {\n const prop = k.startsWith('--') ? k : k.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);\n const val = typeof v === 'number' && !k.startsWith('--') ? `${v}px` : String(v);\n return `${prop}:${val}`;\n })\n .join(';');\n if (css) parts.push(`style=\"${escapeAttr(css)}\"`);\n } else if (typeof value === 'boolean') {\n parts.push(key);\n } else {\n parts.push(`${key}=\"${escapeAttr(String(value))}\"`);\n }\n }\n const open = parts.length > 0 ? `<${tag} ${parts.join(' ')}>` : `<${tag}>`;\n const content = children.map((c) => (typeof c === 'string' && !c.startsWith('<') ? escapeHtml(c) : (c as string))).join('');\n return `${open}${content}</${tag}>` as unknown as JSX.Element;\n}\n\nfunction escapeAttr(s: string): string {\n return s.replace(/&/g, '&').replace(/\"/g, '"').replace(/</g, '<').replace(/>/g, '>');\n}\n\nfunction escapeHtml(s: string): string {\n return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');\n}\n\nexport function TegakiRenderer(props: TegakiRendererProps) {\n const [local, divProps] = splitProps(props, [\n 'text',\n 'font',\n 'time',\n 'onComplete',\n 'effects',\n 'segmentSize',\n 'timing',\n 'showOverlay',\n 'ref',\n ]);\n\n let container!: HTMLDivElement;\n let engine: TegakiEngine | null = null;\n\n const engineOptions = createMemo<TegakiEngineOptions>(() => ({\n text: local.text,\n font: local.font,\n time: local.time,\n effects: local.effects as Record<string, any>,\n segmentSize: local.segmentSize,\n timing: local.timing,\n showOverlay: local.showOverlay,\n onComplete: local.onComplete,\n }));\n\n // Compute initial HTML once — after the engine adopts, all updates go through engine.update().\n const { rootProps, content } = TegakiEngine.renderElements(engineOptions(), solidCreateElement);\n const innerHTML = content as unknown as string;\n\n onMount(() => {\n engine = new TegakiEngine(container, { ...engineOptions(), adopt: true });\n local.ref?.({ engine, element: container });\n });\n\n onCleanup(() => {\n engine?.destroy();\n engine = null;\n });\n\n createEffect(\n on(engineOptions, (options) => {\n engine?.update(options);\n }),\n );\n\n return <div ref={container!} data-tegaki=\"root\"
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/solid/TegakiRenderer.tsx"],"sourcesContent":["/** @jsxImportSource solid-js */\nimport { createEffect, createMemo, type JSX, on, onCleanup, onMount, splitProps } from 'solid-js';\nimport { TegakiEngine, type TegakiEngineOptions } from '../core/engine.ts';\nimport type { TegakiEffects } from '../types.ts';\n\nexport interface TegakiRendererProps extends Omit<TegakiEngineOptions, 'effects'> {\n /** Visual effects applied during canvas rendering. */\n effects?: TegakiEffects<Record<string, any>>;\n class?: string;\n ref?: (handle: TegakiRendererHandle) => void;\n [key: string]: any;\n}\n\nexport interface TegakiRendererHandle {\n readonly engine: TegakiEngine | null;\n readonly element: HTMLDivElement | null;\n}\n\nfunction solidCreateElement(tag: string, props: Record<string, any>, ...children: (JSX.Element | string)[]): JSX.Element {\n const parts: string[] = [];\n for (const [key, value] of Object.entries(props)) {\n if (value == null || value === false) continue;\n if (key === 'style' && typeof value === 'object') {\n const css = Object.entries(value)\n .filter(([, v]) => v != null)\n .map(([k, v]) => {\n const prop = k.startsWith('--') ? k : k.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);\n const val = typeof v === 'number' && !k.startsWith('--') ? `${v}px` : String(v);\n return `${prop}:${val}`;\n })\n .join(';');\n if (css) parts.push(`style=\"${escapeAttr(css)}\"`);\n } else if (typeof value === 'boolean') {\n parts.push(key);\n } else {\n parts.push(`${key}=\"${escapeAttr(String(value))}\"`);\n }\n }\n const open = parts.length > 0 ? `<${tag} ${parts.join(' ')}>` : `<${tag}>`;\n const content = children.map((c) => (typeof c === 'string' && !c.startsWith('<') ? escapeHtml(c) : (c as string))).join('');\n return `${open}${content}</${tag}>` as unknown as JSX.Element;\n}\n\nfunction escapeAttr(s: string): string {\n return s.replace(/&/g, '&').replace(/\"/g, '"').replace(/</g, '<').replace(/>/g, '>');\n}\n\nfunction escapeHtml(s: string): string {\n return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');\n}\n\nexport function TegakiRenderer(props: TegakiRendererProps) {\n const [local, divProps] = splitProps(props, [\n 'text',\n 'font',\n 'time',\n 'onComplete',\n 'effects',\n 'segmentSize',\n 'timing',\n 'showOverlay',\n 'ref',\n ]);\n\n let container!: HTMLDivElement;\n let engine: TegakiEngine | null = null;\n\n const engineOptions = createMemo<TegakiEngineOptions>(() => ({\n text: local.text,\n font: local.font,\n time: local.time,\n effects: local.effects as Record<string, any>,\n segmentSize: local.segmentSize,\n timing: local.timing,\n showOverlay: local.showOverlay,\n onComplete: local.onComplete,\n }));\n\n // Compute initial HTML once — after the engine adopts, all updates go through engine.update().\n const { rootProps, content } = TegakiEngine.renderElements(engineOptions(), solidCreateElement);\n const innerHTML = content as unknown as string;\n\n onMount(() => {\n engine = new TegakiEngine(container, { ...engineOptions(), adopt: true });\n local.ref?.({ engine, element: container });\n });\n\n onCleanup(() => {\n engine?.destroy();\n engine = null;\n });\n\n createEffect(\n on(engineOptions, (options) => {\n engine?.update(options);\n }),\n );\n\n const mergedStyle = { ...rootProps.style, ...(typeof divProps.style === 'object' ? divProps.style : {}) };\n\n return <div ref={container!} data-tegaki=\"root\" {...divProps} style={mergedStyle} innerHTML={innerHTML} />;\n}\n"],"mappings":";;;;;AAkBA,SAAS,mBAAmB,KAAa,OAA4B,GAAG,UAAiD;CACvH,MAAM,QAAkB,EAAE;AAC1B,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,EAAE;AAChD,MAAI,SAAS,QAAQ,UAAU,MAAO;AACtC,MAAI,QAAQ,WAAW,OAAO,UAAU,UAAU;GAChD,MAAM,MAAM,OAAO,QAAQ,MAAM,CAC9B,QAAQ,GAAG,OAAO,KAAK,KAAK,CAC5B,KAAK,CAAC,GAAG,OAAO;AAGf,WAAO,GAFM,EAAE,WAAW,KAAK,GAAG,IAAI,EAAE,QAAQ,WAAW,MAAM,IAAI,EAAE,aAAa,GAAG,CAExE,GADH,OAAO,MAAM,YAAY,CAAC,EAAE,WAAW,KAAK,GAAG,GAAG,EAAE,MAAM,OAAO,EAAE;KAE/E,CACD,KAAK,IAAI;AACZ,OAAI,IAAK,OAAM,KAAK,UAAU,WAAW,IAAI,CAAC,GAAG;aACxC,OAAO,UAAU,UAC1B,OAAM,KAAK,IAAI;MAEf,OAAM,KAAK,GAAG,IAAI,IAAI,WAAW,OAAO,MAAM,CAAC,CAAC,GAAG;;AAKvD,QAAO,GAFM,MAAM,SAAS,IAAI,IAAI,IAAI,GAAG,MAAM,KAAK,IAAI,CAAC,KAAK,IAAI,IAAI,KACxD,SAAS,KAAK,MAAO,OAAO,MAAM,YAAY,CAAC,EAAE,WAAW,IAAI,GAAG,WAAW,EAAE,GAAI,EAAc,CAAC,KAAK,GAAG,CAClG,IAAI,IAAI;;AAGnC,SAAS,WAAW,GAAmB;AACrC,QAAO,EAAE,QAAQ,MAAM,QAAQ,CAAC,QAAQ,MAAM,SAAS,CAAC,QAAQ,MAAM,OAAO,CAAC,QAAQ,MAAM,OAAO;;AAGrG,SAAS,WAAW,GAAmB;AACrC,QAAO,EAAE,QAAQ,MAAM,QAAQ,CAAC,QAAQ,MAAM,OAAO,CAAC,QAAQ,MAAM,OAAO;;AAG7E,SAAgB,eAAe,OAA4B;CACzD,MAAM,CAAC,OAAO,YAAY,WAAW,OAAO;EAC1C;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CAAC;CAEF,IAAI;CACJ,IAAI,SAA8B;CAElC,MAAM,gBAAgB,kBAAuC;EAC3D,MAAM,MAAM;EACZ,MAAM,MAAM;EACZ,MAAM,MAAM;EACZ,SAAS,MAAM;EACf,aAAa,MAAM;EACnB,QAAQ,MAAM;EACd,aAAa,MAAM;EACnB,YAAY,MAAM;EACnB,EAAE;CAGH,MAAM,EAAE,WAAW,YAAY,aAAa,eAAe,eAAe,EAAE,mBAAmB;CAC/F,MAAM,YAAY;AAElB,eAAc;AACZ,WAAS,IAAI,aAAa,WAAW;GAAE,GAAG,eAAe;GAAE,OAAO;GAAM,CAAC;AACzE,QAAM,MAAM;GAAE;GAAQ,SAAS;GAAW,CAAC;GAC3C;AAEF,iBAAgB;AACd,UAAQ,SAAS;AACjB,WAAS;GACT;AAEF,cACE,GAAG,gBAAgB,YAAY;AAC7B,UAAQ,OAAO,QAAQ;GACvB,CACH;CAED,MAAM,cAAc;EAAE,GAAG,UAAU;EAAO,GAAI,OAAO,SAAS,UAAU,WAAW,SAAS,QAAQ,EAAE;EAAG;AAEzG,QAAO,oBAAC,OAAD;EAAK,KAAK;EAAY,eAAY;EAAO,GAAI;EAAU,OAAO;EAAwB;EAAa,CAAA"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { A as TegakiSingletonEffectName, C as Stroke, D as TegakiEffects, E as TegakiEffectName, O as TegakiGlyphData, S as Point, T as TegakiEffectConfigs, _ as CSSLength, a as TimeControlProp, b as LineCap, c as TimelineEntry, d as computeTextLayout, f as ensureFontFace, g as BBox, h as resolveEffects, i as TimeControlMode, j as TimedPoint, k as TegakiMultiEffectName, l as computeTimeline, m as ResolvedEffect, n as TegakiEngine, o as Timeline, p as drawGlyph, r as TegakiEngineOptions, s as TimelineConfig, t as CreateElementFn, u as TextLayout, v as FontOutput, w as TegakiBundle, x as PathCommand, y as GlyphData } from "../index-YdGpXlqf.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/wc/TegakiElement.d.ts
|
|
4
|
+
declare class TegakiElement extends HTMLElement {
|
|
5
|
+
static observedAttributes: ("text" | "font" | "time" | "speed" | "playing" | "loop" | "segment-size" | "show-overlay")[];
|
|
6
|
+
private _engine;
|
|
7
|
+
private _container;
|
|
8
|
+
private _font;
|
|
9
|
+
private _effects;
|
|
10
|
+
private _timing;
|
|
11
|
+
private _onComplete;
|
|
12
|
+
constructor();
|
|
13
|
+
connectedCallback(): void;
|
|
14
|
+
disconnectedCallback(): void;
|
|
15
|
+
attributeChangedCallback(_name: string, _oldValue: string | null, _newValue: string | null): void;
|
|
16
|
+
/** The underlying engine instance. */
|
|
17
|
+
get engine(): TegakiEngine | null;
|
|
18
|
+
/** Set the font bundle directly (alternative to the `font` attribute for registered names). */
|
|
19
|
+
get font(): TegakiBundle | string | undefined;
|
|
20
|
+
set font(value: TegakiBundle | string | undefined);
|
|
21
|
+
/** Visual effects configuration. */
|
|
22
|
+
get effects(): TegakiEngineOptions['effects'];
|
|
23
|
+
set effects(value: TegakiEngineOptions['effects']);
|
|
24
|
+
/** Timeline timing configuration. */
|
|
25
|
+
get timing(): TegakiEngineOptions['timing'];
|
|
26
|
+
set timing(value: TegakiEngineOptions['timing']);
|
|
27
|
+
/** Callback when animation completes. */
|
|
28
|
+
get onComplete(): (() => void) | undefined;
|
|
29
|
+
set onComplete(value: (() => void) | undefined);
|
|
30
|
+
play(): void;
|
|
31
|
+
pause(): void;
|
|
32
|
+
seek(time: number): void;
|
|
33
|
+
restart(): void;
|
|
34
|
+
get currentTime(): number;
|
|
35
|
+
get duration(): number;
|
|
36
|
+
get isPlaying(): boolean;
|
|
37
|
+
get isComplete(): boolean;
|
|
38
|
+
private _buildOptions;
|
|
39
|
+
private _resolveTime;
|
|
40
|
+
private _getNumberAttr;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Register the `<tegaki-renderer>` custom element.
|
|
44
|
+
* Call this once before using the element in HTML.
|
|
45
|
+
*
|
|
46
|
+
* @param tagName - Custom element tag name. Default: `'tegaki-renderer'`.
|
|
47
|
+
* Note: custom element names must contain a hyphen per the HTML spec.
|
|
48
|
+
*/
|
|
49
|
+
declare function registerTegakiElement(tagName?: string): void;
|
|
50
|
+
//#endregion
|
|
51
|
+
export { BBox, CSSLength, CreateElementFn, FontOutput, GlyphData, LineCap, PathCommand, Point, ResolvedEffect, Stroke, TegakiBundle, TegakiEffectConfigs, TegakiEffectName, TegakiEffects, TegakiElement, TegakiEngine, TegakiEngineOptions, TegakiGlyphData, TegakiMultiEffectName, TegakiSingletonEffectName, TextLayout, TimeControlMode, TimeControlProp, TimedPoint, Timeline, TimelineConfig, TimelineEntry, computeTextLayout, computeTimeline, drawGlyph, ensureFontFace, registerTegakiElement, resolveEffects };
|
|
52
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { a as drawGlyph, i as ensureFontFace, n as computeTimeline, r as computeTextLayout, s as resolveEffects, t as TegakiEngine } from "../core-Cw5jNWFa.mjs";
|
|
2
|
+
//#region src/wc/TegakiElement.ts
|
|
3
|
+
/**
|
|
4
|
+
* Observed attribute names.
|
|
5
|
+
* - `text`: the text to render (also settable via textContent)
|
|
6
|
+
* - `font`: registered bundle name (see {@link TegakiEngine.registerBundle})
|
|
7
|
+
* - `time`: time control — a number for controlled mode, `"css"` for CSS mode, omit for uncontrolled
|
|
8
|
+
* - `speed`: playback speed multiplier (uncontrolled mode, default `1`)
|
|
9
|
+
* - `playing`: whether animation is playing (uncontrolled mode, default `true`)
|
|
10
|
+
* - `loop`: loop animation (uncontrolled mode, default `false`)
|
|
11
|
+
* - `segment-size`: segment size for rendering
|
|
12
|
+
* - `show-overlay`: show debug overlay
|
|
13
|
+
*/
|
|
14
|
+
const OBSERVED_ATTRS = [
|
|
15
|
+
"text",
|
|
16
|
+
"font",
|
|
17
|
+
"time",
|
|
18
|
+
"speed",
|
|
19
|
+
"playing",
|
|
20
|
+
"loop",
|
|
21
|
+
"segment-size",
|
|
22
|
+
"show-overlay"
|
|
23
|
+
];
|
|
24
|
+
var TegakiElement = class extends HTMLElement {
|
|
25
|
+
static observedAttributes = [...OBSERVED_ATTRS];
|
|
26
|
+
_engine = null;
|
|
27
|
+
_container;
|
|
28
|
+
_font;
|
|
29
|
+
_effects;
|
|
30
|
+
_timing;
|
|
31
|
+
_onComplete;
|
|
32
|
+
constructor() {
|
|
33
|
+
super();
|
|
34
|
+
const shadow = this.attachShadow({ mode: "open" });
|
|
35
|
+
const style = document.createElement("style");
|
|
36
|
+
style.textContent = `:host { display: inline-block; }`;
|
|
37
|
+
shadow.appendChild(style);
|
|
38
|
+
this._container = document.createElement("div");
|
|
39
|
+
shadow.appendChild(this._container);
|
|
40
|
+
}
|
|
41
|
+
connectedCallback() {
|
|
42
|
+
this._engine = new TegakiEngine(this._container, this._buildOptions());
|
|
43
|
+
}
|
|
44
|
+
disconnectedCallback() {
|
|
45
|
+
this._engine?.destroy();
|
|
46
|
+
this._engine = null;
|
|
47
|
+
}
|
|
48
|
+
attributeChangedCallback(_name, _oldValue, _newValue) {
|
|
49
|
+
this._engine?.update(this._buildOptions());
|
|
50
|
+
}
|
|
51
|
+
/** The underlying engine instance. */
|
|
52
|
+
get engine() {
|
|
53
|
+
return this._engine;
|
|
54
|
+
}
|
|
55
|
+
/** Set the font bundle directly (alternative to the `font` attribute for registered names). */
|
|
56
|
+
get font() {
|
|
57
|
+
return this._font;
|
|
58
|
+
}
|
|
59
|
+
set font(value) {
|
|
60
|
+
this._font = value;
|
|
61
|
+
this._engine?.update(this._buildOptions());
|
|
62
|
+
}
|
|
63
|
+
/** Visual effects configuration. */
|
|
64
|
+
get effects() {
|
|
65
|
+
return this._effects;
|
|
66
|
+
}
|
|
67
|
+
set effects(value) {
|
|
68
|
+
this._effects = value;
|
|
69
|
+
this._engine?.update(this._buildOptions());
|
|
70
|
+
}
|
|
71
|
+
/** Timeline timing configuration. */
|
|
72
|
+
get timing() {
|
|
73
|
+
return this._timing;
|
|
74
|
+
}
|
|
75
|
+
set timing(value) {
|
|
76
|
+
this._timing = value;
|
|
77
|
+
this._engine?.update(this._buildOptions());
|
|
78
|
+
}
|
|
79
|
+
/** Callback when animation completes. */
|
|
80
|
+
get onComplete() {
|
|
81
|
+
return this._onComplete;
|
|
82
|
+
}
|
|
83
|
+
set onComplete(value) {
|
|
84
|
+
this._onComplete = value;
|
|
85
|
+
this._engine?.update(this._buildOptions());
|
|
86
|
+
}
|
|
87
|
+
play() {
|
|
88
|
+
this._engine?.play();
|
|
89
|
+
}
|
|
90
|
+
pause() {
|
|
91
|
+
this._engine?.pause();
|
|
92
|
+
}
|
|
93
|
+
seek(time) {
|
|
94
|
+
this._engine?.seek(time);
|
|
95
|
+
}
|
|
96
|
+
restart() {
|
|
97
|
+
this._engine?.restart();
|
|
98
|
+
}
|
|
99
|
+
get currentTime() {
|
|
100
|
+
return this._engine?.currentTime ?? 0;
|
|
101
|
+
}
|
|
102
|
+
get duration() {
|
|
103
|
+
return this._engine?.duration ?? 0;
|
|
104
|
+
}
|
|
105
|
+
get isPlaying() {
|
|
106
|
+
return this._engine?.isPlaying ?? false;
|
|
107
|
+
}
|
|
108
|
+
get isComplete() {
|
|
109
|
+
return this._engine?.isComplete ?? false;
|
|
110
|
+
}
|
|
111
|
+
_buildOptions() {
|
|
112
|
+
const text = this.getAttribute("text") ?? this.textContent ?? "";
|
|
113
|
+
const fontAttr = this.getAttribute("font");
|
|
114
|
+
return {
|
|
115
|
+
text,
|
|
116
|
+
font: this._font ?? (fontAttr || void 0),
|
|
117
|
+
time: this._resolveTime(),
|
|
118
|
+
effects: this._effects,
|
|
119
|
+
timing: this._timing,
|
|
120
|
+
segmentSize: this._getNumberAttr("segment-size"),
|
|
121
|
+
showOverlay: this.hasAttribute("show-overlay"),
|
|
122
|
+
onComplete: this._onComplete
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
_resolveTime() {
|
|
126
|
+
const timeAttr = this.getAttribute("time");
|
|
127
|
+
if (timeAttr === "css") return "css";
|
|
128
|
+
if (timeAttr != null) {
|
|
129
|
+
const num = Number(timeAttr);
|
|
130
|
+
if (!Number.isNaN(num)) return num;
|
|
131
|
+
}
|
|
132
|
+
const hasSpeed = this.hasAttribute("speed");
|
|
133
|
+
const hasPlaying = this.hasAttribute("playing");
|
|
134
|
+
const hasLoop = this.hasAttribute("loop");
|
|
135
|
+
if (hasSpeed || hasPlaying || hasLoop) return {
|
|
136
|
+
mode: "uncontrolled",
|
|
137
|
+
speed: this._getNumberAttr("speed") ?? 1,
|
|
138
|
+
playing: this.getAttribute("playing") !== "false",
|
|
139
|
+
loop: this.hasAttribute("loop")
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
_getNumberAttr(name) {
|
|
143
|
+
const value = this.getAttribute(name);
|
|
144
|
+
if (value == null) return void 0;
|
|
145
|
+
const num = Number(value);
|
|
146
|
+
return Number.isNaN(num) ? void 0 : num;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Register the `<tegaki-renderer>` custom element.
|
|
151
|
+
* Call this once before using the element in HTML.
|
|
152
|
+
*
|
|
153
|
+
* @param tagName - Custom element tag name. Default: `'tegaki-renderer'`.
|
|
154
|
+
* Note: custom element names must contain a hyphen per the HTML spec.
|
|
155
|
+
*/
|
|
156
|
+
function registerTegakiElement(tagName = "tegaki-renderer") {
|
|
157
|
+
if (!customElements.get(tagName)) customElements.define(tagName, TegakiElement);
|
|
158
|
+
}
|
|
159
|
+
//#endregion
|
|
160
|
+
export { TegakiElement, TegakiEngine, computeTextLayout, computeTimeline, drawGlyph, ensureFontFace, registerTegakiElement, resolveEffects };
|
|
161
|
+
|
|
162
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/wc/TegakiElement.ts"],"sourcesContent":["import { TegakiEngine, type TegakiEngineOptions, type TimeControlProp } from '../core/engine.ts';\nimport type { TegakiBundle } from '../types.ts';\n\n/**\n * Observed attribute names.\n * - `text`: the text to render (also settable via textContent)\n * - `font`: registered bundle name (see {@link TegakiEngine.registerBundle})\n * - `time`: time control — a number for controlled mode, `\"css\"` for CSS mode, omit for uncontrolled\n * - `speed`: playback speed multiplier (uncontrolled mode, default `1`)\n * - `playing`: whether animation is playing (uncontrolled mode, default `true`)\n * - `loop`: loop animation (uncontrolled mode, default `false`)\n * - `segment-size`: segment size for rendering\n * - `show-overlay`: show debug overlay\n */\nconst OBSERVED_ATTRS = ['text', 'font', 'time', 'speed', 'playing', 'loop', 'segment-size', 'show-overlay'] as const;\n\nexport class TegakiElement extends HTMLElement {\n static observedAttributes = [...OBSERVED_ATTRS];\n\n private _engine: TegakiEngine | null = null;\n private _container: HTMLDivElement;\n private _font: TegakiBundle | string | undefined;\n private _effects: TegakiEngineOptions['effects'];\n private _timing: TegakiEngineOptions['timing'];\n private _onComplete: (() => void) | undefined;\n\n constructor() {\n super();\n const shadow = this.attachShadow({ mode: 'open' });\n\n // Host styles: the element itself is just an inline-block wrapper\n const style = document.createElement('style');\n style.textContent = `:host { display: inline-block; }`;\n shadow.appendChild(style);\n\n this._container = document.createElement('div');\n shadow.appendChild(this._container);\n }\n\n // ---------------------------------------------------------------------------\n // Lifecycle\n // ---------------------------------------------------------------------------\n\n connectedCallback(): void {\n this._engine = new TegakiEngine(this._container, this._buildOptions());\n }\n\n disconnectedCallback(): void {\n this._engine?.destroy();\n this._engine = null;\n }\n\n attributeChangedCallback(_name: string, _oldValue: string | null, _newValue: string | null): void {\n this._engine?.update(this._buildOptions());\n }\n\n // ---------------------------------------------------------------------------\n // Property API (for JS usage)\n // ---------------------------------------------------------------------------\n\n /** The underlying engine instance. */\n get engine(): TegakiEngine | null {\n return this._engine;\n }\n\n /** Set the font bundle directly (alternative to the `font` attribute for registered names). */\n get font(): TegakiBundle | string | undefined {\n return this._font;\n }\n\n set font(value: TegakiBundle | string | undefined) {\n this._font = value;\n this._engine?.update(this._buildOptions());\n }\n\n /** Visual effects configuration. */\n get effects(): TegakiEngineOptions['effects'] {\n return this._effects;\n }\n\n set effects(value: TegakiEngineOptions['effects']) {\n this._effects = value;\n this._engine?.update(this._buildOptions());\n }\n\n /** Timeline timing configuration. */\n get timing(): TegakiEngineOptions['timing'] {\n return this._timing;\n }\n\n set timing(value: TegakiEngineOptions['timing']) {\n this._timing = value;\n this._engine?.update(this._buildOptions());\n }\n\n /** Callback when animation completes. */\n get onComplete(): (() => void) | undefined {\n return this._onComplete;\n }\n\n set onComplete(value: (() => void) | undefined) {\n this._onComplete = value;\n this._engine?.update(this._buildOptions());\n }\n\n // Playback controls (delegate to engine)\n\n play(): void {\n this._engine?.play();\n }\n\n pause(): void {\n this._engine?.pause();\n }\n\n seek(time: number): void {\n this._engine?.seek(time);\n }\n\n restart(): void {\n this._engine?.restart();\n }\n\n get currentTime(): number {\n return this._engine?.currentTime ?? 0;\n }\n\n get duration(): number {\n return this._engine?.duration ?? 0;\n }\n\n get isPlaying(): boolean {\n return this._engine?.isPlaying ?? false;\n }\n\n get isComplete(): boolean {\n return this._engine?.isComplete ?? false;\n }\n\n // ---------------------------------------------------------------------------\n // Internal\n // ---------------------------------------------------------------------------\n\n private _buildOptions(): TegakiEngineOptions {\n const text = this.getAttribute('text') ?? this.textContent ?? '';\n const fontAttr = this.getAttribute('font');\n const font = this._font ?? (fontAttr || undefined);\n const time = this._resolveTime();\n\n return {\n text,\n font,\n time,\n effects: this._effects,\n timing: this._timing,\n segmentSize: this._getNumberAttr('segment-size'),\n showOverlay: this.hasAttribute('show-overlay'),\n onComplete: this._onComplete,\n };\n }\n\n private _resolveTime(): TimeControlProp {\n const timeAttr = this.getAttribute('time');\n\n if (timeAttr === 'css') return 'css';\n if (timeAttr != null) {\n const num = Number(timeAttr);\n if (!Number.isNaN(num)) return num;\n }\n\n // Check for uncontrolled mode attributes\n const hasSpeed = this.hasAttribute('speed');\n const hasPlaying = this.hasAttribute('playing');\n const hasLoop = this.hasAttribute('loop');\n\n if (hasSpeed || hasPlaying || hasLoop) {\n return {\n mode: 'uncontrolled',\n speed: this._getNumberAttr('speed') ?? 1,\n playing: this.getAttribute('playing') !== 'false',\n loop: this.hasAttribute('loop'),\n };\n }\n\n return undefined;\n }\n\n private _getNumberAttr(name: string): number | undefined {\n const value = this.getAttribute(name);\n if (value == null) return undefined;\n const num = Number(value);\n return Number.isNaN(num) ? undefined : num;\n }\n}\n\n/**\n * Register the `<tegaki-renderer>` custom element.\n * Call this once before using the element in HTML.\n *\n * @param tagName - Custom element tag name. Default: `'tegaki-renderer'`.\n * Note: custom element names must contain a hyphen per the HTML spec.\n */\nexport function registerTegakiElement(tagName = 'tegaki-renderer'): void {\n if (!customElements.get(tagName)) {\n customElements.define(tagName, TegakiElement);\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tegaki",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Framework-agnostic animated handwriting renderer with React, Svelte, Vue, SolidJS, and
|
|
5
|
+
"description": "Framework-agnostic animated handwriting renderer with React, Svelte, Vue, SolidJS, Astro, and Web Components adapters",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"handwriting",
|
|
8
8
|
"animation",
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
"vue",
|
|
13
13
|
"solid",
|
|
14
14
|
"astro",
|
|
15
|
+
"web-components",
|
|
16
|
+
"custom-elements",
|
|
15
17
|
"font",
|
|
16
18
|
"calligraphy",
|
|
17
19
|
"stroke-order",
|
|
@@ -69,6 +71,12 @@
|
|
|
69
71
|
"types": "./dist/solid/index.d.mts",
|
|
70
72
|
"import": "./dist/solid/index.mjs"
|
|
71
73
|
},
|
|
74
|
+
"./wc": {
|
|
75
|
+
"tegaki@dev": "./src/wc/index.ts",
|
|
76
|
+
"source": "./src/wc/index.ts",
|
|
77
|
+
"types": "./dist/wc/index.d.mts",
|
|
78
|
+
"import": "./dist/wc/index.mjs"
|
|
79
|
+
},
|
|
72
80
|
"./astro": "./src/astro/TegakiRenderer.astro",
|
|
73
81
|
"./fonts/caveat": "./fonts/caveat/bundle.ts",
|
|
74
82
|
"./fonts/italianno": "./fonts/italianno/bundle.ts",
|
|
@@ -86,7 +94,7 @@
|
|
|
86
94
|
],
|
|
87
95
|
"scripts": {
|
|
88
96
|
"typecheck": "tsc --noEmit",
|
|
89
|
-
"build": "tsdown src/index.ts src/core/index.ts src/react/index.ts src/solid/index.ts --dts --sourcemap",
|
|
97
|
+
"build": "tsdown src/index.ts src/core/index.ts src/react/index.ts src/solid/index.ts src/wc/index.ts --dts --sourcemap",
|
|
90
98
|
"prepack": "bun run build && bun ../../scripts/prepareForRelease.ts",
|
|
91
99
|
"generate-fonts": "bun --filter tegaki-generator start generate Caveat --output ../renderer/fonts/caveat && bun --filter tegaki-generator start generate Italianno --output ../renderer/fonts/italianno && bun --filter tegaki-generator start generate Tangerine --output ../renderer/fonts/tangerine && bun --filter tegaki-generator start generate Parisienne --output ../renderer/fonts/parisienne"
|
|
92
100
|
},
|
|
@@ -11,7 +11,7 @@ interface Props extends Omit<TegakiEngineOptions, 'onComplete'> {
|
|
|
11
11
|
[key: string]: any;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const { text, font, bundle: registerBundle, loadFont, time, effects, timing, segmentSize, showOverlay, class: className, ...attrs } = Astro.props;
|
|
14
|
+
const { text, font, bundle: registerBundle, loadFont, time, effects, timing, segmentSize, showOverlay, class: className, style: userStyle, ...attrs } = Astro.props;
|
|
15
15
|
|
|
16
16
|
// Resolve the font bundle for SSR
|
|
17
17
|
let resolvedFont: TegakiBundle | undefined;
|
|
@@ -73,7 +73,9 @@ function htmlCreateElement(tag: string, props: Record<string, any>, ...children:
|
|
|
73
73
|
|
|
74
74
|
const renderResult = shouldRender ? TegakiEngine.renderElements(engineOptions, htmlCreateElement) : null;
|
|
75
75
|
const innerHtml = renderResult?.content ?? '';
|
|
76
|
-
const rootStyle = renderResult
|
|
76
|
+
const rootStyle = renderResult
|
|
77
|
+
? styleToString(renderResult.rootProps.style) + (userStyle ? `;${typeof userStyle === 'object' ? styleToString(userStyle) : userStyle}` : '')
|
|
78
|
+
: typeof userStyle === 'object' ? styleToString(userStyle) : (userStyle ?? '');
|
|
77
79
|
|
|
78
80
|
// For serialization, replace the full bundle with just the family name.
|
|
79
81
|
const clientOptions = {
|
|
@@ -96,5 +96,7 @@ export function TegakiRenderer(props: TegakiRendererProps) {
|
|
|
96
96
|
}),
|
|
97
97
|
);
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
const mergedStyle = { ...rootProps.style, ...(typeof divProps.style === 'object' ? divProps.style : {}) };
|
|
100
|
+
|
|
101
|
+
return <div ref={container!} data-tegaki="root" {...divProps} style={mergedStyle} innerHTML={innerHTML} />;
|
|
100
102
|
}
|
|
@@ -11,7 +11,7 @@ interface Props extends Omit<TegakiEngineOptions, 'effects'> {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
// biome-ignore lint/correctness/noUnusedVariables: attrs is used in Svelte template
|
|
14
|
-
let { text, font, time: timeProp, onComplete, effects, segmentSize, timing, showOverlay, class: className, ...attrs }: Props = $props();
|
|
14
|
+
let { text, font, time: timeProp, onComplete, effects, segmentSize, timing, showOverlay, class: className, style: userStyle, ...attrs }: Props = $props();
|
|
15
15
|
|
|
16
16
|
let container = $state<HTMLDivElement | undefined>();
|
|
17
17
|
let engine = $state<TegakiEngine | null>(null);
|
|
@@ -78,7 +78,7 @@ function styleToString(style: Record<string, any>): string {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
// biome-ignore lint/correctness/noUnusedVariables: used in Svelte template
|
|
81
|
-
const rootStyleStr = styleToString(rootProps.style);
|
|
81
|
+
const rootStyleStr = styleToString(rootProps.style) + (userStyle ? `;${userStyle}` : '');
|
|
82
82
|
|
|
83
83
|
$effect(() => {
|
|
84
84
|
if (!container) return;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { TegakiEngine, type TegakiEngineOptions, type TimeControlProp } from '../core/engine.ts';
|
|
2
|
+
import type { TegakiBundle } from '../types.ts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Observed attribute names.
|
|
6
|
+
* - `text`: the text to render (also settable via textContent)
|
|
7
|
+
* - `font`: registered bundle name (see {@link TegakiEngine.registerBundle})
|
|
8
|
+
* - `time`: time control — a number for controlled mode, `"css"` for CSS mode, omit for uncontrolled
|
|
9
|
+
* - `speed`: playback speed multiplier (uncontrolled mode, default `1`)
|
|
10
|
+
* - `playing`: whether animation is playing (uncontrolled mode, default `true`)
|
|
11
|
+
* - `loop`: loop animation (uncontrolled mode, default `false`)
|
|
12
|
+
* - `segment-size`: segment size for rendering
|
|
13
|
+
* - `show-overlay`: show debug overlay
|
|
14
|
+
*/
|
|
15
|
+
const OBSERVED_ATTRS = ['text', 'font', 'time', 'speed', 'playing', 'loop', 'segment-size', 'show-overlay'] as const;
|
|
16
|
+
|
|
17
|
+
export class TegakiElement extends HTMLElement {
|
|
18
|
+
static observedAttributes = [...OBSERVED_ATTRS];
|
|
19
|
+
|
|
20
|
+
private _engine: TegakiEngine | null = null;
|
|
21
|
+
private _container: HTMLDivElement;
|
|
22
|
+
private _font: TegakiBundle | string | undefined;
|
|
23
|
+
private _effects: TegakiEngineOptions['effects'];
|
|
24
|
+
private _timing: TegakiEngineOptions['timing'];
|
|
25
|
+
private _onComplete: (() => void) | undefined;
|
|
26
|
+
|
|
27
|
+
constructor() {
|
|
28
|
+
super();
|
|
29
|
+
const shadow = this.attachShadow({ mode: 'open' });
|
|
30
|
+
|
|
31
|
+
// Host styles: the element itself is just an inline-block wrapper
|
|
32
|
+
const style = document.createElement('style');
|
|
33
|
+
style.textContent = `:host { display: inline-block; }`;
|
|
34
|
+
shadow.appendChild(style);
|
|
35
|
+
|
|
36
|
+
this._container = document.createElement('div');
|
|
37
|
+
shadow.appendChild(this._container);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Lifecycle
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
connectedCallback(): void {
|
|
45
|
+
this._engine = new TegakiEngine(this._container, this._buildOptions());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
disconnectedCallback(): void {
|
|
49
|
+
this._engine?.destroy();
|
|
50
|
+
this._engine = null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
attributeChangedCallback(_name: string, _oldValue: string | null, _newValue: string | null): void {
|
|
54
|
+
this._engine?.update(this._buildOptions());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Property API (for JS usage)
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
/** The underlying engine instance. */
|
|
62
|
+
get engine(): TegakiEngine | null {
|
|
63
|
+
return this._engine;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Set the font bundle directly (alternative to the `font` attribute for registered names). */
|
|
67
|
+
get font(): TegakiBundle | string | undefined {
|
|
68
|
+
return this._font;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
set font(value: TegakiBundle | string | undefined) {
|
|
72
|
+
this._font = value;
|
|
73
|
+
this._engine?.update(this._buildOptions());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Visual effects configuration. */
|
|
77
|
+
get effects(): TegakiEngineOptions['effects'] {
|
|
78
|
+
return this._effects;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
set effects(value: TegakiEngineOptions['effects']) {
|
|
82
|
+
this._effects = value;
|
|
83
|
+
this._engine?.update(this._buildOptions());
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Timeline timing configuration. */
|
|
87
|
+
get timing(): TegakiEngineOptions['timing'] {
|
|
88
|
+
return this._timing;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
set timing(value: TegakiEngineOptions['timing']) {
|
|
92
|
+
this._timing = value;
|
|
93
|
+
this._engine?.update(this._buildOptions());
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Callback when animation completes. */
|
|
97
|
+
get onComplete(): (() => void) | undefined {
|
|
98
|
+
return this._onComplete;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
set onComplete(value: (() => void) | undefined) {
|
|
102
|
+
this._onComplete = value;
|
|
103
|
+
this._engine?.update(this._buildOptions());
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Playback controls (delegate to engine)
|
|
107
|
+
|
|
108
|
+
play(): void {
|
|
109
|
+
this._engine?.play();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
pause(): void {
|
|
113
|
+
this._engine?.pause();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
seek(time: number): void {
|
|
117
|
+
this._engine?.seek(time);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
restart(): void {
|
|
121
|
+
this._engine?.restart();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
get currentTime(): number {
|
|
125
|
+
return this._engine?.currentTime ?? 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
get duration(): number {
|
|
129
|
+
return this._engine?.duration ?? 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
get isPlaying(): boolean {
|
|
133
|
+
return this._engine?.isPlaying ?? false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
get isComplete(): boolean {
|
|
137
|
+
return this._engine?.isComplete ?? false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// Internal
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
private _buildOptions(): TegakiEngineOptions {
|
|
145
|
+
const text = this.getAttribute('text') ?? this.textContent ?? '';
|
|
146
|
+
const fontAttr = this.getAttribute('font');
|
|
147
|
+
const font = this._font ?? (fontAttr || undefined);
|
|
148
|
+
const time = this._resolveTime();
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
text,
|
|
152
|
+
font,
|
|
153
|
+
time,
|
|
154
|
+
effects: this._effects,
|
|
155
|
+
timing: this._timing,
|
|
156
|
+
segmentSize: this._getNumberAttr('segment-size'),
|
|
157
|
+
showOverlay: this.hasAttribute('show-overlay'),
|
|
158
|
+
onComplete: this._onComplete,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private _resolveTime(): TimeControlProp {
|
|
163
|
+
const timeAttr = this.getAttribute('time');
|
|
164
|
+
|
|
165
|
+
if (timeAttr === 'css') return 'css';
|
|
166
|
+
if (timeAttr != null) {
|
|
167
|
+
const num = Number(timeAttr);
|
|
168
|
+
if (!Number.isNaN(num)) return num;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Check for uncontrolled mode attributes
|
|
172
|
+
const hasSpeed = this.hasAttribute('speed');
|
|
173
|
+
const hasPlaying = this.hasAttribute('playing');
|
|
174
|
+
const hasLoop = this.hasAttribute('loop');
|
|
175
|
+
|
|
176
|
+
if (hasSpeed || hasPlaying || hasLoop) {
|
|
177
|
+
return {
|
|
178
|
+
mode: 'uncontrolled',
|
|
179
|
+
speed: this._getNumberAttr('speed') ?? 1,
|
|
180
|
+
playing: this.getAttribute('playing') !== 'false',
|
|
181
|
+
loop: this.hasAttribute('loop'),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private _getNumberAttr(name: string): number | undefined {
|
|
189
|
+
const value = this.getAttribute(name);
|
|
190
|
+
if (value == null) return undefined;
|
|
191
|
+
const num = Number(value);
|
|
192
|
+
return Number.isNaN(num) ? undefined : num;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Register the `<tegaki-renderer>` custom element.
|
|
198
|
+
* Call this once before using the element in HTML.
|
|
199
|
+
*
|
|
200
|
+
* @param tagName - Custom element tag name. Default: `'tegaki-renderer'`.
|
|
201
|
+
* Note: custom element names must contain a hyphen per the HTML spec.
|
|
202
|
+
*/
|
|
203
|
+
export function registerTegakiElement(tagName = 'tegaki-renderer'): void {
|
|
204
|
+
if (!customElements.get(tagName)) {
|
|
205
|
+
customElements.define(tagName, TegakiElement);
|
|
206
|
+
}
|
|
207
|
+
}
|
package/src/wc/index.ts
ADDED