desck 0.4.2
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/.gitlab-ci.yml +21 -0
- package/README.md +176 -0
- package/desck.cjs +2 -0
- package/elm.json +39 -0
- package/justfile +13 -0
- package/package.json +25 -0
- package/public/index.html +30 -0
- package/public/palette.json +16 -0
- package/public/projects/something.json +139 -0
- package/public/projects/template.json +69 -0
- package/public/script.js +490 -0
- package/public/style.css +471 -0
- package/public/svg/backface.svg +1 -0
- package/public/svg/courts.svg +30 -0
- package/public/svg/digits.svg +18 -0
- package/public/svg/suits.svg +43 -0
- package/server.cjs +316 -0
- package/settings.json +10 -0
- package/src/Card.elm +38 -0
- package/src/ColorPicker.elm +113 -0
- package/src/Deck.elm +571 -0
- package/src/DeckView.elm +415 -0
- package/src/Layout.elm +224 -0
- package/src/Main.elm +3743 -0
- package/src/Palette.elm +302 -0
- package/src/Project.elm +1029 -0
- package/src/Rect.elm +292 -0
- package/src/SaveFile.elm +422 -0
- package/src/Theme.elm +99 -0
- package/src/Util.elm +337 -0
- package/src/Vec.elm +196 -0
- package/src/Zone.elm +364 -0
package/public/script.js
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
let socket = null;
|
|
2
|
+
let connected = false;
|
|
3
|
+
|
|
4
|
+
// TODO Files - separate template from files
|
|
5
|
+
|
|
6
|
+
function getInitial() {
|
|
7
|
+
const hash = window.location.hash;
|
|
8
|
+
if (hash.length > 1) {
|
|
9
|
+
return hash.substring(1);
|
|
10
|
+
} else {
|
|
11
|
+
return "template";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function extractHelp(readme) {
|
|
16
|
+
const someUsage = readme.split("## Help");
|
|
17
|
+
const helpDev = someUsage[1].split("\n## ");
|
|
18
|
+
return helpDev[0].trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// function hsl(c) {
|
|
22
|
+
// return `hsl(${c.h * 360}, ${c.s * 100}%, ${c.l * 100}%)`
|
|
23
|
+
// }
|
|
24
|
+
|
|
25
|
+
function paletteAsCSS(palette) {
|
|
26
|
+
const colors = Object.keys(palette);
|
|
27
|
+
const vars = colors
|
|
28
|
+
.map((name) => ` --${name}: ${palette[name]};`)
|
|
29
|
+
.join("\n");
|
|
30
|
+
const classes = colors
|
|
31
|
+
.map((name) => `.${name} {fill: var(--${name}) !important;}`)
|
|
32
|
+
.join("\n");
|
|
33
|
+
|
|
34
|
+
return `:root {\n${vars}\n}\n${classes}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function styleBlock(css) {
|
|
38
|
+
const style = document.createElement("style");
|
|
39
|
+
style.textContent = css;
|
|
40
|
+
return style;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function loadPalette(palette) {
|
|
44
|
+
if (window.paletteStyle) {
|
|
45
|
+
document.head.removeChild(window.paletteStyle);
|
|
46
|
+
}
|
|
47
|
+
window.paletteObject = palette;
|
|
48
|
+
window.paletteStyle = styleBlock(paletteAsCSS(palette));
|
|
49
|
+
document.head.appendChild(window.paletteStyle);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function loadFont(type, font) {
|
|
53
|
+
window.font[type] = font;
|
|
54
|
+
if (window.fontStyle[type]) {
|
|
55
|
+
document.head.removeChild(window.fontStyle[type]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
window.fontStyle[type] = styleBlock(`
|
|
59
|
+
@import url('https://fonts.googleapis.com/css2?family=${font.replaceAll(" ", "+")}&display=swap');
|
|
60
|
+
:root {
|
|
61
|
+
--desck-${type}-font : "${font}";
|
|
62
|
+
}
|
|
63
|
+
`);
|
|
64
|
+
document.head.appendChild(window.fontStyle[type]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function loadFlags() {
|
|
68
|
+
const project =
|
|
69
|
+
window.location.hash.length > 1
|
|
70
|
+
? atob(window.location.hash.substring(1))
|
|
71
|
+
: null;
|
|
72
|
+
|
|
73
|
+
const parsed = JSON.parse(project);
|
|
74
|
+
|
|
75
|
+
console.log(project);
|
|
76
|
+
return {
|
|
77
|
+
offline: window.location.hostname == "localhost" && true,
|
|
78
|
+
seed: new Date().getTime(),
|
|
79
|
+
window: getRect(),
|
|
80
|
+
initial: project ? parsed.name : "template",
|
|
81
|
+
help: extractHelp(await (await fetch("./README.md")).text()),
|
|
82
|
+
saveFile: project,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function loadSymbolsInto(url, parent) {
|
|
87
|
+
fetch(url)
|
|
88
|
+
.then((response) => response.text())
|
|
89
|
+
.then((svgText) => {
|
|
90
|
+
const parser = new DOMParser();
|
|
91
|
+
const doc = parser.parseFromString(svgText, "image/svg+xml");
|
|
92
|
+
const svg = doc.querySelector("defs");
|
|
93
|
+
const symbols = svg.getElementsByTagName("symbol");
|
|
94
|
+
if (svg) {
|
|
95
|
+
for (let symbol of symbols) {
|
|
96
|
+
parent.appendChild(symbol.cloneNode(true));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
.catch((error) => console.error("Error loading SVG:", error));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function loadJsonProject(url) {
|
|
104
|
+
const response = await fetch(url);
|
|
105
|
+
const json = await response.json();
|
|
106
|
+
return json;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function loadProjectFromSVG(url) {
|
|
110
|
+
const response = await fetch(url);
|
|
111
|
+
if (response.ok) {
|
|
112
|
+
const text = await response.text();
|
|
113
|
+
const parser = new DOMParser();
|
|
114
|
+
const doc = parser.parseFromString(text, "image/svg+xml");
|
|
115
|
+
const svg = doc.querySelector("#view");
|
|
116
|
+
const data = svg.getAttribute("data-project");
|
|
117
|
+
const project = JSON.parse(data);
|
|
118
|
+
return project;
|
|
119
|
+
} else {
|
|
120
|
+
return response;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function downloadBlob(blob, filename) {
|
|
125
|
+
const downloadLink = document.createElement("a");
|
|
126
|
+
downloadLink.href = blob;
|
|
127
|
+
downloadLink.download = filename;
|
|
128
|
+
downloadLink.click();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function exportSymbols(settings) {
|
|
132
|
+
const svg = document.querySelector("#view");
|
|
133
|
+
const project = JSON.parse(svg.getAttribute("data-project"));
|
|
134
|
+
|
|
135
|
+
const backface = svg.querySelector("#backface");
|
|
136
|
+
|
|
137
|
+
if (settings.stripBackface) {
|
|
138
|
+
backface.id = null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let data = new XMLSerializer().serializeToString(svg.querySelector("#defs"));
|
|
142
|
+
|
|
143
|
+
if (settings.stripBackface) {
|
|
144
|
+
backface.id = "backface";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const mapColors = settings ? settings.mapColors : project.settings.mapColors;
|
|
148
|
+
|
|
149
|
+
if (mapColors) {
|
|
150
|
+
Object.keys(window.paletteObject).forEach((name) => {
|
|
151
|
+
const color = window.paletteObject[name];
|
|
152
|
+
data = data.replaceAll(`var(--${name})`, color);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
data = `<svg xmls="http://www.w3.org/2000/svg" display="none">${data}</svg>`;
|
|
157
|
+
|
|
158
|
+
const blob = new Blob([data], { type: "image/svg+xml" });
|
|
159
|
+
|
|
160
|
+
console.log(blob);
|
|
161
|
+
|
|
162
|
+
const url = URL.createObjectURL(blob);
|
|
163
|
+
return {
|
|
164
|
+
url: url,
|
|
165
|
+
blob: blob,
|
|
166
|
+
project: project,
|
|
167
|
+
data: data,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function getProject() {
|
|
172
|
+
const svg = document.querySelector("#view");
|
|
173
|
+
return JSON.parse(svg.getAttribute("data-project"));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function exportImage(settingsMaybe) {
|
|
177
|
+
const svg = document.querySelector("#view");
|
|
178
|
+
const project = JSON.parse(svg.getAttribute("data-project"));
|
|
179
|
+
const clone = svg.cloneNode(true);
|
|
180
|
+
const defs = clone.querySelector("defs");
|
|
181
|
+
|
|
182
|
+
const settings = settingsMaybe ? settingsMaybe : project.settings;
|
|
183
|
+
|
|
184
|
+
let models = [];
|
|
185
|
+
clone.querySelectorAll(".card").forEach((c) => {
|
|
186
|
+
models = models.concat(JSON.parse(c.getAttribute("data-using")));
|
|
187
|
+
c.removeAttribute("data-using");
|
|
188
|
+
});
|
|
189
|
+
clone.querySelectorAll(".card-front-rect").forEach((cfr) => {
|
|
190
|
+
cfr.removeAttribute("cursor");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (settings.stripData) {
|
|
194
|
+
clone.removeAttribute("data-project");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
clone.removeAttribute("style");
|
|
198
|
+
|
|
199
|
+
if (settings.embedSymbols) {
|
|
200
|
+
defs.querySelectorAll("symbol").forEach((symbol) => {
|
|
201
|
+
if (!models.includes(symbol.id) && symbol.id != "backface") {
|
|
202
|
+
defs.removeChild(symbol);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
} else {
|
|
206
|
+
clone.removeChild(defs);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
clone.querySelectorAll(".remove-on-export").forEach((node) => {
|
|
210
|
+
node.remove();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// clone.querySelectorAll(".selection").forEach((selection) => {
|
|
214
|
+
// clone.removeChild(selection)
|
|
215
|
+
// })
|
|
216
|
+
|
|
217
|
+
// if (project.project.background == "transparent") {
|
|
218
|
+
// clone.querySelectorAll("#background").forEach((selection) => {
|
|
219
|
+
// clone.removeChild(selection)
|
|
220
|
+
// })
|
|
221
|
+
// }
|
|
222
|
+
|
|
223
|
+
let data = new XMLSerializer().serializeToString(clone);
|
|
224
|
+
if (settings.mapColors) {
|
|
225
|
+
Object.keys(window.paletteObject).forEach((name) => {
|
|
226
|
+
const color = window.paletteObject[name];
|
|
227
|
+
|
|
228
|
+
data = data.replaceAll(`var(--${name})`, color);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (settings.mapFont) {
|
|
233
|
+
["primary", "secondary"].map((type) => {
|
|
234
|
+
if (window.fontStyle[type]) {
|
|
235
|
+
clone.appendChild(window.fontStyle[type].cloneNode(true));
|
|
236
|
+
data.replace(`var(--desck-${type}-font)`, `"${window.font[type]}"`);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
// TODO Export - convert text object to path with opentype.js
|
|
241
|
+
|
|
242
|
+
const blob = new Blob([data], { type: "image/svg+xml" });
|
|
243
|
+
|
|
244
|
+
const url = URL.createObjectURL(blob);
|
|
245
|
+
return {
|
|
246
|
+
url: url,
|
|
247
|
+
blob: blob,
|
|
248
|
+
project: project,
|
|
249
|
+
data: data,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function exportPNG(settings) {
|
|
254
|
+
const heightPixels = settings ? settings.heightPixels : 100;
|
|
255
|
+
|
|
256
|
+
const { url, project } = exportImage();
|
|
257
|
+
|
|
258
|
+
const canvas = document.createElement("canvas");
|
|
259
|
+
|
|
260
|
+
const cardSize = {
|
|
261
|
+
x: 250,
|
|
262
|
+
y: 350,
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const widthPixels = (cardSize.x / cardSize.y) * heightPixels;
|
|
266
|
+
canvas.width = Math.floor(
|
|
267
|
+
(project.project.viewport.size.x / cardSize.x) * widthPixels,
|
|
268
|
+
);
|
|
269
|
+
canvas.height = Math.floor(
|
|
270
|
+
(project.project.viewport.size.y / cardSize.y) * heightPixels,
|
|
271
|
+
);
|
|
272
|
+
const ctx = canvas.getContext("2d");
|
|
273
|
+
|
|
274
|
+
const img = new Image();
|
|
275
|
+
|
|
276
|
+
return new Promise((resolve) => {
|
|
277
|
+
img.onload = () => {
|
|
278
|
+
ctx.drawImage(img, 0, 0);
|
|
279
|
+
URL.revokeObjectURL(url);
|
|
280
|
+
const pngUrl = canvas.toDataURL("image/png");
|
|
281
|
+
resolve(pngUrl);
|
|
282
|
+
};
|
|
283
|
+
img.src = url;
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function exportSVG() {
|
|
288
|
+
const { url } = exportImage();
|
|
289
|
+
return url;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
let app = null;
|
|
293
|
+
|
|
294
|
+
async function handleOutputs(kind, data) {
|
|
295
|
+
switch (kind) {
|
|
296
|
+
case "export-svg": {
|
|
297
|
+
const svg = exportSVG();
|
|
298
|
+
downloadBlob(svg, `${data}.svg`);
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
case "export-symbols": {
|
|
302
|
+
const { url } = exportSymbols();
|
|
303
|
+
downloadBlob(url, `symbols.svg`);
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
case "export-png": {
|
|
307
|
+
const pixels = getProject().settings.heightPixels;
|
|
308
|
+
const png = await exportPNG(pixels);
|
|
309
|
+
downloadBlob(png, `${data}.png`);
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
case "export-json": {
|
|
313
|
+
const project = JSON.parse(data);
|
|
314
|
+
const blob = new Blob([data], { type: "application/json" });
|
|
315
|
+
const url = URL.createObjectURL(blob);
|
|
316
|
+
downloadBlob(url, `${project.name}.json`);
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
case "load-palette": {
|
|
320
|
+
const palette = JSON.parse(data);
|
|
321
|
+
loadPalette(palette);
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
case "load-primary-font": {
|
|
325
|
+
loadFont("primary", data);
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
case "load-secondary-font": {
|
|
329
|
+
loadFont("secondary", data);
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
case "get-size": {
|
|
333
|
+
app.ports.resizeMessage.send(getRect());
|
|
334
|
+
}
|
|
335
|
+
default:
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function getRect() {
|
|
341
|
+
const view = document.querySelector("#view");
|
|
342
|
+
return view
|
|
343
|
+
? {
|
|
344
|
+
x: view.getBoundingClientRect().width,
|
|
345
|
+
y: view.getBoundingClientRect().height,
|
|
346
|
+
}
|
|
347
|
+
: { x: window.innerWidth, y: window.innerHeight };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function init() {
|
|
351
|
+
let template = await (await fetch("./projects/template.json")).json();
|
|
352
|
+
|
|
353
|
+
let flags = await loadFlags();
|
|
354
|
+
|
|
355
|
+
if (flags.offline) {
|
|
356
|
+
app = Elm.Main.init({
|
|
357
|
+
node: document.querySelector("#main"),
|
|
358
|
+
flags: flags,
|
|
359
|
+
});
|
|
360
|
+
const sendMessage = (kind, data) => {
|
|
361
|
+
app.ports.input.send({
|
|
362
|
+
kind: kind,
|
|
363
|
+
data: data,
|
|
364
|
+
});
|
|
365
|
+
};
|
|
366
|
+
socket = window.io();
|
|
367
|
+
socket.on("connect", () => {
|
|
368
|
+
if (connected) {
|
|
369
|
+
window.location.reload();
|
|
370
|
+
} else {
|
|
371
|
+
connected = true;
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
socket.on("live-reload", () => {
|
|
375
|
+
window.location.reload();
|
|
376
|
+
});
|
|
377
|
+
socket.onAny((...args) => {
|
|
378
|
+
console.log(args[0], args[1]);
|
|
379
|
+
sendMessage(args[0], args[1] ? JSON.stringify(args[1]) : "");
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
app.ports.output.subscribe(async (message) => {
|
|
383
|
+
let { kind, data } = message;
|
|
384
|
+
console.log(kind, data);
|
|
385
|
+
let exported = await handleOutputs(kind, data);
|
|
386
|
+
if (!exported) {
|
|
387
|
+
socket.emit(kind, data);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
} else {
|
|
391
|
+
const last = window.localStorage.getItem("__last");
|
|
392
|
+
const first =
|
|
393
|
+
last && window.localStorage.getItem(last)
|
|
394
|
+
? window.localStorage.getItem(last)
|
|
395
|
+
: JSON.stringify(template);
|
|
396
|
+
flags.initial = last && first ? last : "template";
|
|
397
|
+
app = Elm.Main.init({
|
|
398
|
+
node: document.querySelector("#main"),
|
|
399
|
+
flags: flags,
|
|
400
|
+
});
|
|
401
|
+
const sendMessage = (kind, data) => {
|
|
402
|
+
app.ports.input.send({
|
|
403
|
+
kind: kind,
|
|
404
|
+
data: data,
|
|
405
|
+
});
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const sendFiles = () => {
|
|
409
|
+
sendMessage(
|
|
410
|
+
"files",
|
|
411
|
+
JSON.stringify(
|
|
412
|
+
Object.keys(window.localStorage).filter(
|
|
413
|
+
(n) => n.substring(0, 2) != "__",
|
|
414
|
+
),
|
|
415
|
+
),
|
|
416
|
+
);
|
|
417
|
+
};
|
|
418
|
+
window.localStorage.setItem("template", JSON.stringify(template));
|
|
419
|
+
|
|
420
|
+
sendFiles();
|
|
421
|
+
sendMessage("open", first);
|
|
422
|
+
|
|
423
|
+
app.ports.output.subscribe(async (message) => {
|
|
424
|
+
let { kind, data } = message;
|
|
425
|
+
console.log(kind, data);
|
|
426
|
+
let exported = await handleOutputs(kind, data);
|
|
427
|
+
if (!exported) {
|
|
428
|
+
switch (kind) {
|
|
429
|
+
case "save": {
|
|
430
|
+
window.localStorage.setItem(data.name, JSON.stringify(data));
|
|
431
|
+
window.localStorage.setItem("__last", data.name);
|
|
432
|
+
sendMessage("saved", data.name);
|
|
433
|
+
sendFiles();
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
case "import": {
|
|
437
|
+
const saved = window.localStorage.getItem(data);
|
|
438
|
+
window.localStorage.setItem("__last", data);
|
|
439
|
+
if (saved) {
|
|
440
|
+
sendMessage("open", saved);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
let symbol_container = "#defs";
|
|
449
|
+
loadSymbolsInto("./svg/digits.svg", document.querySelector(symbol_container));
|
|
450
|
+
loadSymbolsInto("./svg/suits.svg", document.querySelector(symbol_container));
|
|
451
|
+
loadSymbolsInto("./svg/courts.svg", document.querySelector(symbol_container));
|
|
452
|
+
// loadSymbolsInto(
|
|
453
|
+
// "./svg/backface.svg",
|
|
454
|
+
// document.querySelector(symbol_container),
|
|
455
|
+
// );
|
|
456
|
+
|
|
457
|
+
let mods = {
|
|
458
|
+
ctrl: false,
|
|
459
|
+
shift: false,
|
|
460
|
+
alt: false,
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
const forwardMods = (event) => {
|
|
464
|
+
mods.ctrl = event.ctrlKey;
|
|
465
|
+
mods.shift = event.shiftKey;
|
|
466
|
+
mods.alt = event.altKey;
|
|
467
|
+
app.ports.input.send({ kind: "mods", data: JSON.stringify(mods) });
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
window.addEventListener("keydown", (event) => {
|
|
471
|
+
forwardMods(event);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
window.addEventListener("keyup", (event) => {
|
|
475
|
+
forwardMods(event);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
window.font = {};
|
|
479
|
+
window.fontStyle = {};
|
|
480
|
+
// window.addEventListener("hashchange", () => window.location.reload())
|
|
481
|
+
|
|
482
|
+
window.addEventListener("context", (e) => {
|
|
483
|
+
e.preventDefault();
|
|
484
|
+
});
|
|
485
|
+
window.addEventListener("resize", () => {
|
|
486
|
+
app.ports.resizeMessage.send(getRect());
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
init();
|