Fossil SCM

fossil-scm / src / fossil.page.pikchrshowasm.js
Blame History Raw 744 lines
1
/*
2
2022-05-20
3
4
The author disclaims copyright to this source code. In place of a
5
legal notice, here is a blessing:
6
7
* May you do good and not evil.
8
* May you find forgiveness for yourself and forgive others.
9
* May you share freely, never taking more than you give.
10
11
***********************************************************************
12
13
This is the main entry point for the WASM rendition of fossil's
14
/pikchrshow app. It sets up the various UI bits, loads a Worker for
15
the pikchr process, and manages the communication between the UI and
16
worker.
17
18
API dependencies: fossil.dom, fossil.copybutton, fossil.storage
19
*/
20
(function(F/*fossil object*/){
21
'use strict';
22
23
/* Recall that the 'self' symbol, except where locally
24
overwritten, refers to the global window or worker object. */
25
26
const D = F.dom;
27
/** Name of the stored copy of this app's config. */
28
const configStorageKey = 'pikchrshow-config';
29
30
/* querySelectorAll() proxy */
31
const EAll = function(/*[element=document,] cssSelector*/){
32
return (arguments.length>1 ? arguments[0] : document)
33
.querySelectorAll(arguments[arguments.length-1]);
34
};
35
/* querySelector() proxy */
36
const E = function(/*[element=document,] cssSelector*/){
37
return (arguments.length>1 ? arguments[0] : document)
38
.querySelector(arguments[arguments.length-1]);
39
};
40
41
/** The main application object. */
42
const PS = {
43
/* Config options. */
44
config: {
45
/* If true, display input/output areas side-by-side, else stack
46
them vertically. */
47
sideBySide: true,
48
/* If true, swap positions of the input/output areas. */
49
swapInOut: false,
50
/* If true, the SVG is allowed to resize to fit the parent
51
content area, else the parent is resized to fit the rendered
52
SVG (as sized by pikchr). */
53
renderAutofit: false,
54
/* If true, automatically render while the user is typing. */
55
renderWhileTyping: false
56
},
57
/* Various DOM elements. */
58
e: {
59
previewCopyButton: E('#preview-copy-button'),
60
previewModeLabel: E('label[for=preview-copy-button]'),
61
zoneInputButtons: E('.zone-wrapper.input > legend > .button-bar'),
62
zoneOutputButtons: E('.zone-wrapper.output > legend > .button-bar'),
63
outText: E('#pikchr-output-text'),
64
pikOutWrapper: E('#pikchr-output-wrapper'),
65
pikOut: E('#pikchr-output'),
66
btnRender: E('#btn-render')
67
},
68
renderModes: ['svg'/*SVG must be at index 0*/,'markdown', 'wiki', 'text'],
69
renderModeLabels: {
70
svg: 'SVG', markdown: 'Markdown', wiki: 'Fossil Wiki', text: 'Text'
71
},
72
_msgMap: {},
73
/** Adds a worker message handler for messages of the given
74
type. */
75
addMsgHandler: function f(type,callback){
76
if(Array.isArray(type)){
77
type.forEach((t)=>this.addMsgHandler(t, callback));
78
return this;
79
}
80
(this._msgMap.hasOwnProperty(type)
81
? this._msgMap[type]
82
: (this._msgMap[type] = [])).push(callback);
83
return this;
84
},
85
/** Given a worker message, runs all handlers for msg.type. */
86
runMsgHandlers: function(msg){
87
const list = (this._msgMap.hasOwnProperty(msg.type)
88
? this._msgMap[msg.type] : false);
89
if(!list){
90
console.warn("No handlers found for message type:",msg);
91
return false;
92
}
93
list.forEach((f)=>f(msg));
94
return true;
95
},
96
/** Removes all message handlers for the given message type. */
97
clearMsgHandlers: function(type){
98
delete this._msgMap[type];
99
return this;
100
},
101
/* Posts a message in the form {type, data} to the db worker. Returns this. */
102
wMsg: function(type,data){
103
this.worker.postMessage({type, data});
104
return this;
105
},
106
/** Stores this object's config in the browser's storage. */
107
storeConfig: function(){
108
F.storage.setJSON(configStorageKey,this.config);
109
}
110
};
111
PS.renderModes.selectedIndex = 0;
112
PS._config = F.storage.getJSON(configStorageKey);
113
if(PS._config){
114
/* Copy all properties to PS.config which are currently in
115
PS._config. We don't bother copying any other properties: those
116
would be stale/removed config entries. */
117
Object.keys(PS.config).forEach(function(k){
118
if(PS._config.hasOwnProperty(k)){
119
PS.config[k] = PS._config[k];
120
}
121
});
122
delete PS._config;
123
}
124
125
/* Randomize the name of the worker script so that it is never cached.
126
** The Fossil /builtin method will automatically remove the "-v000000000"
127
** part of the filename, resolving it to just "pikchr-worker.js". */
128
PS.worker = new Worker('builtin/extsrc/pikchr-worker-v'+
129
(Math.floor(Math.random()*10000000000) + 1000000000)+
130
'.js');
131
PS.worker.onmessage = (ev)=>PS.runMsgHandlers(ev.data);
132
PS.addMsgHandler('stdout', console.log.bind(console));
133
PS.addMsgHandler('stderr', console.error.bind(console));
134
135
/** Handles status updates from the Module object. */
136
PS.addMsgHandler('module', function f(ev){
137
ev = ev.data;
138
if('status'!==ev.type){
139
console.warn("Unexpected module-type message:",ev);
140
return;
141
}
142
if(!f.ui){
143
f.ui = {
144
status: E('#module-status'),
145
progress: E('#module-progress'),
146
spinner: E('#module-spinner')
147
};
148
}
149
const msg = ev.data;
150
if(f.ui.progres){
151
progress.value = msg.step;
152
progress.max = msg.step + 1/*we don't know how many steps to expect*/;
153
}
154
if(1==msg.step){
155
f.ui.progress.classList.remove('hidden');
156
f.ui.spinner.classList.remove('hidden');
157
}
158
if(msg.text){
159
f.ui.status.classList.remove('hidden');
160
f.ui.status.innerText = msg.text;
161
}else{
162
if(f.ui.progress){
163
f.ui.progress.remove();
164
f.ui.spinner.remove();
165
delete f.ui.progress;
166
delete f.ui.spinner;
167
}
168
f.ui.status.classList.add('hidden');
169
/* The module can post messages about fatal problems,
170
e.g. an exit() being triggered or assertion failure,
171
after the last "load" message has arrived, so
172
leave f.ui.status and message listener intact. */
173
}
174
});
175
176
PS.e.previewModeLabel.innerText =
177
PS.renderModeLabels[PS.renderModes[PS.renderModes.selectedIndex]];
178
179
/**
180
The 'pikchr-ready' event is fired (with no payload) when the
181
wasm module has finished loading. */
182
PS.addMsgHandler('pikchr-ready', function(event){
183
PS.clearMsgHandlers('pikchr-ready');
184
F.page.onPikchrshowLoaded(event.data);
185
});
186
187
/**
188
Performs all app initialization which must wait until after the
189
worker module is loaded. This function removes itself when it's
190
called.
191
*/
192
F.page.onPikchrshowLoaded = function(pikchrVersion){
193
delete this.onPikchrshowLoaded;
194
// Unhide all elements which start out hidden
195
EAll('.initially-hidden').forEach((e)=>e.classList.remove('initially-hidden'));
196
const taInput = E('#input');
197
const btnClearIn = E('#btn-clear');
198
btnClearIn.addEventListener('click',function(){
199
taInput.value = '';
200
},false);
201
const getCurrentText = function(){
202
let text;
203
if(taInput.selectionStart<taInput.selectionEnd){
204
text = taInput.value.substring(taInput.selectionStart,taInput.selectionEnd).trim();
205
}else{
206
text = taInput.value.trim();
207
}
208
return text;;
209
};
210
const renderCurrentText = function(){
211
const text = getCurrentText();
212
if(text) PS.render(text);
213
};
214
const setCurrentText = function(txt){
215
taInput.value = txt;
216
renderCurrentText();
217
};
218
PS.e.btnRender.addEventListener('click',function(ev){
219
ev.preventDefault();
220
renderCurrentText();
221
},false);
222
223
/** To be called immediately before work is sent to the
224
worker. Updates some UI elements. The 'working'/'end'
225
event will apply the inverse, undoing the bits this
226
function does. This impl is not in the 'working'/'start'
227
event handler because that event is given to us
228
asynchronously _after_ we need to have performed this
229
work.
230
*/
231
const preStartWork = function f(){
232
if(!f._){
233
const title = E('title');
234
f._ = {
235
pageTitle: title,
236
pageTitleOrig: title.innerText
237
};
238
}
239
//f._.pageTitle.innerText = "[working...] "+f._.pageTitleOrig;
240
PS.e.btnRender.setAttribute('disabled','disabled');
241
};
242
243
/**
244
Submits the current input text to pikchr and renders the
245
result. */
246
PS.render = function f(txt){
247
preStartWork();
248
this.wMsg('pikchr',{
249
pikchr: txt,
250
darkMode: !!window.fossil.config.skin.isDark
251
});
252
};
253
254
/**
255
Event handler for 'pikchr' messages from the Worker thread.
256
*/
257
PS.addMsgHandler('pikchr', function(ev){
258
const m = ev.data, pikOut = this.e.pikOut;
259
pikOut.classList[m.isError ? 'add' : 'remove']('error');
260
pikOut.dataset.pikchr = m.pikchr;
261
const mode = this.renderModes[this.renderModes.selectedIndex];
262
switch(mode){
263
case 'text': case 'markdown': case 'wiki': {
264
let body;
265
switch(mode){
266
case 'markdown':
267
body = ['```pikchr', m.pikchr, '```'].join('\n');
268
break;
269
case 'wiki':
270
body = ['<verbatim type="pikchr">', m.pikchr, '</verbatim>'].join('');
271
break;
272
default:
273
body = m.result;
274
}
275
this.e.outText.value = body;
276
this.e.outText.classList.remove('hidden');
277
pikOut.classList.add('hidden');
278
this.e.pikOutWrapper.classList.add('text');
279
break;
280
}
281
case 'svg':
282
this.e.outText.classList.add('hidden');
283
pikOut.classList.remove('hidden');
284
this.e.pikOutWrapper.classList.remove('text');
285
pikOut.innerHTML = m.result;
286
this.e.outText.value = m.result/*for clipboard copy*/;
287
break;
288
default: throw new Error("Unhandled render mode: "+mode);
289
}
290
let vw = null, vh = null;
291
if('svg'===mode){
292
if(m.isError){
293
vw = vh = '100%';
294
}else if(this.config.renderAutofit){
295
/* FIXME: current behavior doesn't work as desired when width>height
296
(e.g. non-side-by-side mode).*/
297
vw = vh = '98%';
298
}else{
299
vw = m.width+1+'px'; vh = m.height+1+'px';
300
/* +1 is b/c the SVG uses floating point sizes but pikchr()
301
returns truncated integers. */
302
}
303
pikOut.style.width = vw;
304
pikOut.style.height = vh;
305
}
306
}.bind(PS))/*'pikchr' msg handler*/;
307
308
E('#btn-render-mode').addEventListener('click',function(){
309
const modes = this.renderModes;
310
modes.selectedIndex = (modes.selectedIndex + 1) % modes.length;
311
this.e.previewModeLabel.innerText = this.renderModeLabels[modes[modes.selectedIndex]];
312
if(this.e.pikOut.dataset.pikchr){
313
this.render(this.e.pikOut.dataset.pikchr);
314
}
315
}.bind(PS));
316
F.copyButton(PS.e.previewCopyButton, {copyFromElement: PS.e.outText});
317
318
PS.addMsgHandler('working',function f(ev){
319
switch(ev.data){
320
case 'start': /* See notes in preStartWork(). */; return;
321
case 'end':
322
//preStartWork._.pageTitle.innerText = preStartWork._.pageTitleOrig;
323
this.e.btnRender.removeAttribute('disabled');
324
this.e.pikOutWrapper.classList[this.config.renderAutofit ? 'add' : 'remove']('autofit');
325
return;
326
}
327
console.warn("Unhandled 'working' event:",ev.data);
328
}.bind(PS));
329
330
/* For each checkbox with data-csstgt, set up a handler which
331
toggles the given CSS class on the element matching
332
E(data-csstgt). */
333
EAll('input[type=checkbox][data-csstgt]')
334
.forEach(function(e){
335
const tgt = E(e.dataset.csstgt);
336
const cssClass = e.dataset.cssclass || 'error';
337
e.checked = tgt.classList.contains(cssClass);
338
e.addEventListener('change', function(){
339
tgt.classList[
340
this.checked ? 'add' : 'remove'
341
](cssClass)
342
}, false);
343
});
344
/* For each checkbox with data-config=X, set up a binding to
345
PS.config[X]. These must be set up AFTER data-csstgt
346
checkboxes so that those two states can be synced properly. */
347
EAll('input[type=checkbox][data-config]')
348
.forEach(function(e){
349
const confVal = !!PS.config[e.dataset.config];
350
if(e.checked !== confVal){
351
/* Ensure that data-csstgt mappings (if any) get
352
synced properly. */
353
e.checked = confVal;
354
e.dispatchEvent(new Event('change'));
355
}
356
e.addEventListener('change', function(){
357
PS.config[this.dataset.config] = this.checked;
358
PS.storeConfig();
359
}, false);
360
});
361
E('#opt-cb-autofit').addEventListener('change',function(){
362
/* PS.config.renderAutofit was set by the data-config
363
event handler. */
364
if(0==PS.renderModes.selectedIndex && PS.e.pikOut.dataset.pikchr){
365
PS.render(PS.e.pikOut.dataset.pikchr);
366
}
367
});
368
/* For each button with data-cmd=X, map a click handler which
369
calls PS.render(X). */
370
const cmdClick = function(){PS.render(this.dataset.cmd);};
371
EAll('button[data-cmd]').forEach(
372
e => e.addEventListener('click', cmdClick, false)
373
);
374
375
376
////////////////////////////////////////////////////////////
377
// Set up selection list of predefined scripts...
378
if(true){
379
const selectScript = PS.e.selectScript = D.select();
380
D.append(PS.e.zoneInputButtons, selectScript);
381
PS.predefinedPiks.forEach(function(script,ndx){
382
const opt = D.option(script.code ? script.code.trim() :'', script.name);
383
D.append(selectScript, opt);
384
if(!ndx) selectScript.selectedIndex = 0 /*timing/ordering workaround*/;
385
if(ndx && !script.code){
386
/* Treat entries w/ no code as separators EXCEPT for the
387
first one, which we want to keep selectable solely for
388
cosmetic reasons. */
389
D.disable(opt);
390
}
391
});
392
delete PS.predefinedPiks;
393
selectScript.addEventListener('change', function(ev){
394
const val = ev.target.value;
395
if(!val) return;
396
setCurrentText(val);
397
}, false);
398
}/*Examples*/
399
400
/**
401
TODO? Handle load/import of an external pikchr file.
402
*/
403
if(0) E('#load-pikchr').addEventListener('change',function(){
404
const f = this.files[0];
405
const r = new FileReader();
406
const status = {loaded: 0, total: 0};
407
this.setAttribute('disabled','disabled');
408
const that = this;
409
r.addEventListener('load', function(){
410
that.removeAttribute('disabled');
411
stdout("Loaded",f.name+". Opening pikchr...");
412
PS.wMsg('open',{
413
filename: f.name,
414
buffer: this.result
415
});
416
});
417
r.addEventListener('error',function(){
418
that.removeAttribute('disabled');
419
stderr("Loading",f.name,"failed for unknown reasons.");
420
});
421
r.addEventListener('abort',function(){
422
that.removeAttribute('disabled');
423
stdout("Cancelled loading of",f.name+".");
424
});
425
r.readAsArrayBuffer(f);
426
});
427
428
EAll('fieldset.collapsible').forEach(function(fs){
429
const btnToggle = E(fs,'legend > .fieldset-toggle'),
430
content = EAll(fs,':scope > div');
431
btnToggle.addEventListener('click', function(){
432
fs.classList.toggle('collapsed');
433
content.forEach((d)=>d.classList.toggle('hidden'));
434
}, false);
435
});
436
437
if(window.sessionStorage){
438
/* If sessionStorage['pikchr-xfer'] exists and the "fromSession"
439
URL argument was passed to this page, load the pikchr source
440
from the session. This is used by the "open in pikchrshow"
441
link in the forum. */
442
const src = window.sessionStorage.getItem('pikchr-xfer');
443
if( src && (new URL(self.location.href).searchParams).has('fromSession') ){
444
taInput.value = src;
445
window.sessionStorage.removeItem('pikchr-xfer');
446
}
447
}
448
D.append(E('fieldset.options > div'),
449
D.append(D.addClass(D.span(), 'labeled-input'),
450
'pikchr v. '+pikchrVersion));
451
452
PS.e.btnRender.click();
453
454
/** Debounce handler for auto-rendering while typing. */
455
const debounceAutoRender = F.debounce(function f(){
456
if(!PS._isDirty) return;
457
const text = getCurrentText();
458
if(f._ === text){
459
PS._isDirty = false;
460
return;
461
}
462
f._ = text;
463
PS._isDirty = false;
464
PS.render(text || '');
465
}, 800, false);
466
467
taInput.addEventListener('keydown',function f(ev){
468
if((ev.ctrlKey || ev.shiftKey) && 13 === ev.keyCode){
469
// Ctrl-enter and shift-enter both run the current input
470
PS._isDirty = false/*prevent a pending debounce from re-rendering*/;
471
ev.preventDefault();
472
ev.stopPropagation();
473
renderCurrentText();
474
return;
475
}
476
if(!PS.config.renderWhileTyping) return;
477
/* Auto-render while typing... */
478
switch(ev.keyCode){
479
case (ev.keyCode<32): /*any ctrl char*/
480
/* ^^^ w/o that, simply tapping ctrl is enough to
481
force a re-render. Similarly, TAB-ing focus away
482
should not re-render. */
483
case 33: case 34: /* page up/down */
484
case 35: case 36: /* home/end */
485
case 37: case 38: case 39: case 40: /* arrows */
486
return;
487
}
488
PS._isDirty = true;
489
debounceAutoRender();
490
}, false);
491
492
const ForceResizeKludge = (function(){
493
/* Workaround for Safari mayhem regarding use of vh CSS
494
units.... We cannot use vh units to set the main view
495
size because Safari chokes on that, so we calculate
496
that height here. Larger than ~95% is too big for
497
Firefox on Android, causing the input area to move
498
off-screen. */
499
const appViews = EAll('.app-view');
500
const elemsToCount = [
501
/* Elements which we need to always count in the
502
visible body size. */
503
E('body > header'),
504
E('body > nav.mainmenu'),
505
E('body > footer')
506
];
507
const resized = function f(){
508
if(f.$disabled) return;
509
const wh = window.innerHeight;
510
var ht;
511
var extra = 0;
512
elemsToCount.forEach((e)=>e ? extra += F.dom.effectiveHeight(e) : false);
513
ht = wh - extra;
514
appViews.forEach(function(e){
515
e.style.height =
516
e.style.maxHeight = [
517
"calc(", (ht>=100 ? ht : 100), "px",
518
" - 2em"/*fudge value*/,")"
519
/* ^^^^ hypothetically not needed, but both
520
Chrome/FF on Linux will force scrollbars on the
521
body if this value is too small. */
522
].join('');
523
});
524
};
525
resized.$disabled = true/*gets deleted when setup is finished*/;
526
window.addEventListener('resize', F.debounce(resized, 250), false);
527
return resized;
528
})()/*ForceResizeKludge*/;
529
530
delete ForceResizeKludge.$disabled;
531
ForceResizeKludge();
532
}/*onPikchrshowLoaded()*/;
533
534
535
/**
536
Predefined example pikchr scripts. Each entry is an object:
537
538
{
539
name: required string,
540
code: optional code string. An entry with a falsy code is treated
541
like a separator in the resulting SELECT element (a
542
disabled OPTION).
543
}
544
*/
545
PS.predefinedPiks = [
546
{name: "-- Example Scripts --", code: false},
547
/*
548
The following were imported from the pikchr test scripts:
549
550
https://fossil-scm.org/pikchr/dir/examples
551
*/
552
{name:"Cardinal headings",code:` linerad = 5px
553
C: circle "Center" rad 150%
554
circle "N" at 1.0 n of C; arrow from C to last chop ->
555
circle "NE" at 1.0 ne of C; arrow from C to last chop <-
556
circle "E" at 1.0 e of C; arrow from C to last chop <->
557
circle "SE" at 1.0 se of C; arrow from C to last chop ->
558
circle "S" at 1.0 s of C; arrow from C to last chop <-
559
circle "SW" at 1.0 sw of C; arrow from C to last chop <->
560
circle "W" at 1.0 w of C; arrow from C to last chop ->
561
circle "NW" at 1.0 nw of C; arrow from C to last chop <-
562
arrow from 2nd circle to 3rd circle chop
563
arrow from 4th circle to 3rd circle chop
564
arrow from SW to S chop <->
565
circle "ESE" at 2.0 heading 112.5 from Center \
566
thickness 150% fill lightblue radius 75%
567
arrow from Center to ESE thickness 150% <-> chop
568
arrow from ESE up 1.35 then to NE chop
569
line dashed <- from E.e to (ESE.x,E.y)
570
line dotted <-> thickness 50% from N to NW chop
571
`},{name:"Core object types",code:`AllObjects: [
572
573
# First row of objects
574
box "box"
575
box rad 10px "box (with" "rounded" "corners)" at 1in right of previous
576
circle "circle" at 1in right of previous
577
ellipse "ellipse" at 1in right of previous
578
579
# second row of objects
580
OVAL1: oval "oval" at 1in below first box
581
oval "(tall &" "thin)" "oval" width OVAL1.height height OVAL1.width \
582
at 1in right of previous
583
cylinder "cylinder" at 1in right of previous
584
file "file" at 1in right of previous
585
586
# third row shows line-type objects
587
dot "dot" above at 1in below first oval
588
line right from 1.8cm right of previous "lines" above
589
arrow right from 1.8cm right of previous "arrows" above
590
spline from 1.8cm right of previous \
591
go right .15 then .3 heading 30 then .5 heading 160 then .4 heading 20 \
592
then right .15
593
"splines" at 3rd vertex of previous
594
595
# The third vertex of the spline is not actually on the drawn
596
# curve. The third vertex is a control point. To see its actual
597
# position, uncomment the following line:
598
#dot color red at 3rd vertex of previous spline
599
600
# Draw various lines below the first line
601
line dashed right from 0.3cm below start of previous line
602
line dotted right from 0.3cm below start of previous
603
line thin right from 0.3cm below start of previous
604
line thick right from 0.3cm below start of previous
605
606
607
# Draw arrows with different arrowhead configurations below
608
# the first arrow
609
arrow <- right from 0.4cm below start of previous arrow
610
arrow <-> right from 0.4cm below start of previous
611
612
# Draw splines with different arrowhead configurations below
613
# the first spline
614
spline same from .4cm below start of first spline ->
615
spline same from .4cm below start of previous <-
616
spline same from .4cm below start of previous <->
617
618
] # end of AllObjects
619
620
# Label the whole diagram
621
text "Examples Of Pikchr Objects" big bold at .8cm above north of AllObjects
622
`},{name:"Swimlanes",code:` $laneh = 0.75
623
624
# Draw the lanes
625
down
626
box width 3.5in height $laneh fill 0xacc9e3
627
box same fill 0xc5d8ef
628
box same as first box
629
box same as 2nd box
630
line from 1st box.sw+(0.2,0) up until even with 1st box.n \
631
"Alan" above aligned
632
line from 2nd box.sw+(0.2,0) up until even with 2nd box.n \
633
"Betty" above aligned
634
line from 3rd box.sw+(0.2,0) up until even with 3rd box.n \
635
"Charlie" above aligned
636
line from 4th box.sw+(0.2,0) up until even with 4th box.n \
637
"Darlene" above aligned
638
639
# fill in content for the Alice lane
640
right
641
A1: circle rad 0.1in at end of first line + (0.2,-0.2) \
642
fill white thickness 1.5px "1"
643
arrow right 50%
644
circle same "2"
645
arrow right until even with first box.e - (0.65,0.0)
646
ellipse "future" fit fill white height 0.2 width 0.5 thickness 1.5px
647
A3: circle same at A1+(0.8,-0.3) "3" fill 0xc0c0c0
648
arrow from A1 to last circle chop "fork!" below aligned
649
650
# content for the Betty lane
651
B1: circle same as A1 at A1-(0,$laneh) "1"
652
arrow right 50%
653
circle same "2"
654
arrow right until even with first ellipse.w
655
ellipse same "future"
656
B3: circle same at A3-(0,$laneh) "3"
657
arrow right 50%
658
circle same as A3 "4"
659
arrow from B1 to 2nd last circle chop
660
661
# content for the Charlie lane
662
C1: circle same as A1 at B1-(0,$laneh) "1"
663
arrow 50%
664
circle same "2"
665
arrow right 0.8in "goes" "offline"
666
C5: circle same as A3 "5"
667
arrow right until even with first ellipse.w \
668
"back online" above "pushes 5" below "pulls 3 & 4" below
669
ellipse same "future"
670
671
# content for the Darlene lane
672
D1: circle same as A1 at C1-(0,$laneh) "1"
673
arrow 50%
674
circle same "2"
675
arrow right until even with C5.w
676
circle same "5"
677
arrow 50%
678
circle same as A3 "6"
679
arrow right until even with first ellipse.w
680
ellipse same "future"
681
D3: circle same as B3 at B3-(0,2*$laneh) "3"
682
arrow 50%
683
circle same "4"
684
arrow from D1 to D3 chop
685
`},{
686
name: "The Stuff of Dreams",
687
code:`
688
O: text "DREAMS" color grey
689
circle rad 0.9 at 0.6 above O thick color red
690
text "INEXPENSIVE" big bold at 0.9 above O color red
691
692
circle rad 0.9 at 0.6 heading 120 from O thick color green
693
text "FAST" big bold at 0.9 heading 120 from O color green
694
695
circle rad 0.9 at 0.6 heading -120 from O thick color blue
696
text "HIGH" big bold "QUALITY" big bold at 0.9 heading -120 from O color blue
697
698
text "EXPENSIVE" at 0.55 below O color cyan
699
text "SLOW" at 0.55 heading -60 from O color magenta
700
text "POOR" "QUALITY" at 0.55 heading 60 from O color gold
701
`},{name:"Precision Arrows",code:`
702
# Source: https://pikchr.org/home/forumpost/7f2f9a03eb
703
define quiver {
704
dot invis at 0.5 < $1.ne , $1.e >
705
dot invis at 0.5 < $1.nw , $1.w >
706
dot invis at 0.5 < $1.se , $1.e >
707
dot invis at 0.5 < $1.sw , $1.w >
708
709
dot at $2 right of 4th previous dot
710
dot at $3 right of 4th previous dot
711
dot at $4 right of 4th previous dot
712
dot at $5 right of 4th previous dot
713
arrow <- from previous dot to 2nd previous dot
714
arrow -> from 3rd previous dot to 4th previous dot
715
}
716
717
define show_compass_l {
718
dot color red at $1.e " .e" ljust
719
dot same at $1.ne " .ne" ljust above
720
line thick color green from previous to 2nd last dot
721
}
722
723
define show_compass_r {
724
dot color red at $1.w " .w" ljust
725
dot same at $1.nw " .nw" ljust above
726
line thick color green from previous to 2nd last dot
727
}
728
729
PROGRAM: file "Program" rad 45px
730
show_compass_l(PROGRAM)
731
QUIVER: box invis ht 0.75
732
DATABASE: oval "Database" ht 0.75 wid 1.1
733
show_compass_r(DATABASE)
734
735
quiver(QUIVER, 5px, -5px, 5px, 0px)
736
737
text "Query" with .c at 0.1in above last arrow
738
text "Records" with .c at 0.1in below 2nd last arrow
739
`}
740
];
741
742
743
})(window.fossil);
744

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button