@matboks/template-core 0.0.1

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.
@@ -0,0 +1 @@
1
+ export * from "./template.js";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export * from "./template.js";
@@ -0,0 +1,66 @@
1
+ import { Resolution } from "@matboks/utilities";
2
+ import { MakeOptionalRequiredIfIn } from "./types.js";
3
+ import { Context } from "./utilities/context.js";
4
+ type TemplateOptions = {
5
+ projectName: string;
6
+ aspectRatio?: number;
7
+ resolution?: number;
8
+ container?: HTMLElement;
9
+ animate?: boolean;
10
+ numFrames?: number;
11
+ fps?: number;
12
+ numLoops?: number;
13
+ enableSVGExport?: boolean;
14
+ seed?: number;
15
+ };
16
+ type DrawFunction = (data: Context) => void;
17
+ type SetupFunction = (data: Context) => DrawFunction | void;
18
+ declare const defaultOptions: {
19
+ aspectRatio: number;
20
+ container: HTMLElement;
21
+ animate: boolean;
22
+ fps: number;
23
+ numLoops: number;
24
+ enableSVGExport: true;
25
+ seed: number;
26
+ };
27
+ type OptionsWithDefaults = MakeOptionalRequiredIfIn<TemplateOptions, typeof defaultOptions>;
28
+ export declare class Template {
29
+ options: OptionsWithDefaults;
30
+ canvas: HTMLCanvasElement;
31
+ rawCtx: CanvasRenderingContext2D;
32
+ get ctx(): CanvasRenderingContext2D;
33
+ setup?: SetupFunction;
34
+ draw?: DrawFunction;
35
+ private isSetup;
36
+ private isAnimating;
37
+ animationFrame: number;
38
+ realTime: number;
39
+ private previousTime;
40
+ resolution: Resolution;
41
+ seed: number;
42
+ isDebug: boolean;
43
+ rafId: number;
44
+ private templateContext;
45
+ private svgHandler?;
46
+ constructor(options: TemplateOptions);
47
+ static create(options: TemplateOptions): Template;
48
+ updateDocumentTitle(): void;
49
+ private getFrameInfo;
50
+ setupSVG(): void;
51
+ setOptionsToInitial(): void;
52
+ initialize(): void;
53
+ render(): void;
54
+ handleSetup(): void;
55
+ drawFrame(): void;
56
+ startAnimation(): void;
57
+ stopAnimation(): void;
58
+ finishAnimation(): void;
59
+ restart(): void;
60
+ setupResizeListener(): void;
61
+ resizeCanvas(): void;
62
+ private syncCanvasInternalResolution;
63
+ setupKeyListeners(): void;
64
+ getSVG(): string;
65
+ }
66
+ export {};
@@ -0,0 +1,260 @@
1
+ import { Resolution } from "@matboks/utilities";
2
+ import { searchParams } from "./utilities/search-params.js";
3
+ import { getMaximumAvailableResolution, savePNG, saveSVG, openCanvasInNewTab, createHTMLElements, assignDefined, setTheme, debounce } from "./utilities/helpers.js";
4
+ import { context } from "./utilities/context.js";
5
+ import { createSVGHandler } from "./utilities/svg.js";
6
+ const defaultOptions = {
7
+ aspectRatio: 1,
8
+ container: document.body,
9
+ animate: false,
10
+ fps: 60,
11
+ numLoops: 1,
12
+ enableSVGExport: true,
13
+ seed: 0,
14
+ };
15
+ export class Template {
16
+ get ctx() { return this.options.enableSVGExport ? this.svgHandler.ctx2DProxy : this.rawCtx; }
17
+ constructor(options) {
18
+ this.isSetup = false;
19
+ this.isAnimating = false;
20
+ this.animationFrame = 0;
21
+ this.realTime = 0;
22
+ this.previousTime = null;
23
+ this.rafId = -1;
24
+ const parsedOptions = assignDefined(defaultOptions, options, {
25
+ seed: searchParams.get("seed", parseInt),
26
+ animate: searchParams.get("animate", (v) => v === "true"),
27
+ resolution: searchParams.get("resolution", parseInt)
28
+ });
29
+ this.options = parsedOptions;
30
+ this.isDebug = searchParams.get("debug", (v) => v === "true") ?? false;
31
+ this.updateDocumentTitle();
32
+ }
33
+ static create(options) {
34
+ const template = new Template(options);
35
+ template.initialize();
36
+ return template;
37
+ }
38
+ updateDocumentTitle() {
39
+ const { projectName } = this.options;
40
+ if (this.isAnimating) {
41
+ document.title = `${this.getFrameInfo()} - ${this.options.projectName}`;
42
+ }
43
+ else {
44
+ document.title = projectName;
45
+ }
46
+ }
47
+ getFrameInfo() {
48
+ const { animationFrame, options: { numFrames, numLoops } } = this;
49
+ if (numFrames) {
50
+ if (numLoops > 1) {
51
+ const currentLoop = Math.floor(animationFrame / (numFrames + 1)) + 1;
52
+ const currentFrame = (animationFrame + 1) % (numFrames + 1);
53
+ return `#${currentLoop} ${currentFrame} / ${numFrames}`;
54
+ }
55
+ else {
56
+ return `${animationFrame} / ${numFrames}`;
57
+ }
58
+ }
59
+ else {
60
+ return animationFrame.toString();
61
+ }
62
+ }
63
+ setupSVG() {
64
+ const svgHandler = createSVGHandler(this.rawCtx);
65
+ this.svgHandler = svgHandler;
66
+ }
67
+ setOptionsToInitial() {
68
+ const { seed, animate } = this.options;
69
+ this.seed = seed;
70
+ this.isAnimating = animate;
71
+ }
72
+ initialize() {
73
+ this.setOptionsToInitial();
74
+ const { container } = this.options;
75
+ const { root, canvas } = createHTMLElements();
76
+ setTheme(container);
77
+ container.append(root);
78
+ this.canvas = canvas;
79
+ this.rawCtx = this.canvas.getContext("2d");
80
+ // TODO Resize listener triggers initial render. Is this intuitive?
81
+ this.setupResizeListener();
82
+ this.setupKeyListeners();
83
+ }
84
+ render() {
85
+ // debugger
86
+ if (this.options.animate) {
87
+ this.startAnimation();
88
+ }
89
+ else {
90
+ requestAnimationFrame(() => this.drawFrame());
91
+ }
92
+ }
93
+ handleSetup() {
94
+ if (this.isSetup)
95
+ return;
96
+ this.rawCtx.reset();
97
+ if (this.options.enableSVGExport)
98
+ this.setupSVG();
99
+ this.templateContext = context(this);
100
+ const drawFunction = this.setup?.(this.templateContext);
101
+ this.isSetup = true;
102
+ if (drawFunction)
103
+ this.draw = drawFunction;
104
+ }
105
+ drawFrame() {
106
+ this.handleSetup();
107
+ const { previousTime, isAnimating } = this;
108
+ const currentTime = performance.now() / 1000;
109
+ if (isAnimating && previousTime && currentTime - previousTime < 1 / this.options.fps) {
110
+ this.rafId = requestAnimationFrame(() => this.drawFrame());
111
+ return;
112
+ }
113
+ if (previousTime)
114
+ this.realTime += currentTime - previousTime;
115
+ this.previousTime = currentTime;
116
+ this.draw?.(this.templateContext);
117
+ if (this.isAnimating) {
118
+ const { numFrames, numLoops } = this.options;
119
+ this.animationFrame++;
120
+ this.updateDocumentTitle();
121
+ if (numFrames && this.animationFrame === numFrames * numLoops) {
122
+ this.finishAnimation();
123
+ }
124
+ else {
125
+ this.rafId = requestAnimationFrame(() => this.drawFrame());
126
+ }
127
+ }
128
+ else {
129
+ document.complete = true;
130
+ }
131
+ }
132
+ startAnimation() {
133
+ this.rafId = requestAnimationFrame(() => this.drawFrame());
134
+ this.isAnimating = true;
135
+ }
136
+ stopAnimation() {
137
+ cancelAnimationFrame(this.rafId);
138
+ this.isAnimating = false;
139
+ }
140
+ finishAnimation() {
141
+ this.stopAnimation();
142
+ document.complete = true;
143
+ }
144
+ restart() {
145
+ this.stopAnimation();
146
+ this.isSetup = false;
147
+ this.animationFrame = 0;
148
+ this.realTime = 0;
149
+ this.previousTime = null;
150
+ this.render();
151
+ }
152
+ setupResizeListener() {
153
+ const { canvas } = this;
154
+ const containerResizeObserver = new ResizeObserver(debounce(() => this.resizeCanvas(), 100));
155
+ containerResizeObserver.observe(canvas.parentElement.parentElement);
156
+ this.resizeCanvas();
157
+ }
158
+ resizeCanvas() {
159
+ const { canvas, options } = this;
160
+ const frame = canvas.parentElement;
161
+ const { clientWidth, clientHeight } = frame.parentElement;
162
+ const framePadding = parseInt(getComputedStyle(frame).padding);
163
+ const { width, height } = getMaximumAvailableResolution(new Resolution(clientWidth - 2 * framePadding, clientHeight - 2 * framePadding), options.aspectRatio);
164
+ if (canvas.clientWidth === width && canvas.clientHeight === height)
165
+ return;
166
+ canvas.style.width = `${width}px`;
167
+ canvas.style.height = `${height}px`;
168
+ this.syncCanvasInternalResolution();
169
+ }
170
+ syncCanvasInternalResolution() {
171
+ const { canvas, options } = this;
172
+ let width, height;
173
+ if (!options.resolution) {
174
+ ({ width, height } = canvas.getBoundingClientRect());
175
+ width = Math.round(devicePixelRatio * width);
176
+ height = Math.round(devicePixelRatio * height);
177
+ }
178
+ else {
179
+ const { resolution, aspectRatio } = options;
180
+ [width, height] = aspectRatio > 1 ?
181
+ [resolution, resolution / aspectRatio] :
182
+ [aspectRatio * resolution, resolution];
183
+ }
184
+ canvas.width = width;
185
+ canvas.height = height;
186
+ this.resolution = new Resolution(width, height);
187
+ this.restart();
188
+ }
189
+ setupKeyListeners() {
190
+ const { canvas, seed, options } = this;
191
+ const getSeed = () => searchParams.get("seed", parseInt) ?? 0;
192
+ // TODO make syncronisation of values more robust (options and searchParams)
193
+ window.addEventListener("keydown", (event) => {
194
+ switch (event.key) {
195
+ case "a": {
196
+ const animate = this.options.animate = searchParams.toggle("animate");
197
+ if (animate) {
198
+ this.startAnimation();
199
+ }
200
+ else {
201
+ this.stopAnimation();
202
+ }
203
+ break;
204
+ }
205
+ case "n": {
206
+ this.seed = searchParams.set("seed", getSeed() - 1);
207
+ this.restart();
208
+ break;
209
+ }
210
+ case "m": {
211
+ this.seed = searchParams.set("seed", getSeed() + 1);
212
+ this.restart();
213
+ break;
214
+ }
215
+ case "d": {
216
+ this.isDebug = searchParams.toggle("debug");
217
+ break;
218
+ }
219
+ case "t": {
220
+ // Darkmode
221
+ const value = localStorage.getItem("theme");
222
+ if (!value || value === "light") {
223
+ this.options.container.classList.add("dark-template");
224
+ localStorage.setItem("theme", "dark");
225
+ }
226
+ else {
227
+ this.options.container.classList.remove("dark-template");
228
+ localStorage.setItem("theme", "light");
229
+ }
230
+ break;
231
+ }
232
+ case "c": {
233
+ this.setOptionsToInitial();
234
+ searchParams.clearAll();
235
+ this.restart();
236
+ break;
237
+ }
238
+ case "s": {
239
+ const filename = [options.projectName, seed, options.resolution].filter((v) => v).join("-");
240
+ savePNG(filename, this);
241
+ break;
242
+ }
243
+ case "v": {
244
+ const filename = [options.projectName, seed].filter((v) => v).join("-");
245
+ saveSVG(filename, this);
246
+ break;
247
+ }
248
+ case "o": {
249
+ openCanvasInNewTab(canvas);
250
+ break;
251
+ }
252
+ }
253
+ });
254
+ }
255
+ getSVG() {
256
+ if (!this.svgHandler)
257
+ throw new Error("SVG is not enabled");
258
+ return this.svgHandler.getSVG();
259
+ }
260
+ }
@@ -0,0 +1,5 @@
1
+ export type MakeOptionalRequiredIfIn<A, B> = Required<Pick<A, Extract<keyof A, keyof B> & {
2
+ [K in keyof A]-?: {} extends Pick<A, K> ? K : never;
3
+ }[keyof A]>> & Omit<A, {
4
+ [K in keyof A]-?: {} extends Pick<A, K> ? (K extends keyof B ? K : never) : never;
5
+ }[keyof A]>;
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ import { Template } from "../template.js";
2
+ export declare function context(template: Template): {
3
+ template: Template;
4
+ ctx: CanvasRenderingContext2D;
5
+ clear: (backgroundStyle?: string) => void;
6
+ isolate: (fn: () => void) => void;
7
+ stopAnimation: () => void;
8
+ stopWhen: (condition: boolean) => void;
9
+ readonly frameTime: number;
10
+ readonly realTime: number;
11
+ readonly progress: number;
12
+ readonly frame: number;
13
+ readonly resolution: import("@matboks/utilities").Resolution;
14
+ readonly seed: number;
15
+ readonly isDebug: boolean;
16
+ };
17
+ export type Context = ReturnType<typeof context>;
@@ -0,0 +1,49 @@
1
+ import { mod } from "@matboks/utilities";
2
+ // TODO differentiate between frameTime (frame / fps) and progressionTime (frame / numFrames)
3
+ export function context(template) {
4
+ const { ctx, options } = template;
5
+ const isolate = (fn) => {
6
+ ctx.save();
7
+ fn();
8
+ ctx.restore();
9
+ };
10
+ const clear = (backgroundStyle) => {
11
+ const { width, height } = template.resolution;
12
+ isolate(() => {
13
+ ctx.resetTransform();
14
+ if (backgroundStyle) {
15
+ ctx.fillStyle = backgroundStyle;
16
+ ctx.fillRect(0, 0, width, height);
17
+ }
18
+ else {
19
+ ctx.clearRect(0, 0, width, height);
20
+ }
21
+ });
22
+ };
23
+ const stopAnimation = () => {
24
+ template.finishAnimation();
25
+ };
26
+ const stopWhen = (condition) => {
27
+ if (condition)
28
+ stopAnimation();
29
+ };
30
+ return {
31
+ template, ctx,
32
+ clear, isolate,
33
+ stopAnimation, stopWhen,
34
+ get frameTime() {
35
+ return template.animationFrame / options.fps;
36
+ },
37
+ get realTime() {
38
+ return template.realTime;
39
+ },
40
+ get progress() {
41
+ const { numFrames } = options;
42
+ return numFrames ? mod(template.animationFrame, numFrames) / numFrames : 0;
43
+ },
44
+ get frame() { return template.animationFrame; },
45
+ get resolution() { return template.resolution; },
46
+ get seed() { return template.seed; },
47
+ get isDebug() { return template.isDebug; }
48
+ };
49
+ }
@@ -0,0 +1,18 @@
1
+ import { Resolution } from "@matboks/utilities";
2
+ import { Template } from "../template.js";
3
+ export declare function getMaximumAvailableResolution(availableSpace: Resolution, aspectRatio: number): Resolution;
4
+ export declare function savePNG(filename: string, template: Template): void;
5
+ export declare function saveSVG(filename: string, template: Template): void;
6
+ export declare function openCanvasInNewTab(canvas: HTMLCanvasElement): void;
7
+ export declare function createHTMLElements(): {
8
+ root: HTMLDivElement;
9
+ canvas: HTMLCanvasElement;
10
+ };
11
+ type NonNullish<T> = Exclude<T, null | undefined>;
12
+ type MergeDefined<T extends Record<PropertyKey, any>[], R = {}> = T extends [infer F, ...infer Rest] ? MergeDefined<Rest extends Record<PropertyKey, any>[] ? Rest : [], {
13
+ [K in keyof R | keyof F]: K extends keyof F ? NonNullish<F[K]> extends never ? R extends Record<K, any> ? R[K] : F[K] : NonNullish<F[K]> : R extends Record<K, any> ? R[K] : never;
14
+ }> : R;
15
+ export declare function assignDefined<const T extends Record<string, any>[]>(...objects: T): MergeDefined<T>;
16
+ export declare function setTheme(container: HTMLElement): void;
17
+ export declare function debounce(fn: () => void, ms: number): () => void;
18
+ export {};
@@ -0,0 +1,129 @@
1
+ import { Resolution } from "@matboks/utilities";
2
+ export function getMaximumAvailableResolution(availableSpace, aspectRatio) {
3
+ const { width: availableWidth, height: availableHeight } = availableSpace;
4
+ const [width, height] = (aspectRatio * availableHeight > availableWidth ?
5
+ [availableWidth, availableWidth / aspectRatio] :
6
+ [aspectRatio * availableHeight, availableHeight]).map(Math.round);
7
+ return new Resolution(width, height);
8
+ }
9
+ export function savePNG(filename, template) {
10
+ const link = document.createElement("a");
11
+ link.setAttribute('download', `${filename}.png`);
12
+ const data = template.canvas.toDataURL("image/png").replace("image/png", "image/octet-stream");
13
+ ;
14
+ link.setAttribute('href', data);
15
+ link.click();
16
+ link.remove();
17
+ }
18
+ export function saveSVG(filename, template) {
19
+ const link = document.createElement("a");
20
+ link.setAttribute('download', `${filename}.svg`);
21
+ const data = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(template.getSVG());
22
+ link.setAttribute('href', data);
23
+ link.click();
24
+ link.remove();
25
+ }
26
+ export function openCanvasInNewTab(canvas) {
27
+ const data = canvas.toDataURL("png");
28
+ const img = new Image();
29
+ img.src = data;
30
+ const newWindow = window.open();
31
+ img.style.maxHeight = "100%";
32
+ newWindow.document.body.style.margin = "0";
33
+ newWindow.document.body.innerHTML = img.outerHTML;
34
+ }
35
+ export function createHTMLElements() {
36
+ const cssId = "_" + crypto.randomUUID().slice(0, 8);
37
+ const template = document.createElement("template");
38
+ template.innerHTML = `
39
+ <div class="template ${cssId}">
40
+ <div class="container ${cssId}">
41
+ <div class="frame ${cssId}">
42
+ <canvas class="canvas ${cssId}"></canvas>
43
+ </div>
44
+ </div>
45
+ </div>
46
+ `;
47
+ const styleElement = document.createElement("style");
48
+ styleElement.textContent = `
49
+
50
+
51
+ .template.${cssId} {
52
+ box-sizing: border-box;
53
+ width: 100dvw;
54
+ height: 100dvh;
55
+
56
+ background-color: var(--background-color);
57
+ padding-inline: 5dvw;
58
+ padding-block: 5dvh;
59
+
60
+ display: flex;
61
+ justify-content: center;
62
+ align-items: center;
63
+ }
64
+
65
+ :root {
66
+ --background-color: #fafafa;
67
+ }
68
+
69
+ .dark-template {
70
+ --background-color: #1a191e;
71
+ }
72
+
73
+ .container.${cssId} {
74
+ width: 100%;
75
+ height: 100%;
76
+
77
+ display: flex;
78
+ justify-content: center;
79
+ align-items: center;
80
+ }
81
+
82
+ .frame.${cssId} {
83
+ display: flex;
84
+ box-shadow: 0 0 15px rgba(0, 0, 0, 0.15);
85
+
86
+ padding: 10px;
87
+
88
+ background-color: white;
89
+ }
90
+
91
+ .canvas.${cssId} {
92
+ outline: 1px solid rgba(0, 0, 0, 0.07);
93
+ }
94
+ `;
95
+ document.head.appendChild(styleElement);
96
+ const root = template.content.children[0];
97
+ const canvas = root.querySelector("canvas");
98
+ return { root, canvas };
99
+ }
100
+ // Like Object.assign, but does not overwrite defined values with null or undefined
101
+ export function assignDefined(...objects) {
102
+ const result = {};
103
+ for (const obj of objects) {
104
+ for (const key in obj) {
105
+ const value = obj[key];
106
+ if (value !== null && value !== undefined) {
107
+ result[key] = value;
108
+ }
109
+ else if (!(key in result)) {
110
+ // Keep initial null/undefined only if no earlier non-nullish existed
111
+ result[key] = value;
112
+ }
113
+ }
114
+ }
115
+ return result;
116
+ }
117
+ export function setTheme(container) {
118
+ if (localStorage.getItem("theme") === "dark") {
119
+ container.classList.add("dark-template");
120
+ }
121
+ }
122
+ export function debounce(fn, ms) {
123
+ let timeoutId = null;
124
+ return function () {
125
+ if (timeoutId !== null)
126
+ clearTimeout(timeoutId);
127
+ timeoutId = window.setTimeout(fn, ms);
128
+ };
129
+ }
@@ -0,0 +1,10 @@
1
+ export declare class SearchParams {
2
+ url: URL;
3
+ get(name: string): string | null;
4
+ get<R>(name: string, parser: (value: string) => R): R | null;
5
+ clearAll(): void;
6
+ pushToHistory(): void;
7
+ toggle(name: string): boolean;
8
+ set<T>(name: string, value: T): T;
9
+ }
10
+ export declare const searchParams: SearchParams;
@@ -0,0 +1,48 @@
1
+ export class SearchParams {
2
+ constructor() {
3
+ this.url = new URL(window.location.toString());
4
+ }
5
+ get(name, parser) {
6
+ const value = this.url.searchParams.get(name);
7
+ if (!value)
8
+ return null;
9
+ if (parser) {
10
+ return parser(value);
11
+ }
12
+ return value;
13
+ }
14
+ clearAll() {
15
+ this.url.search = "";
16
+ this.pushToHistory();
17
+ }
18
+ pushToHistory() {
19
+ const path = this.url.toString();
20
+ window.history.pushState({ path }, "", path);
21
+ }
22
+ toggle(name) {
23
+ const { searchParams } = this.url;
24
+ let state;
25
+ if (searchParams.get(name) === "true") {
26
+ searchParams.delete(name);
27
+ state = false;
28
+ }
29
+ else {
30
+ searchParams.set(name, "true");
31
+ state = true;
32
+ }
33
+ this.pushToHistory();
34
+ return state;
35
+ }
36
+ set(name, value) {
37
+ const { searchParams } = this.url;
38
+ if (value === null || value === undefined) {
39
+ searchParams.delete(name);
40
+ }
41
+ else {
42
+ searchParams.set(name, value.toString());
43
+ }
44
+ this.pushToHistory();
45
+ return value;
46
+ }
47
+ }
48
+ export const searchParams = new SearchParams();
@@ -0,0 +1,13 @@
1
+ import { Context as SVGContext } from "svgcanvas";
2
+ type SVGCommand = (svgCtx: SVGContext) => void;
3
+ export declare class SVGHandler {
4
+ svgCtx: SVGContext;
5
+ ctx2DProxy: CanvasRenderingContext2D;
6
+ drawCommands: Array<SVGCommand>;
7
+ constructor(ctx: CanvasRenderingContext2D);
8
+ pushCommand(command: SVGCommand): void;
9
+ private applyCommands;
10
+ getSVG(): string;
11
+ }
12
+ export declare function createSVGHandler(ctx: CanvasRenderingContext2D): SVGHandler;
13
+ export {};
@@ -0,0 +1,77 @@
1
+ // @ts-ignore
2
+ import { Context as SVGContext } from "svgcanvas";
3
+ export class SVGHandler {
4
+ constructor(ctx) {
5
+ this.drawCommands = [];
6
+ const { width, height } = ctx.canvas;
7
+ this.svgCtx = new SVGContext({ ctx, width, height });
8
+ }
9
+ pushCommand(command) {
10
+ this.drawCommands.push(command);
11
+ }
12
+ applyCommands() {
13
+ for (const command of this.drawCommands)
14
+ command(this.svgCtx);
15
+ this.drawCommands = [];
16
+ }
17
+ getSVG() {
18
+ this.applyCommands();
19
+ const { svgCtx } = this;
20
+ const svg = svgCtx.getSvg();
21
+ const scale = this.svgCtx.getTransform().a;
22
+ // Fix dash array not being scaled by transform. Note: lineDashOffset is not handled
23
+ for (const element of svg.querySelectorAll("[stroke-dasharray]")) {
24
+ const newValues = element.getAttribute("stroke-dasharray").split(",").map((v) => scale * parseFloat(v));
25
+ element.setAttribute("stroke-dasharray", newValues.join(","));
26
+ }
27
+ return svgCtx.getSerializedSvg();
28
+ }
29
+ }
30
+ export function createSVGHandler(ctx) {
31
+ const svgHandler = new SVGHandler(ctx);
32
+ svgHandler.ctx2DProxy = new Proxy(ctx, {
33
+ get(target, prop, receiver) {
34
+ // First retrieve the underlying value from the real canvas context
35
+ const value = Reflect.get(target, prop, receiver);
36
+ // If it's a function, wrap it so both contexts are called
37
+ if (typeof value === "function") {
38
+ return function (...args) {
39
+ // Call original Canvas 2D method
40
+ const result = value.apply(target, args);
41
+ // Call SVG counterpart ONLY if it exists and is a function
42
+ svgHandler.pushCommand((svgContext) => {
43
+ const svgFn = svgContext[prop];
44
+ if (typeof svgFn === "function") {
45
+ svgFn.apply(svgContext, args);
46
+ }
47
+ });
48
+ return result;
49
+ };
50
+ }
51
+ // Otherwise: plain property -> return the real property value
52
+ return value;
53
+ },
54
+ set(target, prop, newValue) {
55
+ // First update the original canvas context
56
+ target[prop] = newValue;
57
+ // Mirror to SVG context if that property exists
58
+ svgHandler.pushCommand((svgContext) => {
59
+ if (prop in svgContext) {
60
+ try {
61
+ // Some SVG context properties may be read-only -> guard with try
62
+ svgContext[prop] = newValue;
63
+ }
64
+ catch {
65
+ // silently ignore if readonly
66
+ }
67
+ }
68
+ });
69
+ return true;
70
+ },
71
+ has(target, prop) {
72
+ // Behave like a normal CanvasRenderingContext2D
73
+ return prop in target;
74
+ }
75
+ });
76
+ return svgHandler;
77
+ }
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@matboks/template-core",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "files": [
6
+ "dist"
7
+ ],
8
+ "main": "./dist/index.js",
9
+ "devDependencies": {
10
+ "typescript": "~5.6.2",
11
+ "vite": "^6.0.1"
12
+ },
13
+ "dependencies": {
14
+ "svgcanvas": "^2.6.0",
15
+ "@matboks/utilities": "0.0.1"
16
+ },
17
+ "scripts": {
18
+ "build": "tsc"
19
+ }
20
+ }