badgecraft 0.3.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 +31 -0
- package/LICENSE +21 -0
- package/README.md +406 -0
- package/cli/index.mjs +143 -0
- package/cli/render.d.ts +20 -0
- package/cli/render.mjs +87 -0
- package/cli/studio.mjs +98 -0
- package/dist/badge-B-g_9WFj.d.ts +79 -0
- package/dist/badge-CCjUxNIG.d.cts +79 -0
- package/dist/chunk-3OMN635U.js +44 -0
- package/dist/chunk-3OMN635U.js.map +1 -0
- package/dist/chunk-BRT5S5PF.cjs +3418 -0
- package/dist/chunk-BRT5S5PF.cjs.map +1 -0
- package/dist/chunk-EBDSIGZR.cjs +48 -0
- package/dist/chunk-EBDSIGZR.cjs.map +1 -0
- package/dist/chunk-RRYBMB25.js +3410 -0
- package/dist/chunk-RRYBMB25.js.map +1 -0
- package/dist/cli-browser.js +526 -0
- package/dist/element.cjs +176 -0
- package/dist/element.cjs.map +1 -0
- package/dist/element.d.cts +31 -0
- package/dist/element.d.ts +31 -0
- package/dist/element.js +174 -0
- package/dist/element.js.map +1 -0
- package/dist/index.cjs +47 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +58 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +60 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +29 -0
- package/dist/react.d.ts +29 -0
- package/dist/react.js +55 -0
- package/dist/react.js.map +1 -0
- package/dist/studio/assets/badge-9Zq6e9Ht.js +524 -0
- package/dist/studio/assets/base-ItCYnbux.css +1 -0
- package/dist/studio/assets/base-LFa9cTPp.js +1 -0
- package/dist/studio/assets/element-CCTxyf61.js +1 -0
- package/dist/studio/assets/index-BVuSTaz-.css +1 -0
- package/dist/studio/assets/index-CTTcx2pV.js +28 -0
- package/dist/studio/assets/option-keys-CKi71y0n.js +1 -0
- package/dist/studio/assets/react-Cq5Erey2.js +9 -0
- package/dist/studio/assets/react-DyJFYpWJ.js +1 -0
- package/dist/studio/assets/studio-C6plMNOe.css +1 -0
- package/dist/studio/assets/studio-DBcSY6tV.js +1 -0
- package/dist/studio/assets/style-C2sJ2Xy3.css +1 -0
- package/dist/studio/assets/vue-BpGwr3iF.js +1 -0
- package/dist/studio/element.html +31 -0
- package/dist/studio/env/studio.hdr +5 -0
- package/dist/studio/index.html +24 -0
- package/dist/studio/logos/apple.svg +1 -0
- package/dist/studio/logos/atlassian.svg +1 -0
- package/dist/studio/logos/google.svg +1 -0
- package/dist/studio/logos/meta.svg +1 -0
- package/dist/studio/react.html +16 -0
- package/dist/studio/studio.html +27 -0
- package/dist/studio/vue.html +15 -0
- package/dist/types-CHM3-ZZV.d.cts +204 -0
- package/dist/types-CHM3-ZZV.d.ts +204 -0
- package/dist/vue.cjs +93 -0
- package/dist/vue.cjs.map +1 -0
- package/dist/vue.d.cts +115 -0
- package/dist/vue.d.ts +115 -0
- package/dist/vue.js +87 -0
- package/dist/vue.js.map +1 -0
- package/docs/cli.md +146 -0
- package/docs/gallery.jpg +0 -0
- package/docs/performance.md +151 -0
- package/docs/render-quality.md +220 -0
- package/docs/studio.jpg +0 -0
- package/package.json +163 -0
package/cli/render.mjs
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { chromium } from 'playwright-core'
|
|
3
|
+
|
|
4
|
+
/** Render SVG markup with the same WebGL2 renderer used by the browser library. */
|
|
5
|
+
export async function renderBadge(svg, options = {}) {
|
|
6
|
+
const { size = 1024, format = 'png', background, imageQuality = 0.92,
|
|
7
|
+
timeout = 60000, executablePath = process.env.BADGECRAFT_CHROME_PATH ?? process.env.METALLIC_BADGE_CHROME_PATH,
|
|
8
|
+
sandbox = true, ...badgeOptions } = options
|
|
9
|
+
if (typeof svg !== 'string' || !svg.trim().startsWith('<')) throw new Error('Expected SVG markup.')
|
|
10
|
+
if (Buffer.byteLength(svg) > 10 * 1024 * 1024) throw new Error('SVG exceeds the 10 MiB input limit.')
|
|
11
|
+
if (!Number.isInteger(size) || size < 16 || size > 2048) throw new Error('size must be an integer from 16 to 2048.')
|
|
12
|
+
if (!['png', 'webp', 'jpeg'].includes(format)) throw new Error('format must be png, webp, or jpeg.')
|
|
13
|
+
if (!Number.isFinite(timeout) || timeout < 100 || timeout > 300000) throw new Error('timeout must be from 100 to 300000 milliseconds.')
|
|
14
|
+
if (!Number.isFinite(imageQuality) || imageQuality < 0 || imageQuality > 1) throw new Error('imageQuality must be from 0 to 1.')
|
|
15
|
+
if (background !== undefined && typeof background !== 'string') throw new Error('background must be a CSS color.')
|
|
16
|
+
// Image URLs are deliberately unsupported: conversions must not make network requests.
|
|
17
|
+
if (badgeOptions.environment !== undefined && !['studio', 'soft', 'sunset', 'night', 'neon'].includes(badgeOptions.environment)) {
|
|
18
|
+
throw new Error('environment must be studio, soft, sunset, night, or neon.')
|
|
19
|
+
}
|
|
20
|
+
const bundle = await readFile(new URL('../dist/cli-browser.js', import.meta.url), 'utf8')
|
|
21
|
+
let browser
|
|
22
|
+
try {
|
|
23
|
+
browser = await chromium.launch({
|
|
24
|
+
...(executablePath ? { executablePath } : { channel: 'chrome' }),
|
|
25
|
+
headless: true,
|
|
26
|
+
chromiumSandbox: sandbox,
|
|
27
|
+
timeout,
|
|
28
|
+
args: ['--use-angle=swiftshader', '--enable-unsafe-swiftshader'],
|
|
29
|
+
})
|
|
30
|
+
} catch (error) {
|
|
31
|
+
throw new Error(`Could not start Google Chrome. Install Chrome or set BADGECRAFT_CHROME_PATH to its executable. In a container, configure Chrome sandboxing or explicitly pass --no-sandbox.\n${error.message}`, { cause: error })
|
|
32
|
+
}
|
|
33
|
+
let timer
|
|
34
|
+
try {
|
|
35
|
+
const render = async () => {
|
|
36
|
+
const context = await browser.newContext({ viewport: { width: size, height: size }, deviceScaleFactor: 1, serviceWorkers: 'block' })
|
|
37
|
+
// SVG styles can reference remote assets; never allow them to contact the network.
|
|
38
|
+
await context.route(/^(https?|file|ftp):/i, (route) => route.abort())
|
|
39
|
+
const page = await context.newPage()
|
|
40
|
+
page.setDefaultTimeout(timeout)
|
|
41
|
+
await page.setContent('<!doctype html><html><head><meta charset="utf-8"></head><body style="margin:0"><div id="badge" style="width:100vw;height:100vh"></div></body></html>')
|
|
42
|
+
await page.addScriptTag({ content: bundle })
|
|
43
|
+
return page.evaluate(async ({ svg, badgeOptions, size, format, background, imageQuality }) => {
|
|
44
|
+
const { Badgecraft, isSupported } = window.BadgecraftLibrary
|
|
45
|
+
if (!isSupported()) throw new Error('WebGL2 is unavailable in Chrome. Enable software WebGL support; a flat SVG fallback is not an export.')
|
|
46
|
+
if (background !== undefined && !CSS.supports('color', background)) throw new Error('background must be a valid CSS color.')
|
|
47
|
+
const badge = new Badgecraft(document.getElementById('badge'), { ...badgeOptions, svg, idle: 'none', interaction: 'none' })
|
|
48
|
+
try {
|
|
49
|
+
await badge.ready
|
|
50
|
+
// toDataURL forces a synchronous draw of the settled pose.
|
|
51
|
+
badge.toDataURL()
|
|
52
|
+
const canvas = document.createElement('canvas')
|
|
53
|
+
canvas.width = canvas.height = size
|
|
54
|
+
const ctx = canvas.getContext('2d')
|
|
55
|
+
if (format === 'jpeg') {
|
|
56
|
+
ctx.fillStyle = '#ffffff'
|
|
57
|
+
ctx.fillRect(0, 0, size, size)
|
|
58
|
+
}
|
|
59
|
+
if (background !== undefined) {
|
|
60
|
+
ctx.fillStyle = background
|
|
61
|
+
ctx.fillRect(0, 0, size, size)
|
|
62
|
+
}
|
|
63
|
+
ctx.drawImage(badge.canvas, 0, 0, size, size)
|
|
64
|
+
// Detect failed/empty artwork before a background can hide it.
|
|
65
|
+
const artwork = badge.canvas.getContext('2d').getImageData(0, 0, badge.canvas.width, badge.canvas.height).data
|
|
66
|
+
let visible = false
|
|
67
|
+
for (let i = 3; i < artwork.length; i += 4) if (artwork[i] > 0) { visible = true; break }
|
|
68
|
+
if (!visible) throw new Error('The SVG produced no visible artwork.')
|
|
69
|
+
const mime = `image/${format}`
|
|
70
|
+
const data = canvas.toDataURL(mime, imageQuality)
|
|
71
|
+
if (!data.startsWith(`data:${mime};base64,`)) throw new Error(`Chrome does not support ${format} export.`)
|
|
72
|
+
return data.slice(data.indexOf(',') + 1)
|
|
73
|
+
} finally {
|
|
74
|
+
badge.destroy()
|
|
75
|
+
}
|
|
76
|
+
}, { svg, badgeOptions, size, format, background, imageQuality })
|
|
77
|
+
}
|
|
78
|
+
const data = await Promise.race([
|
|
79
|
+
render(),
|
|
80
|
+
new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(`Rendering timed out after ${timeout} ms.`)), timeout) }),
|
|
81
|
+
])
|
|
82
|
+
return Buffer.from(data, 'base64')
|
|
83
|
+
} finally {
|
|
84
|
+
clearTimeout(timer)
|
|
85
|
+
await browser.close()
|
|
86
|
+
}
|
|
87
|
+
}
|
package/cli/studio.mjs
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { createServer } from 'node:http'
|
|
2
|
+
import { readFile, realpath } from 'node:fs/promises'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { resolve, sep, extname } from 'node:path'
|
|
5
|
+
import { parseArgs } from 'node:util'
|
|
6
|
+
import { spawn } from 'node:child_process'
|
|
7
|
+
|
|
8
|
+
const directory = fileURLToPath(new URL('../dist/studio/', import.meta.url))
|
|
9
|
+
const types = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.svg': 'image/svg+xml', '.hdr': 'application/octet-stream', '.png': 'image/png', '.jpg': 'image/jpeg', '.ico': 'image/x-icon' }
|
|
10
|
+
|
|
11
|
+
// Serve only the installed app. Artwork is read inside the browser, never by this server.
|
|
12
|
+
export async function startStudio({ port = 5199 } = {}) {
|
|
13
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error('Studio port must be an integer from 0 to 65535.')
|
|
14
|
+
const root = await realpath(directory)
|
|
15
|
+
await readFile(resolve(root, 'studio.html'))
|
|
16
|
+
const server = createServer(async (req, res) => {
|
|
17
|
+
const address = server.address()
|
|
18
|
+
const allowedHosts = [`127.0.0.1:${address.port}`, `localhost:${address.port}`]
|
|
19
|
+
res.setHeader('X-Content-Type-Options', 'nosniff')
|
|
20
|
+
res.setHeader('Cache-Control', 'no-store')
|
|
21
|
+
res.setHeader('Cross-Origin-Resource-Policy', 'same-origin')
|
|
22
|
+
if (!allowedHosts.includes(req.headers.host)) { res.writeHead(403).end('Forbidden'); return }
|
|
23
|
+
if (!['GET', 'HEAD'].includes(req.method)) { res.writeHead(405, { Allow: 'GET, HEAD' }).end(); return }
|
|
24
|
+
try {
|
|
25
|
+
const path = decodeURIComponent((req.url ?? '/').split('?')[0])
|
|
26
|
+
if (!path.startsWith('/') || /[\\\0]/.test(path) || path.split('/').includes('..')) {
|
|
27
|
+
res.writeHead(400).end('Invalid path'); return
|
|
28
|
+
}
|
|
29
|
+
const file = resolve(root, `.${path === '/' ? '/studio.html' : path}`)
|
|
30
|
+
if (!file.startsWith(root + sep) || !types[extname(file)]) { res.writeHead(404).end('Not found'); return }
|
|
31
|
+
const actual = await realpath(file)
|
|
32
|
+
if (!actual.startsWith(root + sep)) { res.writeHead(404).end('Not found'); return }
|
|
33
|
+
const data = await readFile(actual)
|
|
34
|
+
res.writeHead(200, { 'Content-Type': types[extname(file)], 'Content-Length': data.length })
|
|
35
|
+
res.end(req.method === 'HEAD' ? undefined : data)
|
|
36
|
+
} catch (error) {
|
|
37
|
+
const status = error instanceof URIError ? 400 : ['ENOENT', 'EISDIR', 'ENOTDIR'].includes(error.code) ? 404 : 500
|
|
38
|
+
res.writeHead(status).end(status === 500 ? 'Unable to read Studio asset' : 'Not found')
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
await new Promise((accept, reject) => {
|
|
42
|
+
server.once('error', reject)
|
|
43
|
+
server.listen(port, '127.0.0.1', () => { server.off('error', reject); accept() })
|
|
44
|
+
})
|
|
45
|
+
return { server, url: `http://127.0.0.1:${server.address().port}` }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function openChrome(url) {
|
|
49
|
+
const command = process.platform === 'darwin' ? 'open' : process.env.BADGECRAFT_CHROME_PATH || process.env.METALLIC_BADGE_CHROME_PATH || (process.platform === 'win32' ? `${process.env.PROGRAMFILES}\\Google\\Chrome\\Application\\chrome.exe` : 'google-chrome')
|
|
50
|
+
const args = process.platform === 'darwin' ? ['-a', 'Google Chrome', url] : [url]
|
|
51
|
+
await new Promise((accept, reject) => {
|
|
52
|
+
const child = spawn(command, args, { detached: true, stdio: 'ignore' })
|
|
53
|
+
child.once('error', reject)
|
|
54
|
+
if (process.platform === 'darwin') child.once('exit', (code) => code === 0 ? accept() : reject(new Error('Google Chrome could not be opened.')))
|
|
55
|
+
else child.once('spawn', () => { child.unref(); accept() })
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function runStudio(argv) {
|
|
60
|
+
const { values } = parseArgs({ args: argv, options: {
|
|
61
|
+
port: { type: 'string', short: 'p' }, open: { type: 'boolean' }, help: { type: 'boolean', short: 'h' },
|
|
62
|
+
} })
|
|
63
|
+
if (values.help) {
|
|
64
|
+
process.stdout.write(`badgecraft studio — upload, customize, export, and copy embed code
|
|
65
|
+
|
|
66
|
+
Usage: badgecraft studio [options]
|
|
67
|
+
-p, --port <number> Local port (default: 5199; 0 chooses an available port)
|
|
68
|
+
--open Open Google Chrome
|
|
69
|
+
-h, --help Show help
|
|
70
|
+
|
|
71
|
+
Runs entirely on this computer. Artwork stays in your browser.
|
|
72
|
+
Requires Node.js 22+ and a browser with WebGL2. No checkout or build tools needed.
|
|
73
|
+
Press Ctrl+C to stop the server.
|
|
74
|
+
`)
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
if (values.port !== undefined && !/^\d+$/.test(values.port)) throw new Error('--port must be an integer from 0 to 65535.')
|
|
78
|
+
let app
|
|
79
|
+
try { app = await startStudio(values.port === undefined ? {} : { port: Number(values.port) }) }
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error.code !== 'EADDRINUSE') throw error
|
|
82
|
+
if (values.port !== undefined) throw new Error(`Port ${values.port} is in use. Choose another --port, or use --port 0.`)
|
|
83
|
+
app = await startStudio({ port: 0 })
|
|
84
|
+
}
|
|
85
|
+
process.stdout.write(`Badgecraft Studio: ${app.url}/\nPress Ctrl+C to stop.\n`)
|
|
86
|
+
const stop = () => {
|
|
87
|
+
process.off('SIGINT', stop)
|
|
88
|
+
process.off('SIGTERM', stop)
|
|
89
|
+
app.server.close()
|
|
90
|
+
app.server.closeAllConnections()
|
|
91
|
+
}
|
|
92
|
+
process.once('SIGINT', stop)
|
|
93
|
+
process.once('SIGTERM', stop)
|
|
94
|
+
if (values.open) {
|
|
95
|
+
try { await openChrome(`${app.url}/`) }
|
|
96
|
+
catch { process.stderr.write(`Could not open Google Chrome. Open ${app.url}/ in your browser.\n`) }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { b as BadgecraftHandle, B as BadgeOptions } from './types-CHM3-ZZV.js';
|
|
2
|
+
|
|
3
|
+
/** One requestAnimationFrame loop for every badge on the page. */
|
|
4
|
+
interface FrameTask {
|
|
5
|
+
/** Advance by `dt` seconds. Return true to keep receiving frames. */
|
|
6
|
+
frame(dt: number, now: number): boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A badge rendered into `host`. Framework wrappers (React, Vue, the
|
|
11
|
+
* custom element) are thin layers over this class.
|
|
12
|
+
*/
|
|
13
|
+
declare class Badgecraft implements BadgecraftHandle, FrameTask {
|
|
14
|
+
readonly host: HTMLElement;
|
|
15
|
+
readonly canvas: HTMLCanvasElement;
|
|
16
|
+
/** Resolves once the badge has been baked and drawn; rejects if that fails. */
|
|
17
|
+
readonly ready: Promise<void>;
|
|
18
|
+
private ctx;
|
|
19
|
+
private options;
|
|
20
|
+
private resolved;
|
|
21
|
+
private renderer;
|
|
22
|
+
private maps;
|
|
23
|
+
private mapSize;
|
|
24
|
+
private pendingSize;
|
|
25
|
+
private env;
|
|
26
|
+
private fallbackImage;
|
|
27
|
+
private bakeToken;
|
|
28
|
+
private envToken;
|
|
29
|
+
private visible;
|
|
30
|
+
private destroyed;
|
|
31
|
+
private dirty;
|
|
32
|
+
private isReady;
|
|
33
|
+
private resolveReady;
|
|
34
|
+
private rejectReady;
|
|
35
|
+
private cleanups;
|
|
36
|
+
private pitch;
|
|
37
|
+
private yaw;
|
|
38
|
+
private hover;
|
|
39
|
+
private drag;
|
|
40
|
+
private coasting;
|
|
41
|
+
private spinning;
|
|
42
|
+
private time;
|
|
43
|
+
private shine;
|
|
44
|
+
private shineStart;
|
|
45
|
+
constructor(host: HTMLElement, options: BadgeOptions);
|
|
46
|
+
private ensureHostSize;
|
|
47
|
+
/** Merge new options. Only what changed is recomputed. */
|
|
48
|
+
update(partial: Partial<BadgeOptions>): void;
|
|
49
|
+
/** Replace all options. */
|
|
50
|
+
setOptions(options: BadgeOptions): void;
|
|
51
|
+
spin(turns?: number): void;
|
|
52
|
+
/** Sweep a highlight across the badge once. */
|
|
53
|
+
glint(): void;
|
|
54
|
+
toDataURL(type?: string, quality?: number): string;
|
|
55
|
+
toBlob(type?: string, quality?: number): Promise<Blob | null>;
|
|
56
|
+
destroy(): void;
|
|
57
|
+
private observe;
|
|
58
|
+
private resize;
|
|
59
|
+
private targetMapSize;
|
|
60
|
+
private rebuild;
|
|
61
|
+
private loadEnvironment;
|
|
62
|
+
private handleContextRestore;
|
|
63
|
+
private fail;
|
|
64
|
+
private markReady;
|
|
65
|
+
/** Without WebGL2, show the flat artwork. */
|
|
66
|
+
private loadFallback;
|
|
67
|
+
private paintFallback;
|
|
68
|
+
private invalidate;
|
|
69
|
+
private renderNow;
|
|
70
|
+
/** Scheduler callback: advance motion, redraw if needed. */
|
|
71
|
+
frame(dt: number): boolean;
|
|
72
|
+
private stopMotion;
|
|
73
|
+
private applyInteractionStyle;
|
|
74
|
+
private bindPointer;
|
|
75
|
+
}
|
|
76
|
+
/** Convenience factory. */
|
|
77
|
+
declare function createBadge(host: HTMLElement, options: BadgeOptions): Badgecraft;
|
|
78
|
+
|
|
79
|
+
export { Badgecraft as B, createBadge as c };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { b as BadgecraftHandle, B as BadgeOptions } from './types-CHM3-ZZV.cjs';
|
|
2
|
+
|
|
3
|
+
/** One requestAnimationFrame loop for every badge on the page. */
|
|
4
|
+
interface FrameTask {
|
|
5
|
+
/** Advance by `dt` seconds. Return true to keep receiving frames. */
|
|
6
|
+
frame(dt: number, now: number): boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A badge rendered into `host`. Framework wrappers (React, Vue, the
|
|
11
|
+
* custom element) are thin layers over this class.
|
|
12
|
+
*/
|
|
13
|
+
declare class Badgecraft implements BadgecraftHandle, FrameTask {
|
|
14
|
+
readonly host: HTMLElement;
|
|
15
|
+
readonly canvas: HTMLCanvasElement;
|
|
16
|
+
/** Resolves once the badge has been baked and drawn; rejects if that fails. */
|
|
17
|
+
readonly ready: Promise<void>;
|
|
18
|
+
private ctx;
|
|
19
|
+
private options;
|
|
20
|
+
private resolved;
|
|
21
|
+
private renderer;
|
|
22
|
+
private maps;
|
|
23
|
+
private mapSize;
|
|
24
|
+
private pendingSize;
|
|
25
|
+
private env;
|
|
26
|
+
private fallbackImage;
|
|
27
|
+
private bakeToken;
|
|
28
|
+
private envToken;
|
|
29
|
+
private visible;
|
|
30
|
+
private destroyed;
|
|
31
|
+
private dirty;
|
|
32
|
+
private isReady;
|
|
33
|
+
private resolveReady;
|
|
34
|
+
private rejectReady;
|
|
35
|
+
private cleanups;
|
|
36
|
+
private pitch;
|
|
37
|
+
private yaw;
|
|
38
|
+
private hover;
|
|
39
|
+
private drag;
|
|
40
|
+
private coasting;
|
|
41
|
+
private spinning;
|
|
42
|
+
private time;
|
|
43
|
+
private shine;
|
|
44
|
+
private shineStart;
|
|
45
|
+
constructor(host: HTMLElement, options: BadgeOptions);
|
|
46
|
+
private ensureHostSize;
|
|
47
|
+
/** Merge new options. Only what changed is recomputed. */
|
|
48
|
+
update(partial: Partial<BadgeOptions>): void;
|
|
49
|
+
/** Replace all options. */
|
|
50
|
+
setOptions(options: BadgeOptions): void;
|
|
51
|
+
spin(turns?: number): void;
|
|
52
|
+
/** Sweep a highlight across the badge once. */
|
|
53
|
+
glint(): void;
|
|
54
|
+
toDataURL(type?: string, quality?: number): string;
|
|
55
|
+
toBlob(type?: string, quality?: number): Promise<Blob | null>;
|
|
56
|
+
destroy(): void;
|
|
57
|
+
private observe;
|
|
58
|
+
private resize;
|
|
59
|
+
private targetMapSize;
|
|
60
|
+
private rebuild;
|
|
61
|
+
private loadEnvironment;
|
|
62
|
+
private handleContextRestore;
|
|
63
|
+
private fail;
|
|
64
|
+
private markReady;
|
|
65
|
+
/** Without WebGL2, show the flat artwork. */
|
|
66
|
+
private loadFallback;
|
|
67
|
+
private paintFallback;
|
|
68
|
+
private invalidate;
|
|
69
|
+
private renderNow;
|
|
70
|
+
/** Scheduler callback: advance motion, redraw if needed. */
|
|
71
|
+
frame(dt: number): boolean;
|
|
72
|
+
private stopMotion;
|
|
73
|
+
private applyInteractionStyle;
|
|
74
|
+
private bindPointer;
|
|
75
|
+
}
|
|
76
|
+
/** Convenience factory. */
|
|
77
|
+
declare function createBadge(host: HTMLElement, options: BadgeOptions): Badgecraft;
|
|
78
|
+
|
|
79
|
+
export { Badgecraft as B, createBadge as c };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// src/option-keys.ts
|
|
2
|
+
var optionKeys = [
|
|
3
|
+
"svg",
|
|
4
|
+
"mode",
|
|
5
|
+
"metal",
|
|
6
|
+
"enamel",
|
|
7
|
+
"base",
|
|
8
|
+
"accent",
|
|
9
|
+
"color",
|
|
10
|
+
"frame",
|
|
11
|
+
"rim",
|
|
12
|
+
"outline",
|
|
13
|
+
"fillHoles",
|
|
14
|
+
"relief",
|
|
15
|
+
"environment",
|
|
16
|
+
"environmentRotation",
|
|
17
|
+
"exposure",
|
|
18
|
+
"toneMapping",
|
|
19
|
+
"interaction",
|
|
20
|
+
"tiltMax",
|
|
21
|
+
"pose",
|
|
22
|
+
"idle",
|
|
23
|
+
"locked",
|
|
24
|
+
"lockedColor",
|
|
25
|
+
"shadow",
|
|
26
|
+
"quality",
|
|
27
|
+
"padding",
|
|
28
|
+
"onReady",
|
|
29
|
+
"onError"
|
|
30
|
+
];
|
|
31
|
+
var optionKeySet = new Set(optionKeys);
|
|
32
|
+
function pickOptions(source) {
|
|
33
|
+
const out = {};
|
|
34
|
+
for (const key of optionKeys) if (source[key] !== void 0) out[key] = source[key];
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
function cssSize(size, fallback = 160) {
|
|
38
|
+
const s = size ?? fallback;
|
|
39
|
+
return typeof s === "number" ? `${s}px` : s;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { cssSize, optionKeySet, pickOptions };
|
|
43
|
+
//# sourceMappingURL=chunk-3OMN635U.js.map
|
|
44
|
+
//# sourceMappingURL=chunk-3OMN635U.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/option-keys.ts"],"names":[],"mappings":";AAGO,IAAM,UAAA,GAAa;AAAA,EACxB,KAAA;AAAA,EACA,MAAA;AAAA,EACA,OAAA;AAAA,EACA,QAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,OAAA;AAAA,EACA,OAAA;AAAA,EACA,KAAA;AAAA,EACA,SAAA;AAAA,EACA,WAAA;AAAA,EACA,QAAA;AAAA,EACA,aAAA;AAAA,EACA,qBAAA;AAAA,EACA,UAAA;AAAA,EACA,aAAA;AAAA,EACA,aAAA;AAAA,EACA,SAAA;AAAA,EACA,MAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,aAAA;AAAA,EACA,QAAA;AAAA,EACA,SAAA;AAAA,EACA,SAAA;AAAA,EACA,SAAA;AAAA,EACA;AACF,CAAA;AAOO,IAAM,YAAA,GAAoC,IAAI,GAAA,CAAI,UAAU;AAG5D,SAAS,YAAY,MAAA,EAA+C;AACzE,EAAA,MAAM,MAA+B,EAAC;AACtC,EAAA,KAAA,MAAW,GAAA,IAAO,UAAA,EAAY,IAAI,MAAA,CAAO,GAAG,CAAA,KAAM,MAAA,EAAW,GAAA,CAAI,GAAG,CAAA,GAAI,MAAA,CAAO,GAAG,CAAA;AAClF,EAAA,OAAO,GAAA;AACT;AAEO,SAAS,OAAA,CAAQ,IAAA,EAAmC,QAAA,GAAW,GAAA,EAAa;AACjF,EAAA,MAAM,IAAI,IAAA,IAAQ,QAAA;AAClB,EAAA,OAAO,OAAO,CAAA,KAAM,QAAA,GAAW,CAAA,EAAG,CAAC,CAAA,EAAA,CAAA,GAAO,CAAA;AAC5C","file":"chunk-3OMN635U.js","sourcesContent":["import type { BadgeOptions } from './types'\n\n/** Every key of BadgeOptions, used by the framework wrappers to split props. */\nexport const optionKeys = [\n 'svg',\n 'mode',\n 'metal',\n 'enamel',\n 'base',\n 'accent',\n 'color',\n 'frame',\n 'rim',\n 'outline',\n 'fillHoles',\n 'relief',\n 'environment',\n 'environmentRotation',\n 'exposure',\n 'toneMapping',\n 'interaction',\n 'tiltMax',\n 'pose',\n 'idle',\n 'locked',\n 'lockedColor',\n 'shadow',\n 'quality',\n 'padding',\n 'onReady',\n 'onError',\n] as const satisfies readonly (keyof BadgeOptions)[]\n\n// Compile-time guard: adding an option without listing it here is a type error.\ntype Missing = Exclude<keyof BadgeOptions, (typeof optionKeys)[number]>\nconst exhaustive: Missing extends never ? true : Missing = true\nvoid exhaustive\n\nexport const optionKeySet: ReadonlySet<string> = new Set(optionKeys)\n\n/** Collect defined option values from an arbitrary props object. */\nexport function pickOptions(source: Record<string, unknown>): BadgeOptions {\n const out: Record<string, unknown> = {}\n for (const key of optionKeys) if (source[key] !== undefined) out[key] = source[key]\n return out as unknown as BadgeOptions\n}\n\nexport function cssSize(size: number | string | undefined, fallback = 160): string {\n const s = size ?? fallback\n return typeof s === 'number' ? `${s}px` : s\n}\n"]}
|