Welcome to #MindPage! <<bounce('◄ tap!')>>
- Check out intro by founder Olcan.
- Tap items to edit. <<jslink('location.reload()','Reload')>> to reset.
- Tap <<_user.image>> above to sign in.
#_pin #_bounce
Welcome to #MindPage! <<bounce('◄ tap!')>>
#_pin #_bounce
<<evallink(_this,'google()',icons.google)>>
<<evallink(_this,'tweet()',icons.twitter)>>
<<icons.twitter>>
<<icons.news>>
<<icons.youtube>>
<<icons.covid>>
<<icons.covid_ctp>>
// image macros for menu links above
const s = 24 // use 24px icons
const img_favicon = (d) => img(`https://${d}/favicon.ico`, s)
const img_touch = (d) => img(`https://${d}/apple-touch-icon.png`, s)
const img_db = (n) => img(`https://www.dropbox.com/s/${n}?dl=0`, s)
const img_wp = (n) => img('https://upload.wikimedia.org/wikipedia/'+n, s)
const icons = {
// google: img_favicon('google.com'),
google: img_wp(escape('commons/c/c1/Google_"G"_logo.svg')),
cnn: img_db('j1vx346nq7x0ujw/cnn.png'),
news: img_db('di7uv2b5d2lot26/apple_news.png'),
twitter: img_db('bsgvkbn8gszx79x/twitter.png'),
youtube: img_db('6ox8qfjpu1bn6a5/youtube.png'),
covid: img_db('0fez9jgzueiosdf/covid.png'),
covid_ctp: img_db('em0t8h940oi55ka/covid_ctp.png')
}
const urls = {
covid: 'https://www.worldometers.info/coronavirus/country/us/',
covid_ctp: 'https://covidtracking.com/data/charts/us-all-key-metrics'
}
// functions used in menu links above
function google() {
const q = MindBox.get().trim(); MindBox.clear()
window.open("https://google.com/search?q="+encodeURIComponent(q))
}
function tweet() {
const t = MindBox.get().trim(); MindBox.clear()
// for web twitter: https://twitter.com/intent/tweet?text=...
location.href="twitter://post?message="+encodeURIComponent(t)
}
#_pin/dot/1 #_menu
#items/naming/nested names have the form #…/name.
items and items/naming are context for this item.#Features are enabled by certain special tags: #/log #/_pin #/_menu #/_context #/_async #/_init #/_welcome #/_listen #/_autorun #/_autodep #/_style #/_spell #/_debug.
#Features/_autodep tag enables item and all descendants to automatically become first dependency on their children, thus forming a dependency chain/tree without having to use repetitive hidden tags.
#Shortcuts are listed below using #key_symbols. Most shortcuts are not available on virtual keyboards due to modifiers that are either missing (e.g. ⌘) or inconsistent (e.g. ⇧). Equivalent functionality should be available using buttons (e.g. create and save buttons for #⇧⏎). Please report issues to support@mind.page.
| Keys | Item Editor | MindBox | Window |
|---|---|---|---|
| ⇧⏎ | Save | Create¹ | Create |
| ⌘S | Save⁹ | Create | Create |
| ⌘⏎ | Save+Run | Create+Run | Create+Run |
| ⌃⏎ | Save+Run | Create+Run | Create+Run |
| ⌥⌘⁶⏎ | Save+Run | Create²+Run | Create²+Run |
| ⌥⏎ | Run+Save⁹ | Create | Create |
| ⎋ | Cancel | Cancel | Clear⁸ |
| ⌫ | — | — | Clear |
| ⇧⌫ | — | Clear | Clear |
| ⌘⌫ | Delete | Clear | Clear |
| ^⌫ | Delete | Clear | Clear |
| ⇥ | Indent | Indent | MindBox⁷ |
| ⇧⇥ | Unindent | Unindent | MindBox |
| ⌘/ | (Un)Comment | (Un)Comment | MindBox |
| ⇧⌘I | Add Image | Add Image | Add Image |
| ⇧⌘S | Resume³ | Resume | Resume |
| ⇧⌘⏎ | Resume | Create | Resume |
| ⇧⌃⏎ | Resume | Create | Resume |
| ⇧⌥⌘⁷⏎ | Resume | Save | Resume |
| ⌘↑ | Edit Prev | History Prev⁴ | MindBox |
| ⌘↓ | Edit Next | History Next⁵ | MindBox |
| ¹ | Create & save new item, continue editing |
| ² | Create & save new item (stop editing) |
| ³ | Resume last edit (after save) |
| ⁴ | Previous in MindBox history |
| ⁵ | Next in MindBox history, or edit top item |
| ⁶ | Any 2+ modifiers (⌃⌥⌘) allowed |
| ⁷ | Focus keyboard on #MindBox |
| ⁸ | Focus on #MindBox if already clear |
| ⁹ | Save item, continue editing |
#Features/_style tag designates the item as a style item such that its css_style block is automatically installed/updated on the page (under <head><style …>). In addition, any dependents of the style item are automatically styled (on their <div class="item …">) using a special CSS class name derived from the style item name as in dep_<name>. Any invalid characters (outside [A-Za-z0-9_]) are replaced with _.
#MindPage/core/functions are:
window, e.g. html(content).set(text)._item(name,[log_errors]) returns _Item with name (label or id)._items([label]) returns array of _Items._exists(name,[allow_multiple]) returns true if item name exists._create(text,[options]) creates new item w/ given text._labels([selector]) returns array of labels matching optional selector._sublabels(parent) returns array of labels nested under parent label._host returns (canonical) hostname for client_is_local(host) returns true if host is considered local_delay(ms) returns promise resolved after specified time._update_dom() returns promise resolved after dom is up-to-date._scroll_to(y) scroll vertically to position y._encrypt(text) encrypts a (text) string._decrypt(text) decrypts an encrypted (text) string._encrypt_bytes(uint8array) encrypts an byte array._decrypt_bytes(uint8array) decrypts an encrypted byte array._encrypt_item(item) encrypts an item._decrypt_item(item) decrypts an encrypted item._render_item(item) renders an item._parse_tags(text) parses tags in text._parse_label(text) parses label in text._replace_tags(text, regex, replacer) replaces tags in text._resolve_tags(label, tags) resolves tags relative to label._resolve_tag(label, tag) resolves tag relative to label._special_tag(tag) returns if tag enables any #features._extract_block(text, type) extracts block(s) in text._replace_block(text, type, replacer) replaces block(s) in text._stringify(x) converts x to a string._byte_stringify(x) converts x to a byte (binary) string._encode(string,[encoding='base64']) encodes string into encoding:'base64' string, 'utf8' string, 'utf8_array' (Uint8Array), or 'byte_array' (Uint8Array)._decode(encoded,[encoding='base64']) decodes string from specified encoding._concat_byte_arrays(...arrays) concatenates byte arrays._hash(x,[hasher],[stringifier]) hashes x using hasher and stringifier. Default hasher is _hash_64_fnv1a (see below) and default stringifier is toString for functions and JSON.stringify for everything else. Returns integer (≤52-bit) or hex string (>52-bit) depending on hasher:_hash_32_djb2(string) hashes using djb2 (xor variant)._hash_32_fnv1a(string) hashes using 32-bit fnv-1a._hash_32_murmur3(string) hashes using 32-bit murmur v3._hash_52_fnv1a(string) hashes using 52-bit fnv-1a._hash_64_fnv1a(string) hashes using 64-bit fnv-1a._hash_128_murmur3_x64(string) hashes using 128-bit murmur v3 (x64 variant)._hash_128_murmur3_x86(string) hashes using 128-bit murmur v3 (x86 variant)._hash_160_sha1(string) hashes using 160-bit secure hash algo 1.#video macro embeds a YouTube video into item.
const video_options = {
autoplay: false,
show_related: false // false may still show videos from same channel
}
function video(video_id, options) {
options = _.merge(video_options, options);
return html(_item('$id').read('html') // from html block (edit to see)
.replaceAll('%video_id', video_id)
.replaceAll('%autoplay', options.autoplay ? '1':'0')
.replaceAll('%rel', options.show_related ? '1' : '0'))
}
// optional toggle that can show/hide videos in item
const video_toggle = _item('$id').read('html_toggle') // see below
.replaceAll('%id', _that.id)
// optional link to video on youtube
const video_link = (video_id) =>
`[↗︎ open](https://www.youtube.com/video/${video_id}?rel=0)`
// optional link to replace embedded video in same item
const video_switch = (video_id, text) =>
_item('$id').read('html_switch') // see below
.replaceAll('%id', _that.id)
.replaceAll('%video_id', video_id)
.replaceAll('%text', text)
<style>
/* .video { display: none } */
/* .video-toggle-hide { display: none } */
/* .video-toggle-show { display: inline } */
/* .item.show-videos .video { display: block } */
/* .item.show-videos .video-toggle-hide { display: inline } */
/* .item.show-videos .video-toggle-show { display: none } */
.video-toggle-show { display: none }
.hide-videos .item:not(.show-videos) .video { display: none }
.hide-videos .item:not(.show-videos) .video-toggle-hide { display: none }
.hide-videos .item:not(.show-videos) .video-toggle-show { display: inline }
.video-toggle-show, .video-toggle-hide { float: right }
.video-toggle-hide { border-radius: 4px 4px 0 0 !important; background: black !important }
/* NOTE: 2px vertical padding allows touching video below <ul> */
.video-toggle-show, .video-toggle-hide { padding: 2px 8px !important; }
</style>
<div id="$cid" class="video" style="position:relative;width:100%;height:0;padding-bottom:56.25%;background:#171717;border-radius: 4px 0 4px 4px;overflow:hidden">
<iframe class="video" src="https://www.youtube.com/embed/%video_id?autoplay=%autoplay&rel=%rel&playsinline=1" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen style="position:absolute;top:0;left:0;width:100%;height:100%;border-radius:4px 0 4px 4px;overflow:hidden"></iframe>
</div>
<a href="javascript:_item('%id').elem.classList.add('hide-videos');_item('%id').elem.querySelector('.item').classList.remove('show-videos')" onclick="event.stopPropagation()" class="video-toggle-hide">▲ hide video</a> <a href="javascript:_item('%id').elem.classList.remove('hide-videos');_item('%id').elem.querySelector('.item').classList.add('show-videos')" onclick="event.stopPropagation()" class="video-toggle-show">▼ show video</a>
<a href="javascript:_item('%id').elem.querySelector('iframe').contentWindow.location.replace(_item('%id').elem.querySelector('iframe').src.replace(/(embed\/).+?(\?|$)/,'$1%video_id$2'));_item('%id').elem.classList.remove('hide-videos');_item('%id').elem.querySelector('.item').classList.add('show-videos')" onclick="event.stopPropagation()">%text</a>
#gapi/examples/gmail is an example for Gmail API. Quick Start Guide explains how to enable in console and create credentials for access.
const scope = 'https://www.googleapis.com/auth/gmail.readonly'
const doc = 'https://gmail.googleapis.com/$discovery/rest?version=v1'
await gapi.client._auth({ scope: scope, discoveryDocs: doc })
await gapi.client._try(async ({gmail})=>{
// get list of emails in inbox (max 1 to print top email only)
// see https://developers.google.com/gmail/api/reference/rest/v1/users.messages/list
let messages = (await gmail.users.messages.list({
userId:'me', labelIds:['INBOX'], maxResults: 1 }))?.result?.messages ?? []
_this.log(`found ${messages?.length} messages`)
if (!messages?.length) return _this.remove('_output') // no emails, stop
// retrieve email contents
messages = await Promise.all(messages.map(msg=>
gmail.users.messages.get({ userId:'me', id:msg.id })))
_this.log(`retrieved ${messages.length} messages`)
return summarize_email(messages[0].result)
})
function summarize_email(email) {
const subject = email.payload.headers.find(h=>h.name=='Subject')
?.value ?? '(missing subject)'
let parts = (email.payload.parts ?? []).map(p=>p.parts ?? p).flat()
let body = email.payload.body.data ??
parts.find(p=>p.mimeType=='text/plain')?.body.data
if (body) {
// see https://stackoverflow.com/a/28622096 for body decoding logic
body = _decode(body.replace(/-/g, '+').replace(/_/g, '/'), 'base64')
body = body.replace(/\r\n/g, '\n').trim() // replace CRLF w/ LF only then trim
// decode html entities using DOMParser, see https://stackoverflow.com/a/34064434
body = new DOMParser().parseFromString(body, "text/html")
.documentElement.textContent
body = body.replace(/#/g, '\\#') // escape hashtags
body = body.replace(/\s+/g, ' ') // normalize whitespace for snippet
} else body = '(missing body)'
const url = 'https://mail.google.com/mail/u/0/#inbox/' + email.id
return [subject, _.truncate(body, {length:100}), url].join('\n\n')
}
#_gapi
#gapi/examples/profile is a basic example for Google People API.Quick Start Guide explains how to enable in console and create credentials for access.
const scope = 'https://www.googleapis.com/auth/userinfo.profile'
const doc = 'https://people.googleapis.com/$discovery/rest'
await gapi.client._auth({ scope: scope, discoveryDocs: doc })
await gapi.client._try(async ({people:{people}})=>{
// fetch and output user name
const response = await people.get({
resourceName: 'people/me',
personFields: 'names'
})
return "Your name is " + response.result.names[0]?.displayName
})
#_gapi
#commands/agenda command <<cmdlink('/agenda')>> creates an item listing upcoming events. Follow #gapi/examples/calendar to set up the API before initial use.
async function run() {
const events = await fetch_events(5) // from #gapi/examples/calendar
let agenda = events.length == 0 ? 'No upcoming events.' :
["|||", "|-|-", ...events.map((event) => {
const ms = event.start.local.getTime() - Date.now()
return '|' + [time_short(ms), event.summary].join('|')
})].map((s)=>"> "+s).join('\n')
return {
text: "**Upcoming Events** from #commands/agenda\n" + agenda,
// scroll to created item after dom update (so item.elem is non-null)
init: item => _update_dom().then(()=>_scroll_to(item.elem.offsetTop)),
edit: false
}
}
#_gapi/examples/calendar #_macros
#gapi/examples/calendar is an example for Google Calendar API. Quick Start Guide explains how to enable in console and create credentials for access.
const events = await fetch_events() // see helper definition below
// output markdown table w/ event name, start date/time, calendar name
if (events.length == 0) _this.write('No upcoming events found.','_md_output')
else _this.write(['|||||', '|:-|:-|:-|:-|', ...events.map(e=>{
const date = e.start.local.toLocaleDateString()
const time = e.start.local.toLocaleTimeString()
const cal = e.cal.summaryOverride || e.cal.summary || e.cal.id
return '|' + [ _.truncate(e.summary), date, time, _.truncate(cal)].join(' | ')
})].join('\n'),'_md_output')
// helper to fetch events in all calendars, ordered by start time
async function fetch_events(limit_per_cal = 10) {
const scope = 'https://www.googleapis.com/auth/calendar.readonly'
const doc = 'https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest'
await gapi.client._auth({ scope: scope, discoveryDocs: doc })
return await gapi.client._try(async ({calendar})=>{
// get all calendars
const cals = (await calendar.calendarList.list()).result.items
// read future events (up to 10 per calendar)
let events = (await Promise.all(cals.map(async cal=>
(await calendar.events.list({
calendarId: cal.id, timeMin: new Date().toISOString(),
showDeleted: false, singleEvents: true,
maxResults: limit_per_cal, orderBy: 'startTime'
})).result.items.map(e=>Object.assign(e,{cal}))
))).flat()
// compute event start-end times in local time zone, then sort by start time
events.forEach(e=>{
if (!e.start.dateTime) { // all-day event
e.start.local = new Date(e.start.date + ' 00:00:00')
e.end.local = new Date((e.end.date || e.start.date) + ' 00:00:00')
} else {
e.start.local = new Date(e.start.dateTime)
e.end.local = new Date(e.end.dateTime || e.start.dateTime)
}
})
return _.sortBy(events, 'start.local')
})
}
#_gapi
#gapi is Google API client library. Here we load the core client library gapi.client and extend it with _auth([options]) and _try(func) to simplify authenticated access to specific scopes and credentials. See examples profile, calendar, and gmail.
// load GAPI, GIS (for auth), and GAPI client as needed
if (!window.gapi?.client || !window.google?.accounts?.oauth2) {
await _load([
window.gapi || 'https://apis.google.com/js/api.js',
window.google?.accounts?.oauth2 || 'https://accounts.google.com/gsi/client'
])
await new Promise((resolve, reject)=>gapi.load('client',
{callback: resolve, onerror: reject}))
}
const _gapi = _item('$id') // this item
gapi.client._auth = async function({
scope, discoveryDocs, // required scope & discovery urls
apiKey, clientId, clientSecret // optional auth info
}) {
if (!scope) fatal('missing scope')
if (!discoveryDocs) fatal('missing discoveryDocs')
const scopes = scope.split(' ').map(s=>s.split('/').pop()).join(' ') // for logging
discoveryDocs = [discoveryDocs].flat() // allow singleton doc
try {
apiKey ||= _gapi.global_store.api_key ||= await _modal({
content:`${_this.name} needs your [API key](https://console.developers.google.com/apis/credentials)`,
confirm:"Use API Key",
cancel:"Cancel",
input:""
})
if (!apiKey) throw new Error('missing api key')
clientId ||= _gapi.global_store.client_id ||= await _modal({
content:`${_this.name} needs your [Client ID](https://console.developers.google.com/apis/credentials)`,
confirm:"Use Client ID",
cancel:"Cancel",
input:""
})
if (!clientId) throw new Error('missing client id')
clientSecret ||= _gapi.global_store.client_secret ||= await _modal({
content:`${_this.name} needs your [Client Secret](https://console.developers.google.com/apis/credentials)`,
confirm:"Use Client Secret",
cancel:"Cancel",
input:""
})
if (!clientSecret) throw new Error('missing client secret')
// init GAPI client (multiple init ok)
await gapi.client.init({ apiKey, discoveryDocs })
// get token if needed
if (!_gapi._global_store._token_response ||
!google.accounts.oauth2.hasGrantedAllScopes(
_gapi._global_store._token_response, ...scope.split(' '))) {
// get authorization code (with user consent) for "offline" (refreshable) token
// note there are two cases where auth client fails to return/throw errors:
// if popups are blocked, then it simply logs an error via console.error
// if popup is closed w/o proceeding w/ auth, then it does nothing
// to handle these, we set up console.error and a focus handler
// see https://stackoverflow.com/q/72387245
const _console_error = console.error // to be restored below
let on_focus // to be removed as listener below
let start = Date.now()
_this.debug(`GAPI requesting authorization code for '${scopes}' ...`)
const auth_code = await new Promise(async (resolve, reject) => {
// set up console.error to handle popup errors (e.g. blocked by browser)
console.error = (...args) => {
if (args.join().includes('popup')) reject(new Error(args.join()))
else _console_error(...args)
}
// set up focus handler to handle return to window without authorization
on_focus = () => reject(new Error('failed to complete authorization'))
addEventListener('focus', on_focus)
try {
// display modal to clarify next step and help enable popups (esp. on ios)
const confirmed = await _modal({
content:`${_this.name} needs your permission for '${scopes}'`,
confirm:"Continue",
cancel:"Cancel",
})
if (!confirmed) return reject(new Error('cancelled by user'))
const auth_client = await google.accounts.oauth2.initCodeClient({
client_id: clientId, scope, ux_mode: 'popup',
hint: _user.email, // help skip account selection
select_account: false,
callback: (resp) => {
if (resp.error) return reject(resp.error)
_this.log(`GAPI received authorization code for '${scopes}' in ${Date.now()-start}ms`)
resolve(resp.code)
}
})
auth_client.requestCode()
} catch (e) {
console.error(e)
reject(resp)
}
}).finally(()=>{
console.error = _console_error
removeEventListener('focus', on_focus)
})
start = Date.now()
_this.debug(`GAPI requesting access/refresh tokens for '${scopes}' ...`)
// https://developers.google.com/identity/protocols/oauth2/web-server#httprest_3
const resp = await fetch('https://oauth2.googleapis.com/token', {
method:'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code: auth_code,
client_id: clientId,
client_secret: clientSecret,
grant_type: 'authorization_code',
redirect_uri: 'https://' + location.host
})
}).then(r=>r.json())
if (resp.error)
throw new Error(`GAPI token request failed: ${JSON.stringify(resp)}`)
_this.log(`GAPI received access/refresh tokens for '${scopes}' in ${Date.now()-start}ms`)
_gapi.global_store._token_response = resp
}
gapi.client.setToken(_gapi.global_store._token_response)
// set up gapi.client._refresh_token()
// NOTE: access tokens are short-lived (~1h based on inspection of token response in console), so caller should call gapi.client._refresh_token() on 401 or 403 errors w/ status PERMISSION_DENIED (see getToken function in example at https://developers.google.com/identity/oauth2/web/guides/migration-to-gis#gapi-asyncawait)
gapi.client._refresh_token ??= async () => {
let start = Date.now()
_this.debug(`GAPI requesting fresh access token ...`)
// https://developers.google.com/identity/protocols/oauth2/web-server#httprest_7
const resp = await fetch('https://oauth2.googleapis.com/token', {
method:'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: clientId,
client_secret: clientSecret,
grant_type: 'refresh_token',
refresh_token: _gapi.global_store._token_response.refresh_token
})
}).then(r=>r.json())
if (resp.error)
throw new Error(`GAPI token request failed: ${JSON.stringify(resp)}`)
_this.log(`GAPI received new access token in ${Date.now()-start}ms`)
// keep refresh token in _token_response
// scopes are also maintained in _aggregate_ by google (server side)
// this allows the client to maintain a single access token w/ all permissions
resp.refresh_token = _gapi.global_store._token_response.refresh_token
_gapi.global_store._token_response = resp
gapi.client.setToken(resp)
}
// uncomment to test refresh
// await gapi.client._refresh_token()
} catch(e) {
throw new Error(`GAPI init failed for '${scopes}': ${e}`)
}
}
gapi.client._try = async function(func, tries = 0) {
if (!gapi.client._refresh_token) fatal('gapi.client._auth is required before _try')
try {
return await func(gapi.client)
} catch (e) {
e = e.result?.error ?? e // dig into gapi error
// if html is returned in e.body, use title as error message
let title = e.body?.match(/<title>(.+?)<\/title>/)?.pop()
if (title) {
// if code block is found in body, append to title
const code = e.body?.match(/<code>(.+?)<\/code>/)?.pop()
if (code) title += ' (' + code + ')'
e = new Error(title)
}
if (tries == 0) { // consider retry
if (e.code == 401 || (e.code == 403 && e.status == 'PERMISSION_DENIED')) {
// access token is missing, invalid, or expired, so retry after refresh
_this.log(`GAPI retrying with fresh token after error ${e.code} (${e.status})`)
await gapi.client._refresh_token()
return await gapi.client._try(func, ++tries)
}
}
_this.error(e)
}
}
#_load #_async
#MindPage is a secure #private notebook that renders plain text #items into #text, #images, #code, #math, #charts, #graphs, #animations, or any other web content. MindPage is readily exportable, extensible, open source, and in #beta stage. See also intro by founder Olcan.
#_context
#animations can be generated using #macros/animation ...
<<animation(green_box, {autoplay:true})>>
<<animation(red_box, {start:.5, show_reset:true})>>
<<animation(explosion)>>
<<animation(explosion_gradient)>>
function green_box(ctx, t, w, h) {
ctx.clearRect(0, 0, w, h)
ctx.fillStyle = 'green'
ctx.fillRect(t/3*w-25, (.5+.25*Math.sin(t*Math.PI))*h-25, 50, 50)
}
function red_box(ctx, t, w, h) {
ctx.clearRect(0, 0, w, h)
ctx.fillStyle = 'red'
ctx.fillRect(t/3*w-25, (.5+.25*Math.sin(t*4*Math.PI))*h-25, 50, 50)
}
function explosion(ctx, t, w, h) {
ctx.clearRect(0, 0, w, h)
const circles = 50
ctx.lineWidth = w/circles
ctx.strokeStyle = 'white'
for (let j=0; j<circles; ++j) {
const r = (j+0.5) / circles
ctx.globalAlpha = Math.pow(
Math.min(1, Math.max(0, 1 - Math.abs(t/3-r))), 10)
ctx.beginPath();
ctx.arc(w*.5, h*.5, r*w, 0, 2 * Math.PI);
ctx.stroke();
}
}
function explosion_gradient(ctx, t, w, h) {
ctx.clearRect(0, 0, w, h)
const stops = 50
let grd = ctx.createRadialGradient(w*.5, h*.5, 0, w*.5, h*.5, w)
for (let j=0; j<stops; ++j) {
const r = j/stops
const a = Math.pow(Math.min(1, Math.max(0, 1 - Math.abs(t/3-r))), 10)
grd.addColorStop(r, `rgba(255, 255, 255, ${a}`);
}
ctx.fillStyle = grd;
ctx.fillRect(0, 0, w, h);
}
... and can be GPU-accelerated using WebGL:
<<animation(webgl_draw,{init:webgl_init(_this.read('glsl_shader'))})>>
precision mediump float;
uniform float t, w, h;
void main(void) {
float x = gl_FragCoord.x / w;
float y = 1.0 - gl_FragCoord.y / h;
y = ((y - 0.5) * h / w) + 0.5; // rescale y to get circles
float r = sqrt((x-0.5)*(x-0.5) + (y-0.5)*(y-0.5));
float a = pow(min(1.0, max(0.0, 1.0 - abs(t/3.0 - r))), 10.0);
gl_FragColor = vec4(a); // premultiplied alpha reduces fringing
}
Also see examples #/brownian_motion and #/ornstein_uhlenbeck.
#_macros/animation #_webgl
#animations/ornstein_uhlenbeck
dX_t = \mu_t dt + \sigma dW_t -\theta X_t t$const sigma = 0.05 // diffusion rate (∝1/mass)
const theta = 0.1 // mean reversion rate
const tail_time = 5 // past length in seconds
const sim_time = 5 // future sim time (in secs, when paused)
const sim_count = 1000 // number of future simulations (when paused)
// const drift = (t) => ({x:0,y:0})
const drift = (t) => ({ // ∞-shaped periodic drift
x: Math.cos(.5 * t * 2*Math.PI),
y: 1.5 * Math.cos(t * 2*Math.PI)
})
<<animation(draw, {period:0, style:'width:500px;height:500px', reset:true, init})>>
let t0 = 0
let x = .5, y = .5 // start in center
let path = [[x,y]]
const max_points = tail_time * 60
function draw(ctx, t, w, h, {pause, t_end=0, offset=0, alpha=1}) {
if (t > t0 + .1) { t0 = t; return } // ignore big time steps
if (t < t0) { t0 = 0; x = .5; y = .5; path = [[x,y]] } // reset
// on pause, simulate future paths
if (pause) {
path = [[x,y]] // clear tail on pause (could be optional)
const sim_start = Date.now()
const x_sim = x, y_sim = y, t0_sim = t0, path_sim = path.slice()
ctx.clearRect(0, 0, w, h)
for (let i=0; i<sim_count; ++i) {
draw(ctx, t, w, h, {
pause: false,
t_end: t + sim_time,
offset: i==0 ? 0 : path_sim.length-1,
alpha: 5/sim_count
})
x = x_sim; y = y_sim; t0 = t0_sim; path = path_sim.slice()
}
return {t} // rewind time to ignore simulation time
}
const pi = path.length - 1 // last point before drawing
do {
// update position, time, tail
const W = jStat.normal(0,sigma*Math.sqrt(t-t0))
const mu = drift(t)
x += W.sample() + (mu.x - theta*(x-.5)) * (t-t0)
y += W.sample() + (mu.y - theta*(y-.5)) * (t-t0)
path.push([x,y])
t0 = t
if (t_end<=0) while (path.length > max_points) path.shift()
else t += 1/60 // simulate at 60fps
} while (t < t_end)
// draw tail
// do not clear canvas if simulating futures (t_end>0)
if (t_end<=0) ctx.clearRect(0, 0, w, h)
ctx.strokeStyle = 'white'
ctx.lineWidth = 3
for (let i=offset; i<path.length-1; ++i) {
ctx.globalAlpha = i < pi ? (i+1)/path.length : alpha
ctx.beginPath()
ctx.moveTo(path[i][0]*w, path[i][1]*h)
ctx.lineTo(path[i+1][0]*w,path[i+1][1]*h)
ctx.stroke()
}
// draw particle (at start of path if simulating futures)
ctx.globalAlpha = 1
ctx.fillStyle = 'white'
ctx.beginPath()
if (t_end<=0) ctx.arc(w*x, h*y, 10, 0, 2*Math.PI)
else ctx.arc(w*path[pi][0], h*path[pi][1], 10, 0, 2*Math.PI)
ctx.fill()
}
// initializer to ensure jStat is loaded
async function init(canvas) {
await _load(window.jStat || 'https://cdn.jsdelivr.net/npm/jstat/dist/jstat.min.js')
return canvas.getContext('2d')
}
#_macros/animation #_jStat
#animations/brownian_motion
dX_t = \mu_t dt + \sigma dW_t$const sigma = 0.05 // inverse mass of particle
const tail = 10 // past length in seconds
const drift = (t) => ({ // ∞-shaped periodic drift
x: Math.cos(.5 * t * 2*Math.PI),
y: 1.5 * Math.cos(t * 2*Math.PI)
})
<<animation(draw, {period:0, reset:true, init})>>
let t0 = 0
let x = .5, y = .5 // start in center
let path = [[x,y]]
const maxPoints = tail * 60
function draw(ctx, t, w, h) {
if (t > t0 + .1) { t0 = t; return } // ignore big time steps
if (t < t0) { t0 = 0; x = .5; y = .5; path = [[x,y]] } // reset
// update position, time, tail
const W = jStat.normal(0,sigma*Math.sqrt(t-t0))
const mu = drift(t)
x += W.sample() + mu.x * (t-t0)
y += W.sample() + mu.y * (t-t0)
t0 = t
path.push([x,y])
while (path.length > maxPoints) path.shift()
// draw tail
ctx.clearRect(0, 0, w, h)
ctx.strokeStyle = 'white'
ctx.lineWidth = 3
for (let i=0; i<path.length-1; ++i) {
ctx.globalAlpha = (i+1)/path.length
ctx.beginPath()
ctx.moveTo(path[i][0]*w, path[i][1]*h)
ctx.lineTo(path[i+1][0]*w,path[i+1][1]*h)
ctx.stroke()
}
// draw particle
ctx.globalAlpha = 1
ctx.fillStyle = 'white'
ctx.beginPath()
ctx.arc(w*x, h*y, 10, 0, 2*Math.PI)
ctx.fill()
}
// initializer to ensure jStat is loaded
async function init(canvas) {
await _load(window.jStat || "https://cdn.jsdelivr.net/npm/jstat@latest/dist/jstat.min.js")
return canvas.getContext('2d')
}
#_macros/animation #_jStat
#TensorFlow/examples/linear_regression is the official linear regression tutorial from TensorFlow CodeLab. We extend the example slightly with L2 regularization, early stopping criteria (to help avoid poor results), and support for WebAssembly (CPU), which can be faster than WebGL (GPU) on smaller problems.
const wasm = true // use WebAssembly (CPU) vs WebGL (GPU)
async function main() {
// define and summarize model
const model = tf.sequential()
model.add(tf.layers.dense({
inputShape: [1], units: 1, name:'linear_l2',
kernelRegularizer: tf.regularizers.l2({l2:.01})
}))
render(tfvis.show.modelSummary,
['model', {name:'model', tab:_this.name}], model)
// fetch and plot data
let data = await fetch('https://storage.googleapis.com' +
'/tfjs-tutorials/carsData.json')
data = await data.json()
data = data.map(car => ({
mpg: car.Miles_per_Gallon, horsepower: car.Horsepower
})).filter(car => car.mpg && car.horsepower)
_this.log(`fetched ${data.length} data points`)
const obs = data.map(d => ({x:d.horsepower,y:d.mpg}))
render(tfvis.render.scatterplot,
['data', {name: 'data', tab:_this.name}],
{values:obs, series:['observed']},
{xLabel:'horsepower',yLabel:'mpg',height:300})
// convert data into tensors
data = tf.tidy(()=>{ // auto-release temporary tensors
tf.util.shuffle(data)
const J = data.length
let inputJ1 = tf.tensor2d(data.map(d=>d.horsepower), [J,1])
let labelJ1 = tf.tensor2d(data.map(d=>d.mpg), [J,1])
const [input_min, input_max] = [inputJ1.min(), inputJ1.max()]
const [label_min, label_max] = [labelJ1.min(), labelJ1.max()]
inputJ1 = inputJ1.sub(input_min).div(input_max.sub(input_min))
labelJ1 = labelJ1.sub(label_min).div(label_max.sub(label_min))
return { inputs: inputJ1, labels: labelJ1,
input_min, input_max, label_min, label_max } })
// train model
model.compile({
optimizer: tf.train.sgd(.01),
loss: tf.losses.meanSquaredError,
metrics: ['mse']
})
tfvis.visor().open() // open visor (sidebar)
const start = Date.now()
let history = []
await model.fit(data.inputs, data.labels, {
batchSize: 16, epochs: 100, shuffle: true,
callbacks: [
tf.callbacks.earlyStopping({monitor:'mse',
minDelta:.0005, patience:5, verbose:1}),
new tf.CustomCallback({
onEpochEnd: (epoch, log) => {
history.push(log)
render(tfvis.show.history,
['history', { name: 'training', tab:_this.name }],
history, ['mse'], {xLabel:'iteration',yLabel:'mse'})
}
})
]
})
_this.log(`training done in ${Date.now()-start}ms`)
// generate predictions
const [xK, predK] = tf.tidy(() => {
let xK = tf.range(0,100).div(100)
let predK1 = model.predict(xK.reshape([100, 1]))
const {input_min, input_max, label_min, label_max} = data
xK = xK.mul(input_max.sub(input_min)).add(input_min)
predK1 = predK1.mul(label_max.sub(label_min)).add(label_min)
return [xK.dataSync(), predK1.dataSync()]
})
// plot together with observed data
const preds = Array.from(xK).map((x,k)=>({x,y:predK[k]}))
render(tfvis.render.scatterplot,
['data', {name: 'data', tab:_this.name}],
{values: [obs, preds], series:['observed','predicted']},
{xLabel:'horsepower',yLabel:'mpg',height:300})
// clean up
tf.dispose(data)
tf.dispose(model)
}
await tf._run(main, wasm ? 'wasm' : 'webgl')
<<tfvis_container('model')>>
<<tfvis_container('data')>>
<<tfvis_container('history')>>
<<tfvis_visor_toggle>>
#_TensorFlow/vis
fetched 392 data points
Epoch 42: early stopping.
training done in 1325ms
tf._run took 1810ms using wasm
#_pin/1 #_video
#beta means actively developed but stable enough for general public use, now open to anyone with a Google account for authentication. A GitHub account is also recommended for technical users.
#commands/export command <<cmdlink('/export')>> downloads a MindPage.zip file containing all of your items as plain text files:
000001) and dated by last interaction time.000001/*.jpg). .markdown since items often contain Markdown formatting./export [extension], e.g. <<cmdlink('/export txt')>>.async function run(extension) {
if (!extension) extension = "markdown"
const items = _items()
const modal = _modal({content: `Exporting ${items.length} items ...`});
await _load([
window.JSZip || 'https://cdn.jsdelivr.net/npm/jszip@3.6.0/dist/jszip.js',
window.saveAs || 'https://cdn.jsdelivr.net/npm/file-saver@2.0.5/dist/FileSaver.min.js'
])
let zip = new JSZip()
let root = zip.folder('MindPage')
await Promise.all(items.map(async (item, index)=>{
let fname = (index+1).toString().padStart(6, '0')
let text = item.text;
const srcs = item.images() // private image srcs
if (srcs.length > 0) {
_modal_update(modal, {content: `Downloading ${srcs.length} images for item ${item.name} ...`})
const blobs = await item.images({output:'blob'});
let folder = root.folder(fname)
blobs.forEach((blob, i) => {
const img_fname = srcs[i]+'.'+mime2ext(blob.type)
text = text.replaceAll(srcs[i], fname + "/" + img_fname)
folder.file(img_fname, blob, {date:new Date(item.time)})
})
}
root.file(fname+'.'+extension, text, {date:new Date(item.time)})
}))
_modal_update(modal, {content: `Exporting ${items.length} items ...`})
saveAs(await zip.generateAsync({ type:'blob' }), `MindPage.zip`)
_modal_close(modal)
}
// from https://stackoverflow.com/a/8801399
function mime2ext(type) {
switch(type) {
case "image/gif": return "gif"
case "image/jpeg": return "jpg"
case "image/png": return "png"
case "image/tiff": return "tiff"
case "image/vnd.wap.wbmp": return "wbmp"
case "image/x-icon": return "ico"
case "image/x-jng": return "jng"
case "image/x-ms-bmp": return "bmp"
case "image/svg+xml": return "svg"
case "image/webp": return "webp"
case "application/pdf": return "pdf"
default: throw new Error("unknown image type: "+type)
}
}
#_async #_load
<div id="time-$cid" style="color:#aaa">
<div class="info" style="float:right"></div>
<div class="time"></div>
<style> #item .unit { font-size: 80%; color:#666; margin-right:5px } </style>
<script>
const unit = (s) => `<span class="unit">${s}</span>`
_this.dispatch_task('time', ()=>{
let div = _this.elem?.querySelector("#time-$cid")
if (!div) return // div not on page (e.g. item being edited)
const date = new Date()
const day_time = date.toLocaleString()
.replace(",", " ").replace(/ +(AM|PM)/, (_,u)=>unit(u))
const dow = date.toLocaleString(
navigator.language, {weekday:'short'})
div.children[0].innerHTML =
_user.oldest_item_time_string.replace(/[mdh]/, (u)=>unit(u))
+ " " + Math.floor(_user.total_text_length/1024) + unit('KB')
div.children[1].innerHTML = dow + " " + day_time
}, 0, 1000) // dispatch now and every second
</script>
</div>
#_pin/dot/0
#TensorFlow/examples/blazeface implements blazeface (paper) face detection demo. Run item to start. Tap video to stop.
const width = 300 // separate from underlying video image size
const target_fps = 20 // lower target can make page more responsive
<!-- _cache_key that depends on $id only ensures same div is used across edits, and _skip_invalidation ensures same div is used across runs; otherwise each run invalidates cache and replaces div by default, which is usually appropriate but not here since we manipulate the div directly and replacing it between runs can reset messages or leak video streams preventing proper stopping of camera -->
<div id="faces-$id" _cache_key="faces-$id" _skip_invalidation style="position:relative;min-height:30px;cursor:pointer">
<video style="position:absolute" autoplay playsinline></video>
<canvas style="position:absolute"></canvas>
<span style="position:absolute">Run item to start face detection.</span>
</div>
const span = _this.elem?.querySelector('#faces-$id span')
if (span) span.innerHTML = "Loading ..."
const tf_url = 'https://cdn.jsdelivr.net/npm/@tensorflow'
await _load([ window.tf || tf_url + '/tfjs/dist/tf.min.js',
window.blazeface || tf_url + '-models/blazeface' ])
const model = await blazeface.load()
let video, canvas, lastUpdateTime
let height, scale, div_initialized = false
let updateCount = 0
let fps = 0
let lastFPSCalcTime = Date.now()
let start = Date.now()
const update = async () => {
if (update != _this.store.update_task) return // cancelled
const div = _this.elem?.querySelector('#faces-$id')
// skip update if div is not on page (item hidden or editing)
if (!div) { setTimeout(()=>requestAnimationFrame(update), 250); return }
const span = div.querySelector('span')
if (video != div.children[0]) { // need to start video
video = div.children[0]
span.innerHTML = "Initializing video ..."
await _update_dom()
let stream = await navigator.mediaDevices.getUserMedia({video:{facingMode:"user"}})
video.srcObject = stream
// video.play() // fails on ios, but autoplay works
// wait until video is loaded and metadata (width/height) available
await new Promise((resolve)=>{video.onloadeddata = resolve})
console.log("started video", video.videoWidth, video.videoHeight)
span.innerHTML = "Detecting faces ..."
scale = width / video.videoWidth
height = scale * video.videoHeight
video.width = width
video.height = height
// NOTE: div may no longer be available at this point (e.g. if item was hidden during video init), so we handle div init separately below
div_initialized = false
}
if (!div_initialized) {
// postpone if div is not on item (e.g. item hidden or editing)
if (!div.closest('.item')) {
setTimeout(()=>requestAnimationFrame(update), 250)
return
}
canvas = div.children[1]
canvas.width = width
canvas.height = height
div.style.width = width + 'px'
div.style.height = height + 'px'
div.onclick = (e) => {
e.stopPropagation()
tf.dispose(model) // release memory used for model
_this.store.update_task = null // stop updates
video.pause() // just in case
video.srcObject.getTracks().forEach(track => track.stop())
video.srcObject = null // release stream
video.height = 0
canvas.height = 0
video = canvas = null
div.style.height = 'auto'
// div.style.zoom = '1'
span.innerHTML = 'Run item to start face detection.'
span.style.padding = 0
span.style.background = 'transparent'
}
span.style.padding = '0 10px' // insert for video overlay
span.style.background = 'rgba(0,0,0,.5)'
div_initialized = true
// start animation frames once dom is updated
_update_dom().then(()=>requestAnimationFrame(update))
return
}
// get face predictions from model
const faces = await model.estimateFaces(
video, false/*tensors*/, false /*flip*/, true /*annotate*/)
if (update != _this.store.update_task) return // cancelled
// draw predictions on canvas
const ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, canvas.width, canvas.height)
if (lastUpdateTime && Date.now() - lastFPSCalcTime > 1000) {
fps = Math.floor(1000 * updateCount/(Date.now()-lastFPSCalcTime))
lastFPSCalcTime = Date.now()
updateCount = 0
}
span.innerHTML = `${faces.length} face(s), ${fps} fps`
for (let i = 0; i < faces.length; i++) {
const start = faces[i].topLeft
const end = faces[i].bottomRight
const size = [end[0] - start[0], end[1] - start[1]]
ctx.lineWidth = 5;
ctx.strokeStyle = 'rgb(0,255,0)'
ctx.strokeRect(start[0]*scale, start[1]*scale, size[0]*scale, size[1]*scale)
const landmarks = faces[i].landmarks
ctx.fillStyle = 'rgb(255,0,0)'
for (let j = 0; j < landmarks.length; j++)
ctx.fillRect(landmarks[j][0]*scale, landmarks[j][1]*scale, 5, 5)
}
updateCount++
lastUpdateTime = Date.now()
setTimeout(()=>{
start = Date.now()
requestAnimationFrame(update)
}, Math.max(0, 1000/target_fps - (Date.now()-start)))
}
_this.store.update_task = update // cancels any others
await update() // start updating, await first update
#_TensorFlow
started video 640 480
#python/example #_python
print('hello world!')
hello world!
#typescript/example #_typescript
const hello : string = 'hello world';
console.log(hello) // log for _log block
return hello // also return for _output block
hello world
hello world
#webppl/examples/HMM implements #/model.
var J = 4; // total steps
var x0 = 1 // initial state
var yJ = [0,0,0,0] // observations (y0 ignored)
<<bar_chart(_this.read('_output'),'height:200px',{posterior:'#d61',prior:'gray'})>>
<!--removed-->
var X_ = function(x) { Discrete({ps:x?[3,7]:[7,3]}) }
var Y_ = function(x) { Discrete({ps:x?[1,9]:[9,1]}) }
var xJ_ = function(J, _xJ, yJ) {
if (_xJ.length == J) return _xJ // done!
var x = sample(X_(_xJ[_xJ.length-1]))
if (yJ && yJ.length > _xJ.length) observe(Y_(x), yJ[_xJ.length])
xJ_(J, _xJ.concat(x), yJ)
}
var prior = function() { xJ_(J,[x0]) }
var posterior = function() { xJ_(J,[x0],yJ) }
webppl._summarize(_enumerate(posterior), _enumerate(prior))
[["x","[1,0,0,0]","[1,1,0,0]","[1,0,0,1]","[1,0,1,0]","[1,1,1,0]"],["posterior","0.83","0.09","0.04","0.02","0.01"],["prior","0.15","0.15","0.06","0.03","0.15"]]
<!--/removed-->
#_webppl #_macros/bar_chart #_macros
webppl took 364ms
#webppl/examples/hmm/model follows the HMM example from http://dippl.org/chapters/04-factorseq.html.
<div id="dot-$id" class="dot" style="height:200px">
<script>
var dot = `digraph {
x0[shape=doublecircle]
subgraph { x1->y1 }
subgraph { x2->y2 }
subgraph cluster { xj->yj; label="J-2"; class=stack }
x0->x1->x2[constraint=false,minlen=3]
x2->xj[constraint=false,minlen=5,style=dotted]
y1,y2,yj[shape=doublecircle]
x0[label="$x_0$"]
x1[label="$x_1$"]
x2[label="$x_2$"]
xj[label="$x_j$"]
y1[label="$y_1$"]
y2[label="$y_2$"]
yj[label="$y_j$"]
}`
graphviz._graph('#dot-$id', dot)
</script>
</div>
$x_j \mid (x_{j-1}=1) \sim \{ 0^.3, 1^.7 \}$
$x_j \mid (x_{j-1}=0) \sim \{ 0^.7, 1^.3 \}$
$y_j \mid (x_{j}=1) \sim \{ 0^.1, 1^.9 \}$
$y_j \mid (x_{j}=0) \sim \{ 0^.9, 1^.1 \}$
<<spacer(10)>>
$(x_j) \mid x_0,(y_j) \sim\ ?$
#_graphviz #_macros
#weight item charts your weight over time. You can enter your weight using #commands/weight or by editing this item (tap to see _data block).
<!--removed-->
3/2/2021 187.8
3/1/2021 188.7
2/26/2021 188.8
2/22/2021 189.2
2/19/2021 189.5
2/12/2021 188.9
2/8/2021 191.4
2/5/2021 192
1/29/2021 193.7
1/26/2021 195.2
1/25/2021 195.3
1/22/2021 195.3
1/20/2021 196.1
1/18/2021 197.2
1/13/2021 198.7
<!--/removed-->
<div id="chart-$id" class="c3" style="height:120px"> <script>
var data = _this.read('_data').split('\n').map((line)=>
line.trim().replace(/["']?(\S+?)["']?[\s,:]+(\S+?)/, '"$1": $2')
).filter((t)=>t)
data = JSON.parse("{"+data.join(',')+"}")
c3._chart('#chart-$id', {
data: {
x:'x',
columns: [ ['x', ..._.keys(data).map(Date.parse)],
['weight', ..._.values(data)] ]
},
axis: {
x: { type: 'timeseries' },
y: { tick: { count:5, format: d3.format('.4r') } }
},
padding: { top: 10, right: 10 } // avoid cutting off tick labels
})
</script> </div>
#_c3
#TensorFlow/util defines various utility functions for #TensorFlow.
if (window.tf) { // skip if tf is not loaded yet
// _run invokes given function and checks for memory leaks
// wraps sync functions (but NOT async functions) in tf.tidy
// disposes globals both before and after invoking function
// sets (or switches) to specified backend if necessary
tf._run = async (func, backend = 'webgl') => {
const start = Date.now()
await tf._set_backend(backend)
tf._release_memory()
const _async = func.constructor.name == 'AsyncFunction'
const out = _async ? await _this.attach(func()) :
tf.tidy(_this.attach(func))
_this.log(`tf._run took ${Date.now()-start}ms ` +
`using ${tf.getBackend()}`)
tf._release_memory()
await Promise.resolve(out).then(()=>
tf._check_memory_for_leaks())
return out
}
// sets backend, loading additional files as necessary
tf._set_backend = async (backend) => {
if (tf.getBackend() == backend) return
if (backend == 'wasm' && !tf.engine()?.registryFactory?.wasm) {
await _load('https://cdn.jsdelivr.net/npm/@tensorflow' +
'/tfjs-backend-wasm/dist/tf-backend-wasm.js')
}
await tf.setBackend(backend)
}
// releases any disposable global state for memory check
tf._release_memory = () => {
tf.disposeVariables()
// dispose store._tf in any animations
_.values(window._animations).forEach(anim=>{
if (!anim.store?._tf) return
tf.dispose(anim.store._tf)
anim.store._tf = {}
})
}
// checks memory for possible leaks usign tf.memory()
tf._check_memory_for_leaks = () => {
const mem = tf.memory()
if (mem.unreliable) _this.warn('tf.memory unreliable')
if (mem.numBytes > 0) _this.error('tf.memory.numBytes', mem.numBytes)
if (mem.numBytesInGPU > 0)
_this.error('tf.memory.numBytesInGPU', mem.numBytesInGPU)
}
} // if (window.tf)
#_TensorFlow
#commands/js command /js code evaluates JavaScript code by creating a new item with a js_input block and then running it. Example: <<cmdlink('/js 2+2','/js 2+2','edit:false,run:true')>>
const run = (js) => ({text: "```js_input\n" + js + "\n```"})
#_macros #_MindBox
#images/macros can help simplify image syntax. You can use the generic macro or define your own macros for special urls or formatting.
| <<img('icon.png')>> | \<<img('…')>> |
| <<img('icon.png', 40)>> | \<<img('…', 40)>> |
| <<img('icon.png', 32, 'auto', 'border:2px solid white; border-radius:8px')>> | \<<img('…', 32, 'auto', 'border:2px solid white; border-radius:8px')>> |
#images are added using +img button or ⇧⌘I shortcut.
| <img src="icon.png"> | <img src="…"> |
| <img src="icon.png" width=40> | <img src="…" width=40> |
| <img src="icon.png" width=32 style="border:2px solid white; border-radius:8px"> | <img src="…" width=32 style="border:1px solid white"> |
#TensorFlow/vis is the official Visualization Library for #TensorFlow. Keyboard shortcuts ` (toggle visor) and ⇧` (=~, to toggle maximize once open) can be used to toggle the "visor" (sidebar) once library is loaded. Macro \<<visor_toggle>> (defined below) can also be used to render a toggle link in items. See basic #/example and linear_regression tutorial.
if (!window.tfvis) {
const base = 'https://cdn.jsdelivr.net/npm/@tensorflow'
const tfjs = base + '/tfjs/dist/tf.min.js'
const tfjs_vis = base + '/tfjs-vis/dist/tfjs-vis.umd.min.js'
if (window.tf) { // load tfjs-vis only
await _load(tfjs_vis)
} else { // load together with tfjs and then set backend
await _load(tfjs, tfjs_vis)
await tf.setBackend('webgl')
}
tfvis.visor().close() // initialize visor closed, minimized
if (tfvis.visor().isFullscreen()) tfvis.visor().toggleFullscreen()
}
<!--removed-->
// convenience function that can render to multiple containers
// string targets are treated as container element ids, all other
// targets are handled by tfvis (most common is a SurfaceInfo object)
const render = (tfvis_func, targets, ...args) => {
return Promise.all([targets].flat().map((target) => {
if (typeof target == 'string') { // interpret as container elem id
// NOTE: missing containers can happen if the item is run before
// first rendering, e.g. if it was typed/pasted into a new editor,
// or if the item is hidden as it is being run; in these cases we
// simply retry every 250ms for 3s and then log an error
const start = Date.now()
const try_render = () => {
const elem = _this.elem?.querySelector('#'+target)
if (elem) _this.attach(tfvis_func(elem, ...args))
else if (Date.now() - start < 3000) setTimeout(try_render, 250)
else console.error(`render: missing container '${target}'`)
}
try_render()
} else return tfvis_func(target, ...args)
}))
}
// set log filter for "Platform browser has already ..." warning
// so it does not get written into item (still logged in console)
// also include the WebGLv2 filter from #TensorFlow
_this.log_options.filter = (entry) =>
!entry.text.startsWith('Platform browser has already') &&
entry.text != 'Could not get context for WebGL version 2'
// install css for when tfvis is loaded
document.head.insertAdjacentHTML("beforeend", "<style>" +
_item('$id').read('css_loaded') + "</style>") // see below
// set up css and any (global) macros
function _init() {
document.head.insertAdjacentHTML("beforeend", "<style>" +
_this.read('css') + "</style>") // see below
// tfvis_visor_toggle macro for toggle link rendered in item
window.tfvis_visor_toggle = `<a _cached class="visor-toggle" title="toggle visor (~)" onclick="window.tfvis?.visor().toggle()">visor</a>`
// tfvis_container macro for elements (divs) for rendering in item
// custom id-only _cache_key ensures element is shared across edits
// _skip_invalidation attribute ensures element is shared across runs
window.tfvis_container = (id) =>
`<div class="tfjs-container" id="${id}" _cache_key="${id}-$id" ` +
`_skip_invalidation></div>`
}
#tfjs-visor-container { color:black; position:absolute; height: 100%; top:0; right:0; z-index:5 }
#tfjs-visor-container button { border:0; cursor:pointer }
#tfjs-visor-container { filter: invert(100%) }
#tfjs-visor-container img { filter: invert(100%) }
.item .tfjs-container { filter: invert(100%); margin-top: 5px }
.item .tfjs-container img { filter: invert(100%) }
.item .tfjs-container .tf-table { filter: invert(100%); border-spacing: 10px 5px; margin: 0 -10px; width: calc(100% + 20px); }
.item .tfjs-container { filter: invert(100%) }
.item .tf-table th { background:#171717; padding:0 5px; border-radius:4px; border:0 !important }
.item .tf-table td { padding:0 5px; border:0 !important }
.item .visor-toggle { position:absolute; z-index:1; right:10px; bottom:10px; cursor:pointer; visibility:hidden } /* hidden until tfvis loaded */
/* workaround for width being too large on smaller iPhones */
@media only screen and (max-width: 600px) {
#tfjs-visor-container { zoom: 0.6 }
/* hide useless (and buggy) Maximize button */
.visor button:first-of-type { display: none }
}
.item .visor-toggle { visibility:visible }
<!--/removed-->
#_load #_async #_init #_TensorFlow
#TensorFlow is a machine learning library. See basic #/example, linear regression tutorial, or face detection demo. Default backend is WebGL (GPU). WebGL 2.0 is used if available (may need to be enabled in browser settings). Helper functions in #/util can help with memory management or with switching to the WebAssembly (CPU) backend, which can be faster on smaller problems.
if (!window.tf) {
await _load('https://cdn.jsdelivr.net/npm/@tensorflow' +
'/tfjs/dist/tf.min.js')
await tf.setBackend('webgl') // default backend (can be changed)
// import #TensorFlow/util _after_ loading tf
_this.eval(_item('#TensorFlow/util').read('js'))
}
// set log filter for "Could not get context for WebGL version 2"
// so it does not get written into item (still logged in console)
_this.log_options.filter = (entry) =>
entry.text != 'Could not get context for WebGL version 2'
#_load #async #/util
#TensorFlow/example #_TensorFlow
function main() {
const J = 1024
const xJJ = tf.randomNormal([J,J])
const yJJ = tf.randomNormal([J,J])
const zJJ = tf.dot(xJJ, yJJ)
}
await tf._run(main) // use default backend (webgl)
await tf._run(main, 'wasm')
tf._run took 75ms using webgl
tf._run took 430ms using wasm
#TensorFlow/vis/example renders a summary of a simple model into both the visor (sidebar) and a container element below. Also opens the visor and renders a toggle link on the lower right using the macro \<<visor_toggle>>.
const backend = 'webgl' // try wasm for smaller models
function main() {
const model = tf.sequential()
model.add(tf.layers.dense({inputShape: [1], units: 1}))
model.add(tf.layers.dense({units: 1}))
// model.summary(60) // log to console (and _log block)
// render into 'summary' container and visor (sidebar)
render(tfvis.show.modelSummary,
['summary', {name:'model', tab:_this.name}], model)
tfvis.visor().open()
}
await tf._run(main, backend)
<<tfvis_container('summary')>>
<<tfvis_visor_toggle>>
#_TensorFlow/vis
tf._run took 5ms using webgl
#c3 is a charting library #preloaded as c3 (or window.c3) and extended here with c3._chart, the preferred constructor for c3 charts on MindPage.
function _init() {
c3._chart = c3_chart; // see below
document.head.insertAdjacentHTML("beforeend", "<style>" +
_this.read('css') + "</style>") // see below
}
function c3_chart(selector, options) {
let elem = document.querySelector(selector)
// NOTE: single-item rendering can be useful for debugging
// if (_that.id != "KqhfMvy3l5OXRzB18Sai") return
if (!elem) return // element removed
const rotated = options.axis?.rotated
const labeled = options.data?.labels
const barchart = options.data?.type == 'bar'
let defaults = {
bindto: selector,
point: { r: 3 },
axis: {
x: {
show: true,
tick: { outer: false, multiline: false },
},
y: {
show: !labeled,
tick: { outer: false, multiline: false },
padding: {
bottom: 10,
top: rotated && labeled ? 70 : labeled ? 40 : 10,
},
},
y2: {
tick: { outer: false, multiline: false }
}
},
grid: { focus: { show: !barchart } },
legend: { show: false },
transition: { duration: 0 }, // disable animations
// NOTE: onrendered must be used carefully because it is invoked on every render (sometimes apparently multiple times per render) as well as during every tab switch and can cause unnecessary slowdown and potential memory leaks -- it can be useful for debugging chart rendering, e.g. to make sure that no "zombie charts" are getting rendered
// onrendered: _that.attach(() => {
// // console.debug('onrendered', elem.id)
// let elem = document.querySelector(selector)
// if (!elem) console.error("rendering zombie chart!")
// if (elem.offsetWidth == 0)
// console.error("zero-width _chart elem")
// })
}
options = _.merge(defaults, options);
if (labeled) elem.classList.add("c3-labeled")
if (rotated) elem.classList.add("c3-rotated")
if (barchart) elem.classList.add("c3-barchart")
// ensure element height matches chart height if specified
if (options.size?.height)
elem.style.height = options.size.height + 'px';
setTimeout(_that.attach(()=>{
let elem = document.querySelector(selector)
if (!elem) return; // element was removed
if (elem.offsetWidth == 0) {
// NOTE: seems to happen occasionally and without visible issue
console.warn("zero-width _chart elem")
_that.invalidate_elem_cache()
// elem._destroy()
return;
}
// console.debug('c3.generate on ', elem)
const chart = c3.generate(options)
// enable various handler functions and required css selectors
elem.setAttribute("_resize", "")
elem.setAttribute("_destroy", "")
elem.setAttribute("_clickable", "")
//elem._chart = chart
elem._resize = () => chart.resize()
elem._destroy = () => {
chart.destroy()
elem.classList.add('c3') // restore .c3 class removed by chart.destroy()
}
elem._clickable = (e) => {
if (e.target.closest(".c3-legend-item-event")) return true
// cursor == 'pointer' covers most foreground elements (e.g. bars) but on mobile devices the background .c3-event-rect is also used for (persistent) tooltips since foreground elements can be small
if (e.target.style.cursor == "pointer") return true
if (e.target.closest(".c3-event-rect")) return true
return false // pass clicks through to item
}
// start monitoring element width, invalidate cache if 0
const monitorElemWidth = _that.attach(()=>{
const elem = document.querySelector(selector)
if (!elem) return // element removed, stop monitoring
if (elem.offsetWidth == 0) {
console.error("zero-width _chart elem")
_that.invalidate_elem_cache()
// elem._destroy()
return // stop monitoring
}
setTimeout(monitorElemWidth, 1000)
})
monitorElemWidth()
}), 100);
// return chart;
}
/* original styles at https://github.com/c3js/c3/blob/master/c3.css */
.c3 { background: #171717; border-radius: 4px; position: relative }
.c3 { break-inside: avoid } /* for multi-column, can be slow */
.c3:not(:first-child) { margin-top: 4px }
.c3:not(:last-child) { margin-bottom: 4px }
.c3 text {
fill: gray;
stroke: none;
font-size: 14px;
font-family: Avenir Next, Helvetica;
}
.c3 path.domain, .c3 .tick line { stroke: gray }
.c3-tooltip { box-shadow: none }
.c3-tooltip th, .c3-tooltip tr, .c3-tooltip td {
color: black;
font-weight: 600;
background-color: #999;
border: 1px solid #444;
}
/* reset .item table spacing styles intended for markdown tables */
.item table.c3-tooltip { border-spacing: 0; margin-left: 0 }
.c3-tooltip th { font-weight: 700 }
.c3-grid { opacity: 0.5 }
.c3-line { stroke-width: 3px }
/* .c3-rotated, .c3-barchart are defined in c3_chart above */
.c3:not(.c3-rotated) .c3-text { transform: translate(0, -5px) }
.c3-rotated .c3-axis-x { transform: translate(0, 1px) }
.c3-rotated .c3-texts .c3-text { transform: translate(0, -1px) }
.c3-barchart .c3-axis-x .tick line { display: none }
.c3-barchart .c3-axis-x .domain { display: none }
#_init
#MindBox is the multi-purpose text box at the top of the page.
create or hit #⇧⏎ to create new item.MindBox (or window.MindBox)#_MindBox to auto-update.get(), set(text), clear(), toggle(text), focus(), create(text,[options]), elem (property)const MindBox = {
get: () => MindBox.elem.value,
set: (text) => {
MindBox.elem.value = text
// also shift selection to the end
// on Safari, setting the selection can auto-focus so we have to blur
const wasFocused = document.activeElement.isSameNode(MindBox.elem)
MindBox.elem.selectionStart = MindBox.elem.value.length
if (!wasFocused) MindBox.elem.blur()
// trigger input event for handling of change
MindBox._input()
},
clear: () => MindBox.set(''),
toggle: (text) => {
if (MindBox.get().trim() == text.trim()) MindBox.clear()
else MindBox.set(text)
},
focus: (text) => {
if (typeof text == 'string') MindBox.set(text)
MindBox.elem.selectionStart = MindBox.elem.value.length
MindBox.elem.focus()
// scroll up to header if necessary
const header = document.getElementById('header')
if (document.body.scrollTop > header.offsetTop)
document.body.scrollTo(0, header.offsetTop)
},
// MindBox.create emulates 'create' button
// use window._create for creating items w/o history & command parsing
create: (text, {edit=false,run=false}={}) => {
if (typeof text == 'string') MindBox.set(text)
// simulate modified keydown for Enter, then keyup for modifiers
// modifier keyups are required by MindBox in case modifiers
// affect behavior of commands or other triggered code
MindBox._keydown({
code:'Enter',
metaKey:true, // first modifier is create shortcut
ctrlKey:!edit, // second modifier disables editing
shiftKey:!run // shift disables running
})
MindBox._keyup({code:'Meta'})
MindBox._keyup({code:'Control'})
MindBox._keyup({code:'Shift'})
return window._mindbox_return // can be created item or promise
},
// elem getter
get elem() {
return document.getElementById('textarea-mindbox')
},
// event triggers for internal use
_input: () => { MindBox.elem.dispatchEvent(
new Event('input')) },
_keydown: (key) => { MindBox.elem.dispatchEvent(
new KeyboardEvent('keydown', key)) },
_keyup: (key) => { MindBox.elem.dispatchEvent(
new KeyboardEvent('keyup', key)) },
}
// make global for easy access from HTML and non-dependents
// (only dependents will auto-update with latest code)
function _init() { window.MindBox = MindBox; }
#webppl/run is a promise-based wrapper for webppl.run.
function webppl_run(code, options) {
return webppl.__run = Promise.allSettled([webppl.__run]).then(()=>
new Promise((resolve, reject) => {
const start = Date.now()
try {
webppl.run(code, (s, x) => {
if (_.keys(s).length > 0) // log webppl state
console.log("webppl state:", JSON.stringify(s));
console.log(`webppl took ${Date.now()-start}ms`)
resolve(x);
}, _.merge({ errorHandlers: [reject] }, options))
} catch (e) { reject(e) }
})
)
}
#features/_listen tag enables code to be evaluated on certain events:
_on_change(text) is evaluated at each change to #MindBox triggered by user typing in, or programmatically. Text can be empty._on_search(text) is evaluated at each new search via #MindBox triggered by user typing in (subject to debounce), or programmatically. Search text can be empty._on_create(text) is evaluated each time new text is submitted on #MindBox using create button, keyboard shortcut, or programmatically._on_command_<name>(`…`) is evaluated when command /name … is submitted on #MindBox. Argument … can be empty. Valid return values are in listed in #commands._on_focus() is evaluated each time MindPage window (or tab) is "focused" to receive keyboard, touch, or other input._on_item_change(id, label, prev_label, deleted, remote, dependency) is evaluated at each change to an item or its dependencies._on_local_store_change(id) is evaluated at each change to local_store of an item._on_global_store_change(id, remote) is evaluated at each change to global_store of an item._on_attr_change(id, remote) is evaluated at each change to attr of an item.function _on_search(text) { alert("You searched for: " + text) }
function _on_create(text) { alert("You created: " + text) }
#features/_async tag enables async evaluation.
_this.start(async()=>{ ... })return any output.await on promises to avoid premature return.write_log._this.log_options.invoke, attach, dispatch, promise, resolve, ...debug, info, log, warn, error_this is still available from lexical context of item.#Features/_autorun tag enables item to be auto-run on changes to #dependencies.
#Features/_spell tag manually enables spell-checking on item. #//_nospell can be used to manually disable spell-checking. Without these tags, spell-checking is enabled automatically for items that do not contain any code blocks (delimited by ```).
#Features/_nospell tag manually disables spell-checking on item. Also see #//_spell.
#macros are #code that generate content.
\<<...>> where ... is #code._html blockhtml(…) macro below.\<<say_hello()>> from #code #_code gives: <<say_hello()>>#_macros):const px = (s) => typeof s == 'number' ? s+'px' : s
const img = (u, w='auto', h='auto', style='') => `<img src="${u}" style="width:${px(w)};height:${px(h)};${style}">`
const code = (text) => "`" + text + "`"
const block = (type, content) => '```'+type+'\n' + content + '\n```'
const html = (content) => block('_html', content)
const json = (obj) => block('json',JSON.stringify(obj, null, 2)
?.replace(/"([^"]+)":/g,'$1:')/*?.replace(/,([^\s\d])/g,", $1")*/)
// NOTE: using onmousedown + cancelled onclick maintains keyboard focus and is generally more robust, especially on mobile devices w/ virtual keyboards
const jslink = (js, text=js, classes='', style='', title=js) =>
`<a href="#" onmousedown="` + _.escape(js) +
`;event.preventDefault();event.stopPropagation()" ` +
`onclick="event.preventDefault();event.stopPropagation()" ` +
`class="${classes}" style="${style}" title="${_.escape(title)}">${text}</a>`
const evallink = (item, js, text=js, classes='', style='', title=js) =>
jslink(`_item('${item.id}')` +
'.eval(`' + js.replace(/([`\\$])/g,"\\$1") + '`)', text, classes, style, title)
const cmdlink = (cmd, text=cmd, options='', classes='', style='', title=cmd) =>
jslink(`MindBox.create(\`${cmd}\`,{${options}})`, text, classes, style, title)
const spacer = (height=10, width=10) =>
`<div style="height:${height}px; width:${width}px"></div>`
const time_short = (ms) => (
ms < 60*1000 ? "<1m" :
ms < 60*60*1000 ? Math.floor(ms/(60*1000)) + "m" :
ms < 24*60*60*1000 ? Math.floor(ms/(60*60*1000)) + "h" :
Math.floor(ms/(24*60*60*1000)) + "d"
)
// register most common macros globally
// importing via #_macros is optional for auto-updating
// also allows _this to refer to invoking item (vs #macros) as expected
// we avoid use of _this in global macros to avoid this ambiguity
function _init() {
window.img = img
window.code = code
window.block = block
window.html = html
window.json = json
window.jslink = jslink
window.evallink = evallink
window.cmdlink = cmdlink
window.spacer = spacer
}
#_init
#MindPage/core/_Item object holds a reference to a MindPage item and provides access to a wide range of properties and functions:
eval([js, options]) evaluates code in context of item.eval_macros(text, [options]) evaluates macros in given text.read([type, options]) reads text from item.read_deep(type, [options]) reads text from item + dependencies.read_input(type, [options]) reads input from item + dependencies.write(text, [type], [options]) writes text into item.clear(type) clears blocks in item.remove(type) removes blocks in item.delete([confirm]) deletes item.get_log([options]) returns console log messages for item.write_log([options]) writes log messages into item.show_logs(autohide_after) shows and auto-hides log messages.hide_logs() hides log messages.show_status([status], [progress]) shows status on running item.clear_status() clears (and hides) status on itemtouch([save=false]) updates (and optionally saves) time of item.save() saves item.start(async_func) starts async evaluation on item.invoke(func) invokes function with item on the stack.attach(func) attaches function to item by wrapping it with invoke.attach(promise) attaches promise to item by wrapping then,catch,finally.dispatch(func, [delay_ms]) dispatches attached function.dispatch_task(name, func, [delay_ms, period_ms]) dispatches named task.cancel_task(name) cancels named task.pending_task(name) returns true if named task is pending completion.promise(func) returns new attached promise with attached executor.resolve(thing) returns new attached promise that resolves thing.debug|info|log|warn|error(...) invokes attached console.*(...).fatal(...) throws Error(...) including stack trace.delay(ms) returns promise resolved after specified time.images([options]) returns array of uploaded images.invalidate_elem_cache() invalidates rendered element cache for item.cached(key, value_function) returns cached value from item.cache.invalidate_cache() invalidates cache for next access via item.cache.validate_cache() validates cache for current item.deephash.save_local_store() saves item.local_store to localStorage.save_global_store() saves item.global_store to firebase.#commands/restore command /restore [name|version] restores an existing or deleted item from a past version.
/restore lists valid names (id:<id>) for recently deleted items./restore name lists valid versions (version:…) for item name.name can be existing item #label or id or deleted item id./restore version creates a new item with text from version.#restored or copy text into existing item.async function run(arg) {
if (_user.uid == 'anonymous') {
alert('/restore command is not useful for anonymous accounts')
return
}
const limit_per_item = 50
const limit_for_deletions = 100 // need extra to skip non-deleted
if (!arg) { // list recent deletions
// const res = await window.firebase.firestore()
// .where("user", "==", _user.uid) // security requirement
// .orderBy("time", "desc")
// .limit(limit_for_deletions).get()
const { getDocs, query, collection, getFirestore, where, orderBy, limit } =
firebase.firestore
const res = await getDocs(query(
collection(getFirestore(firebase), "history"),
where("user", "==", _user.uid), // security requirement
orderBy("time", "desc"),
limit(limit_for_deletions)
))
let versions = []
let deleted = new Set() // set of deleted item ids
res.forEach((doc)=>{
const version = doc.data()
if (deleted.has(version.item)) return // skip older version
if (_exists(version.item)) return // skip non-deleted
deleted.add(version.item) // found deleted item
if (!version.text && !version.cipher)
console.error('unexpected history item', version)
let bytes = (version.text || version.cipher || '').length + ' bytes'
if (version.text === undefined) bytes += ' (encrypted)'
const date = new Date(version.time)
versions.push(`id:${version.item} ${date.toLocaleTimeString()} ${date.toLocaleDateString()} ${bytes}`)
})
if (versions.length == 0) {
alert(`no recent deletions found`)
} else {
const count = versions.length +
(versions.length == limit_per_item ? '+':'')
return { // create item for easy examination of listing
text:[`${count} deleted items found:`,
'```', versions, '```'].flat().join('\n'),
}
}
} else if (!arg.startsWith('version:') &&
(arg.startsWith('#') || arg.startsWith('id:') || _item(arg))) {
// treat arg as existing item #label|id or deleted item id
let id = arg.replace(/^id:/, '') // drop id: prefix
if (arg.startsWith('#')) { // look up id from #name
if (!_item(arg)) { alert(`item ${arg} not found`); return }
id = _item(arg).id
}
// const res = await window.firebase.firestore()
// .collection("history")
// .where("user", "==", _user.uid) // security requirement
// .where("item", "==", id)
// .orderBy("time", "desc")
// .limit(limit_per_item).get()
const { getDocs, query, collection, getFirestore, where, orderBy, limit } =
firebase.firestore
const res = await getDocs(query(
collection(getFirestore(firebase), "history"),
where("user", "==", _user.uid), // security requirement
where("item", "==", id),
orderBy("time", "desc"),
limit(limit_per_item)
))
let versions = []
res.forEach((doc)=>{
const version = doc.data()
let bytes = (version.text || version.cipher || '').length + ' bytes'
if (version.text === undefined) bytes += ' (encrypted)'
const date = new Date(version.time)
versions.push(`version:${doc.id} ${date.toLocaleTimeString()} ${date.toLocaleDateString()} ${bytes}`)
})
if (versions.length == 0) {
alert(`no versions found for item '${arg}'`)
} else {
const count = versions.length +
(versions.length == limit_per_item ? '+':'')
return { // create item for easy examination of listing
text:[`${count} versions found for item ${arg}:`,
'```', versions, '```'].flat().join('\n'),
}
}
} else { // treat arg as version
const id = arg.replace(/^version:/, '') // drop version: prefix
let docref
try { // missing doc can throw permission error
// doc = await window.firebase.firestore()
// .collection("history").doc(id).get()
const { getDoc, doc, getFirestore } = firebase.firestore
docref = await getDoc(doc(getFirestore(firebase), 'history', id))
} catch (e) { console.error(e) }
if (!docref.exists()) { alert(`could not find version '${arg}'`); return }
const item = await _decrypt_item(docref.data())
// NOTE: we prefix #restored label to avoid conflicts/errors
return { text:'#restored ' + item.text }
}
}
#_async
#commands are #code run via #MindBox. Examples: #/hello #/hello_again #/js #/alert #/copy #/export #/restore #/agenda #/invert #/weight.
/name … and hit #⇧⏎.run(`…`) from item named #commands/<name>._on_command_<name>(`…`) in listeners.async or return promise, e.g. #/agenda.undefined) to clear text and defocus.{ text:"…" } to create new item.edit:true|false to start editing created item.run:true|false to run js_input in item.init:(item)=>{...} to initialize item.null or _Item to do nothing.null) is also returned to the programmatic context where command is triggered.#commands/copy command /copy name copies an item. Item name can be specified as either #label or id. For example, this item can be specified as either <<_this.label>> or <<_this.id>>.
const run = (name) => ({text:_item(name).read()})
#typescript enables TypeScript. See #/example.
if (!window.ts) {
const url_base = 'https://unpkg.com/typescript@4.2.3/lib'
if (!window.ts_libs) window.ts_libs = {}
const ts_lib = (n) => !!window.ts_libs[n] || fetch(url_base + `/lib.${n}.d.ts`).then(r=>r.text()).then(t=>window.ts_libs[n]=t)
await _load(url_base + '/typescriptServices.js',
ts_lib('es5'), ts_lib('es6'), ts_lib('dom'))
}
// enable 'run' for (typescript|ts)_input blocks
function _run() {
const ts = '(()=>{ ' + _this.read_input('(typescript|ts)') + '\n})()'
const start = Date.now()
const js = transpile(ts).trim().split('\n').slice(1,-1).join('\n')
console.debug('typescript transpiler took', Date.now()-start, 'ms')
return _this.eval(js, {async:true}).catch((e)=>{})
}
// basic ts->js transpiler that logs all errors to console
function transpile(input) {
// options mimic ts.transpileModule
// https://github.com/microsoft/TypeScript/
// blob/master/src/services/transpile.ts
const options = ts.getDefaultCompilerOptions()
options.suppressOutputPathCheck = true
options.allowNonTsExtensions = true
options.lib = Object.keys(ts_libs)
if (!window.ts_files) window.ts_files = Object.fromEntries(
Object.entries(ts_libs).map(([n, t])=>[n,ts.createSourceFile(n,t)]))
const fname = _this.name
const file = ts.createSourceFile(fname, input, options.target)
window.ts_files[fname] = file
const newLine = ts.getNewLineCharacter(options)
if (!window.ts_output) window.ts_output = {}
// NOTE: using fewer arguments for compiler host (vs transpileModule)
if (!window.ts_compiler) window.ts_compiler = {
getSourceFile: (name) => ts_files[name],
writeFile: (name, text) => { window.ts_output[name] = text },
getDefaultLibFileName: () => '',
useCaseSensitiveFileNames: () => false,
getCanonicalFileName: fileName => fileName,
getCurrentDirectory: () => '',
getNewLine: () => newLine
}
const program = ts.createProgram([fname], options, ts_compiler)
// diagnostics output mimics typescript-simple
// https://github.com/teppeis/typescript-simple/blob/master/index.ts
const diagnostics = program.getSyntacticDiagnostics(file)
.concat(program.getSemanticDiagnostics(file))
.concat(program.getOptionsDiagnostics())
if (diagnostics.length > 0) logDiagnostics(diagnostics)
program.emit()
return ts_output[fname+'.js'] // .js appended by compiler
}
function logDiagnostics(diagnostics) {
return diagnostics.forEach((d) => {
if (d.file && typeof d.start === 'number') console.error(d.messageText + ` (line:${d.file.getLineAndCharacterOfPosition(d.start).line})`)
else console.error(d.messageText)
})
}
#_load #_async
#python enables Python via Brython. See #/example.
if (!window.__BRYTHON__) {
const _brython = 'https://cdn.jsdelivr.net/npm/brython@3.9.1'
await _load(_brython + '/brython.min.js',
_brython + '/brython_stdlib.js')
}
// enable 'run' for python_input blocks
function _run() {
try {
__BRYTHON__.debug = 0
const js = __BRYTHON__.python_to_js(_this.read_input('python'))
return _this.eval(js, {async:true})
} catch (e) {
console.error(`#python error: ${e.args[0]} (line:${e.$line_info})`)
}
}
#_load #_async
#macros/animation macro generates animation on a canvas. Tap canvas to play/pause. Can be customized using an initializer (init) function that returns the context for drawing. The initializer can be async or return a promise, e.g. for loading external libraries. See #webgl for a basic initializer for GPU acceleration.
const animation_options = (options={}) => _.merge({
zoom: 1/devicePixelRatio, // zoom factor (visible px / canvas px)
target_fps: 60, // target frames/sec (lower to reduce load)
autoplay: false, // start playing immediately or require click
start: 0, // starting time (secs, determines initial frame)
period: 3, // period (secs) for repeats (0 means infinite)
style: '', // canvas element style (anything except zoom)
div_style: '', // style for animation container div
reset_style: '', // style for reset button
play_style: '', // style for play button
time_style: '', // style for time text element
fps_style: '', // style for fps text element
status_style: '',// style for status text element
time_format: '.1f', // time (secs) text format
time_prefix: '', // prefix for time text
time_suffix: 's', // suffix for time text
fps_suffix: ' fps', // suffix for fps text
show_reset: options.period==0, // show reset button?
show_fps: true, // display fps?
// context initializer function (can be async return promise)
init: (canvas) => canvas.getContext('2d')
}, options)
function animation(draw = (ctx, t, w, h) => {}, options = {}) {
options = animation_options(options)
if (!window._animations) window._animations = {}
window._animations['$cid'] = {init:options.init, draw, cid:'$cid', item:_this}
return html(_item('$id').read('html') // see html section
.replaceAll('%cid', '$cid') // use $cid of macro instead of script
.replaceAll('%div_style', options.div_style)
.replaceAll('%style', options.style)
.replaceAll('%div_style', options.div_style)
.replaceAll('%fps_style', options.fps_style)
.replaceAll('%time_style', options.time_style)
.replaceAll('%status_style', options.status_style)
.replaceAll('%reset_style', options.reset_style)
.replaceAll('%options', JSON.stringify(options)))
}
<!-- *_removed block is not included in rendered item -->
<!-- %cid is a unique content identifier based on macro usage -->
<div id="animation-%cid" class="animation" style="%div_style">
<div class="fps" style="%fps_style"></div>
<div class="time" style="%time_style"></div>
<div class="status" style="%status_style"></div>
<div class="play" style="%play_style"></div>
<div class="reset" style="%reset_style">reset</div>
<canvas style="%style"></canvas>
<script>
const options = JSON.parse(`%options`)
const div = document.querySelector('#animation-%cid')
const canvas = div.querySelector('canvas')
const fps_elem = div.querySelector('.fps')
const time_elem = div.querySelector('.time')
const status_elem = div.querySelector('.status')
const play_elem = div.querySelector('.play')
const reset_elem = div.querySelector('.reset')
const time_formatter = d3.format(options.time_format)
canvas.width = canvas.offsetWidth / options.zoom
canvas.height = canvas.offsetHeight / options.zoom
canvas.style.zoom = options.zoom
let pause = !options.autoplay
if (options.autoplay) play_elem.style.display = 'none'
// show/hide reset button based on option
// show even if t=start because reset may affect first frame
reset_elem.style.display = options.show_reset ? 'block' : 'none'
let stopped = false
let stopTime = 0
// NOTE: startTime is used to calculate animation time (t) and can be adjusted arbitrarily by the animation drawing function to modify t
let startTime = 0 // set inside update() so first frame is t=start
div.style.cursor = 'pointer'
div.onclick = (e) => {
e.stopPropagation()
if (!pause) pause = true
else if (stopped) {
pause = stopped = false
play_elem.style.display = 'none'
fps_elem.style.display = options.show_fps ? 'block' : 'none'
if (startTime == 0) console.warn('unpaused with startTime==0')
else startTime += (Date.now() - stopTime) // skip stopped time
fps = 0
frameCount = 0
frameCountStartTime = Date.now()
lastFrameRequestTime = Date.now()
update()
}
_this.touch() // soft touch item
}
reset_elem.onclick = (e) => {
e.stopPropagation()
startTime = stopTime = 0
if (stopped) update() // update frame and set start/stopTime
_this.touch() // soft touch item
}
const _draw = _animations['%cid'].draw
const store = _animations['%cid'].store = {}
_animations['%cid'].elem = div
let fps = 0
let frameCount = 0
let frameCountStartTime = 0
let lastFrameRequestTime = 0
let update
_this.resolve(_animations['%cid'].init(canvas)).then((ctx) => {
div.classList.add('initialized')
frameCountStartTime = Date.now()
lastFrameRequestTime = Date.now()
update = () => {
if (canvas._update != update) return // cancelled update loop
// pause animation if canvas is not on non-hidden item
if (!_this.elem?.contains(canvas) || _this.elem?.matches('.hidden'))
pause = true
// calculate time and draw frame
if (startTime == 0) startTime = Date.now() - options.start * 1000
let t = (Date.now() - startTime) * 0.001
if (options.period > 0) t = t % options.period
const obj = _draw(ctx, t, canvas.width, canvas.height, {pause, store, item:_this, cid:'%cid'})
// draw function can pause, reset, adjust t, display status
// (not documented yet, subject to change)
if (typeof obj == 'object') {
if (obj.reset) {
startTime = stopTime = 0 // ensures next frame is t=start
update() // update frame and set start/stopTime
return
}
if (obj.pause) pause = true
if (typeof obj.t == 'number') {
const t_now = (Date.now() - startTime) * 0.001
startTime += (t_now - obj.t) * 1000 // ensures t_now == obj.t
}
if (typeof obj.status == 'string') {
status_elem.textContent = obj.status
status_elem.style.display = obj.status ? 'block' : 'none'
}
}
// format and display time
time_elem.textContent = options.time_prefix + time_formatter(t) + options.time_suffix
// calculate and display fps
if (options.show_fps) {
frameCount++
const now = Date.now()
if (now - frameCountStartTime > 1000) {
fps = 1000 * frameCount / (now - frameCountStartTime)
frameCountStartTime = now
frameCount = 0
}
fps_elem.textContent = (fps == 0 ? '⋯' : fps.toFixed(0)) + options.fps_suffix
}
if (pause) { // paused, stop updating
stopped = true
stopTime = Date.now()
play_elem.style.display = 'block'
fps_elem.style.display = 'none'
return
} else if (stopped) console.warn('unpaused while stopped')
// dispatch next frame, controlling for frame rate
const nextFrame = () => {
lastFrameRequestTime = Date.now()
requestAnimationFrame(update)
}
const fps_delay = Math.max(0,
1000/options.target_fps - (Date.now() - lastFrameRequestTime))
if (fps_delay == 0) nextFrame()
else setTimeout(nextFrame, fps_delay)
}
canvas._update = update // cancels any other update tasks
update() // start updating
}).catch(console.error)
</script></div>
function _init() {
document.head.insertAdjacentHTML("beforeend", "<style>" +
_this.read('css') + "</style>") // see below
}
.animation { background:#171717; width:fit-content; position: relative; display: inline-block; user-select: none; -webkit-user-select: none }
.animation:not(.initialized) > * { visibility: hidden; }
.animation > .fps { position: absolute; top:0; right:0; padding:5px; background:rgba(0,0,0,.75); border-bottom-left-radius: 4px; font-size: 80%; line-height: 100%; color:gray }
.animation > .time { position: absolute; top:0; left:0; padding:5px; background:rgba(0,0,0,.75); border-bottom-right-radius: 4px; font-size: 80%; line-height: 100%; color:gray }
.animation > .reset { position: absolute; bottom:0; left:0; padding:5px; background:rgba(0,0,0,.75); border-top-right-radius: 4px; font-size: 80%; line-height: 100%; color:gray }
.animation > .status { position: absolute; bottom:0; right:0; padding:5px; background:rgba(0,0,0,.75); border-top-left-radius: 4px; font-size: 80%; line-height: 100%; color:gray; display: none }
.animation > .play { position: absolute; top:50%; left:50%; margin-left:-20px; margin-top:-20px; border-left: 40px solid rgba(255,255,255,.5); border-top: 20px solid transparent; border-bottom: 20px solid transparent }
.animation > canvas { border-radius: 4px; vertical-align: middle }
.animation:not(:first-child) { margin-top: 4px; }
.animation:not(:last-child) { margin-bottom: 4px; }
#_init
#webppl is a probabilistic programming language. Here we load the library and extend it with #/run, #/summarize, and #/util. See example HMM.
if (!window.webppl) {
// cdn url from: https://probmods.github.io/webppl-editor/
await _load('https://s3-us-west-2.amazonaws.com' +
'/cdn.webppl.org/webppl-v0.9.9.js')
}
// enable 'run' for webppl_input blocks
const _run = () => webppl._run(_this.read_input('webppl'))
// javascript functions attached to webppl
webppl._run = webppl_run // for js
webppl._summarize = webppl_summarize // for webppl
webppl._samples = webppl_samples // for webppl
// webppl functions from #webppl/util: _enumerate
#_load #_async #_webppl/run #_webppl/util #_webppl/summarize
#commands/weight command /weight weight records today's weight in item #weight.
function run(weight) {
if (!(weight > 0)) { // also rejects empty/non-numeric strings
alert(`invalid weight '${weight}'`)
return '/weight ' // clear for easy re-entry
}
const data_item = '#weight' // item containing _data
const today = new Date().toLocaleDateString()
let data = _item(data_item).read('_data')
if (data.startsWith(today)) data = data.replace(/^.+?\n/,"")
_item(data_item).write(`${today} ${weight}\n` + data, '_data')
}
#webgl defines a basic webgl canvas context initializer webgl_init that only requires a "pixel" shader that assigns a color to each pixel. An example shader and basic animation drawing function (for #macros/animation) is also defined and demonstrated below.
<<animation(webgl_draw,{init:webgl_init(_this.read('glsl_example'))})>>
precision mediump float;
uniform float t, w, h;
void main(void) {
float x = gl_FragCoord.x / w;
float y = 1.0 - gl_FragCoord.y / h;
float r = (x + y) * 0.5;
float a = pow(min(1.0, max(0.0, 1.0 - abs(t/3.0 - r))), 10.0);
gl_FragColor = vec4(a); // premultiplied alpha reduces fringing
}
// basic reusable drawing function
function webgl_draw(gl, t, w, h) {
gl.viewport(0, 0, w, h)
// write t, w, h for shader (see webgl_init below for setup)
gl.uniform1f(t_loc, t)
gl.uniform1f(w_loc, w)
gl.uniform1f(h_loc, h)
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4) // invokes shader
}
let t_loc, w_loc, h_loc
function webgl_init(shader) { return (canvas) => {
const gl = canvas.getContext('webgl')
// enable alpha channel, premultiplied to reduce fringing
// http://www.realtimerendering.com/blog/gpus-prefer-premultiplication/
gl.enable(gl.BLEND)
//gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.blendFunc(gl.ONE /*premultiplied*/, gl.ONE_MINUS_SRC_ALPHA)
// compile vertex shader
const vs = gl.createShader(gl.VERTEX_SHADER)
gl.shaderSource(vs, 'attribute vec2 c; void main(void) { gl_Position = vec4(c, 0.0, 1.0); }')
gl.compileShader(vs)
// compile fragment shader
const fs = gl.createShader(gl.FRAGMENT_SHADER)
gl.shaderSource(fs, shader)
gl.compileShader(fs)
// link program
const prog = gl.createProgram()
gl.attachShader(prog, vs)
gl.attachShader(prog, fs)
gl.linkProgram(prog)
gl.useProgram(prog)
// set up vertex shader coordinate array buffer
const vb = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, vb)
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(
[-1, 1, -1, -1, 1, -1, 1, 1]), gl.STATIC_DRAW)
const c_loc = gl.getAttribLocation(prog, "c")
gl.vertexAttribPointer(c_loc, 2, gl.FLOAT, false, 0, 0)
gl.enableVertexAttribArray(c_loc)
// set up fragment shader inputs
t_loc = gl.getUniformLocation(prog, "t")
w_loc = gl.getUniformLocation(prog, "w")
h_loc = gl.getUniformLocation(prog, "h")
return gl
} }
#_macros/animation
#features/_pin/dot tag designates dotted items:
···) under #MindBox.#_pin/dot/....#bounce macro renders text that "bounces" text as in the ◄ tap! prompt in the welcome item. The bounce starts at welcome ends after a prescribed search or a timeout period.
const bounce = (text, search="#MindPage", ms=60000) => `<span class="bounce" _time="${ms}" _search="${search}" _cached>${text}</span>`
function _on_welcome() {
document.head.insertAdjacentHTML("beforeend", "<style>" +
_this.read('css') + "</style>") // see below
document.querySelectorAll('.bounce').forEach((elem)=>{
setTimeout(()=>{
elem.classList.add('stop')
}, parseInt(elem.getAttribute('_time')))
})
}
function _on_search(text) {
document.querySelectorAll('.bounce').forEach((elem)=>{
if (text.toLowerCase().startsWith(
elem.getAttribute('_search').toLowerCase())) {
elem.style.opacity = 0
elem.classList.remove('bounce')
}
})
}
function _on_create(text) {}
@keyframes bounce {
0% { transform: translateX(0) scale(1); color: #aaa; }
100% { transform: translateX(25px) scale(1.3); color: #fff; }
}
.bounce {
display: inline-block;
animation-duration: .35s;
animation-name: bounce;
animation-iteration-count: 100;
animation-direction: alternate;
animation-timing-function: ease-out;
}
.bounce.stop {
animation-name: none;
}
#_welcome #_listen
#charts can be generated using #macros such as #macros/bar_chart #_macros/bar_chart:
<<bar_chart([['y1',4,2,5,10],['y2',2,3,6,12]],'height:200px')>>
Alternatively, use #c3 directly from an _html block:
<div id="chart-$id" class="c3" style="height:160px"><script>
c3._chart('#chart-$id', {
data: { columns: [['y1',4,2,6,10],['y2',2,3,7,12]] },
axis: {
x: { padding: .1 },
y: {
min:0, tick: { values:[0, 5,10] },
padding: { bottom:0, top:10 }
}
}
});
</script></div>
#load function loads external libraries. Examples: #mathjs #jStat #webppl #TensorFlow #gapi, ...
await _load(
window.lib1 || "url1", // load only if !window.lib1
window.lib2 || "url2", // load only if !window.lib2
// custom_init(),
)
// ... post-load init
// Alternative Usage (assuming lib1 primary)
if (!window.lib1) { // skip load+init if lib1 loaded
await _load("url1", "url2", /* custom_init() */)
// ... post-load init
}
// loads given url string(s)
// flattens out arrays, skips non-strings
// resolves any Promise arguments simultaneously with url loads
// (allows simultaneous custom init besides url loading)
window._load = (...urls) =>
Promise.all(_.flattenDeep([...urls])
.filter((u)=>(typeof u === 'string')).map(
(src) => new Promise((resolve, reject) => {
const start = Date.now()
console.debug(`loading url '${src}' ...`)
let script = document.createElement('script')
script.src = src
script.onload = () => {
console.debug(`loaded url '${src}' in ${Date.now()-start}ms`)
resolve()
}
script.onerror = reject
document.head.appendChild(script)
})).concat(_.flattenDeep([...urls])
.filter((u)=>u instanceof Promise)))
#mathjs/example #_mathjs
return math.stirlingS2(10,5)
42525
#jStat/example #_jStat
return jStat.normal(0,1).sample()
-1.138345570479304
#MindPage/core consists of #/properties, #/functions, #/commands, and #preloaded libraries that you can use to build your own #macros and #commands. You can also #load any external libraries that you need. Full source code is available at https://github.com/olcan/mind.page. #_context
#code can be inline or inside blocks:
const say_hello = () => "hello world!"
#_hidden tags.#features/_welcome tag enables "welcome" code to be evaluated after the initial page has been rendered and any welcome modal has been dismissed.
_on_welcome() is evaluated once at welcome time._welcome functions require reload.function _welcome() { alert("Welcome to MindPage!") }
#graphviz is a graph visualization library #preloaded via its js/wasm port and extended here with graphviz._graph, the preferred constructor for graphviz graphs on MindPage.
function _init() {
window.graphviz = window["@hpcc-js/wasm"].graphviz
graphviz._graph = graphviz_graph; // see below
document.head.insertAdjacentHTML("beforeend", "<style>" +
_this.read('css') + "</style>") // see below
}
.dot {
padding: 5px 0;
width: fit-content; /* left-aligns graph if width:auto */
display: inline-block;
vertical-align: top;
cursor: default;
}
.dot > svg {
display: block;
height: 100%;
width: auto;
max-width: 100%;
margin: auto;
}
.dot .graph text { stroke: none !important }
.dot .node, .dot .edge { cursor: pointer }
// best way to define defaults for dot graphs is to insert dot attributes, which are turned into SVG attributes by Graphviz (hpcc-js/wasm), which then take lowest priority and can be easily modified either in the dot code or using CSS
// for layouts, see https://www.graphviz.org/documentation/
function graphviz_graph(selector, dot, layout = "dot") {
const nodedefs = `color="#999999", fontcolor="#999999", fontname="Avenir Next, Helvetica", fontsize=20, shape=circle, fixedsize=true`;
const edgedefs = `color="#999999", fontcolor="#999999", fontname="Avenir Next, Helvetica",penwidth=1`;
const graphdefs = `bgcolor=invis; color="#666666"; fontcolor="#666666"; fontname="Avenir Next, Helvetica"; fontsize=20; nodesep=.2; ranksep=.3; node[${nodedefs}]; edge[${edgedefs}]`;
const subgraphdefs = `labeljust="r"; labelloc="b"; edge[minlen=2]`;
dot = dot.replace(/(subgraph.*?{)/g, `$1\n${subgraphdefs};\n`);
dot = dot.replace(/(graph.*?{)/g, `$1\n${graphdefs};\n`);
graphviz.layout(dot, "svg", layout).then(_that.attach(svg => {
let elem = document.querySelector(selector);
if (!elem) {
// console.error("missing _graph elem", selector)
_that.invalidate_elem_cache()
return
}
elem.innerHTML = svg;
// declare nodes/edges clickable and block default handlers
elem.setAttribute("_clickable", "")
const clickables = '.node,.edge,.cluster'
elem._clickable = (e) => {
e.stopPropagation()
return e.target.closest(clickables)
}
// NOTE: additional handlers can prevent errors e.g. for long clicks
elem.onmousedown = (e) => {
if (e.target.closest(clickables)) e.stopPropagation() }
elem.onmouseup = (e) => {
if (e.target.closest(clickables)) e.stopPropagation() }
elem.onclick = _that.attach((e) => {
if (!e.target.closest(clickables)) return
e.stopPropagation()
_that.touch()
let text, title
if (e.target.closest('a')) { // extract xlink:title attribute
text = e.target.closest('a')
.querySelector('text')?.textContent
title = e.target.closest('a').getAttribute('xlink:title')
} else { // extract <title> tag
text = e.target.closest(clickables)
.querySelector('text')?.textContent
title = e.target.closest(clickables)
.querySelector('title')?.textContent
}
if (text && title && text != title) _modal(text+': '+title)
else if (text || title) _modal(text || title)
})
// dispatch to check rendering if element
setTimeout(_that.attach(()=>{
if (!_that.elem?.contains(elem)) {
// console.error("detached _graph elem", selector)
_that.invalidate_elem_cache()
return
}
// _dot_rendered renders $math$ (via #mathjax)
// and implements class=stack for clusters
_dot_rendered(_that, elem)
}))
}))
}
#_init
#graphs #_macros/graph can be generated using #macros/graph:
<<graph('graph{a--b}')>>
<<graph('digraph{rankdir=LR;a->c;b->c}')>>
<<graph('digraph{a->c;b->c;c->c;b->d}')>>
Alternatively, use #graphviz directly from an _html block:
<div id="dot-$cid" class="dot" style="height:120px"><script>
graphviz._graph('#dot-$cid', _this.read('dot')) // see dot block below
</script></div>
digraph {
rankdir=LR;
x1->x2->x3->x4;
x1->x3; x2->x4;
x1[label="$x_1$"];
x2[label="$x_2$"];
x3[label="$x_3$"];
x4[label="$x_4$"];
}
#private means that your items are readable only by you, on your devices. This is achieved using encryption based on a personal secret that is never sent or stored anywhere, as can be verified from open source code. This is much more private than using authentication alone, which still leaves your content readable to service providers, who often also use closed source code that can not be verified.
#Items are small units of content ordered dynamically by recency and relevance. Create items using create button or #⇧⏎ shortcut. Organize by #/naming or tagging. #_context
intro #todo #log #_menu #_pin/dot/2
#items/ordering is determined primarily by recency as you edit or interact with items, and relevance as you type into #MindBox.
#MindPage/core/functions/_modal opens a modal window for basic confirmation and input prompts. Programmable click handlers can be used to trigger certain actions that can otherwise be disallowed in some browsers (e.g. iOS Safari), such as:
window.open for API authorization (OAuth) flows, e.g. #gapi.HTMLElement.focus to focus on input elements, e.g. #/example:_modal_close([modal],[output]) and _modal_update(modal, options). Visibility can be checked using _modal_visible([modal]).alert/confirm/prompt dialogs not working after using browser history (e.g. going back/forward) in iOS. _modal_alert(msg, [done]), _modal_confirm(msg, [confirmed], [cancelled]), and _modal_prompt(msg, [default], [confirmed], [cancelled]) can be used as convenient async replacements.Welcome <<_user.name>> to your MindPage!
Edit: just tap on an item. Try this one!
Create: tap button above or use ⇧⏎ shortcut.
Learn more: review intro or cheat sheet.
Sign out: tap on your image <<_user.image>> above.
#items/naming is based on starting #name tags.
#items/naming.#⇧⏎ is the universal shortcut to create or save items in MindPage. You can also use the corresponding buttons indicated in <font color=#6f6>green</font>.
#MindPage/core/functions/_modal/example shows a modal that triggers MindBox.focus() from its confirmation click handler, as required by some browsers (e.g. iOS Safari) that otherwise block keyboard focus actions.
_modal({
content: "This is an example modal dialog.",
confirm: "Focus on MindBox",
cancel: "Cancel",
background: "cancel",
onConfirm: () => MindBox.focus()
})
#features/_init tag enables initialization code.
_init() function is evaluated once during page load.js_init block if it exists (otherwise from js)._init functions require reload to take effect.function _init() { console.log("Welcome to MindPage!") }
#text can be formatted with #/emphasis, #/lists, #/headings, #/blockquotes, and most other Markdown syntax.
#commands/invert command <<cmdlink('/invert')>> inverts colors persistently.
const run = () => {
document.documentElement.classList.toggle("invert",
_this.local_store.invert = !_this.local_store.invert)
}
function _init() {
document.head.insertAdjacentHTML("beforeend", "<style>" +
_this.read('css') + "</style>") // see below
document.documentElement.classList.toggle("invert",
_this.local_store.invert == true)
}
html.invert { filter: invert(100%) }
html.invert img { filter: invert(100%) }
html.invert textarea { caret-color: #0ff; } /* keep red caret */
#_init
#commands/alert command <<cmdlink('/alert text')>> displays an alert box with given text.
function run(text) { alert(text); return '/alert ' }
#commands/hello_again command <<cmdlink('/hello_again')>> creates an item named #hello and then writes output and log messages into it.
const run = () => ({
text:'#hello world!',
edit:false, // no editing
init:(item) => {
console.log("initing item ", item.name);
item.invoke(()=>{ console.log("hello world!") })
item.write('hello world!') // write _output
item.write_log() // write _log
}
})
#commands/hello command <<cmdlink('/hello')>> creates an item to say hello.
const run = () => ({text:`hello world!`})
2/13/2021 9:00:00 PM Weekly Review 2/14/2021 11:30:00 PM Copleri hazirla 2/17/2021 1:30:00 PM Temizlik 2/20/2021 9:00:00 PM Weekly Review 2/21/2021 11:30:00 PM Copleri hazirla
#commands/debug creates a debug version of an item to help debug a recent evaluation performed on that item.
function run(args) {
// parse args as: /debug name [trigger = run]
let name, trigger;
({name, trigger} = _.merge({ trigger: "run" },
args.match(/^(?<name>\S+)\s*(?<trigger>\S+)?\s*$/)?.groups))
if (!name) {
alert("usage: /debug name [trigger = run]");
return "/debug ";
}
const item = _item(name)
const text = item.debug_store[trigger];
if (!text) {
const triggers = _.keys(item.debug_store)
alert(`/debug: eval trigger '${trigger}' not found for item '${name}'; available triggers are: ` + triggers.join(", "))
return `/debug ${args}`;
}
return {text:"#_debug " + text, run:false}
}
#MindPage/core/properties are:
window): #/_user #/_stack #/_this #/_that_item(name), _items(label), #/_this, #/_that, #/_stack:id |
<<code(_this.id)>> |
name |
<<code(_this.name)>> |
label |
<<code(_this.label)>> |
time |
<<code(_this.time)>> |
text |
... |
length |
<<code(_this.length)>> |
hash |
<<code(_this.hash)>> |
deephash |
<<code(_this.deephash)>> |
async |
<<code(_this.async)>> |
deepasync |
<<code(_this.deepasync)>> |
index |
<<code(_this.index)>> |
position |
<<code(_this.position)>> |
tags |
<<_this.tags.map(code).join(' ')>> |
tags_raw |
<<_this.tags_raw.map(code).join(' ')>> |
tags_visible |
<<_this.tags_visible.map(code).join(' ')>> |
tags_hidden |
<<_this.tags_hidden.map(code).join(' ')>> |
dependencies |
<<_this.dependencies.map(code).join(' ')>> |
dependents |
<<_this.dependents.map(code).join(' ')>> |
running |
<<code(_this.running)>> |
status |
<<code(_this.status)>> |
progress |
<<code(_this.progress)>> |
saving |
<<code(_this.saving)>> |
saving_global_store |
<<code(_this.saving_global_store)>> |
saved_id |
<<code(_this.saved_id)>> |
editing |
<<code(_this.editing)>> |
editable |
<<code(_this.editable)>> |
pushable |
<<code(_this.pushable)>> |
attr |
<<code(_this.attr)>> |
elem |
<<code(_this.elem)>> |
log_options |
<<code(JSON.stringify(_this.log_options))>> |
read_only |
<<code(_this.read_only)>> |
store |
<<code(JSON.stringify(_this.store))>> |
local_store |
<<code(JSON.stringify(_this.local_store))>> |
global_store |
<<code(JSON.stringify(_this.global_store))>> |
debug_store |
<<code(_this.debug_store)>> (can be large) |
cache |
<<code(JSON.stringify(_this.cache))>> |
#MindPage/core/commands are:
_signin new user._signout current user._undelete recently deleted items (in current session)._duplicate selected (editing) item._dependencies of selected (editing) item, listed in new item._dependents of selected (editing) item, listed in new item._invert colors._zoom [factor] page._reload page.#text/blockquotes
this is
a blockquotethat can be nested
inside another blockquote
#jStat #_load #_async is a statistics library. See #jStat/example.
if (!window.jStat) {
await _load('https://cdn.jsdelivr.net/npm' +
'/jstat@1.9.5/dist/jstat.min.js')
}
#Dependencies are created using hidden tags.
···) under item. Tap for listing.#code/run code inside js_input blocks using blue run button or ⌥⏎ while editing. #_code
console.log(say_hello()) // goes into _log block
say_hello() // goes into _output block
hello world!
hello world!
#features/_debug tag disables all code wrapping (e.g. for #features/_async) to enable debugging commands such as #commands/debug.
#features/_context tag enables item to be brought up as context for other items that are tagged in it. This feature is similar to using nested names but allows arbitrary names as long as they are unique.
#features/_menu tag enables special styling where tags and links expand to make them as large as possible for easy tapping.
#features/_pin tag pins item at the top.
#_pin/... can be used for natural ordering.#_context/pin/... can be used to specify context.#items/tagging is based on #hashtags.
[text](#tag).#_tag.#features/log label designates log items:
#log or contain hidden #_log.Yet another example #todo item.
#todo This is another example todo item.
#todo tag is commonly used as a label for todo items. This is an example.
#math can be inline: $e^x=\sum_{k=a}^\infty \frac{x^k}{k!}$, between lines:
$$e^x=\sum_{k=a}^\infty \frac{x^k}{k!}$$,
in blockquotes:
$$
e^x=\sum_{k=a}^\infty \frac{x^k}{k!}$$,
or inside_mathblocks:
e^x=\sum_{k=a}^\infty \frac{x^k}{k!}
\$`...`$ for inline math and \$$`...`$$ display math.#Key_symbols
| Symbol | Key |
|---|---|
| ⇧ | Shift |
| ⏎ | Return (Enter) |
| ⌘ | Command (Windows) |
| ⌃ | Control |
| ⌥ | Option (Alt) |
| ⇥ | Tab |
| ⌫ | Backspace (Delete) |
| ⎋ | Escape |
| ↑ | Up Arrow |
| ↓ | Down Arrow |
#webppl/util utility functions for #webppl.
var _enumerate = function(func) { Infer({method:'enumerate'}, func) }
const webppl_samples = (dist) => dist.samples.map((s)=>s.value)
#MindPage/core/properties/_that is the currently evaluating item at the bottom of the evaluation stack, equivalent to _item(_stack[0]) (see stack). For example, below macros evaluate _that.name vs _this.name on #MindPage:
_that.name = <<_item('#MindPage').eval('_that.name')>>_this.name = <<_item('#MindPage').eval('_this.name')>>#MindPage/core/properties/_stack returns the current evaluation stack. For example, below is a macro that evaluates _stack on #MindPage:
<<_item('#MindPage').eval('json(_stack.map((id)=>_item(id).name))')>>`
#macros/graph #_graphviz macro generates a graph using #graphviz.
function graph(dot, style) {
return html(_item('$id').read('html') // from html block (edit to see)
.replaceAll('%dot', dot)
.replaceAll('%style', style))
}
<!-- *_removed block is not included in rendered item -->
<!-- $cid is a unique content id based on macro usage -->
<div id="graph-$cid" class="dot" style="%style"><script>
graphviz._graph('#graph-$cid', `%dot`)
</script></div>
#macros/bar_chart macro generates a simple bar chart using #c3.
function bar_chart(data, style='', bar_colors={}) {
if (data=="") return ""
return html(_item('$id').read('html') // from html block (edit to see)
.replaceAll('%data', typeof data == 'string' ?
data : JSON.stringify(data))
.replaceAll('%style', style)
.replaceAll('%bar_colors', JSON.stringify(bar_colors)))
}
<!-- *_removed block is not included in rendered item -->
<!-- $cid is a unique content identifier based on macro usage -->
<!-- c3._chart is defined in item #c3 referenced/imported above -->
<div id="bar_chart-$cid" class="c3" style="%style"><script>
const data = JSON.parse(`%data`)
const columns = Array.isArray(data) ? data :
[['x',..._.keys(data)], ['y',..._.values(data)]]
const x = columns[0][0] == 'x' ? 'x' : ''
c3._chart('#bar_chart-$cid', {
data: { columns, x, type:'bar', colors:JSON.parse(`%bar_colors`) },
axis: {
rotated:true,
x: { type:'category', tick: { multiline:false } },
y: { show: false } // visible in tooltips
},
padding: { bottom: 7 }, // helps center rotated bar charts
legend: { show: (columns.length - (x?1:0) > 1) }
})
</script></div>
#MindPage/core/properties/_user is the current user:
<<json(_user)>>
#MindPage/core/properties/_this is the currently evaluating item at the top of the evaluation stack, equivalent to _item(_.last(_stack)) (see stack). For example, below is the macro \<<json(_this)>> evaluated as this item is rendered:
<<json(_this)>>
#flat names are not nested.
back button to restore a previous context.#log This is an example log item. Editing this item will not move it up in time.
#more types of content is possible using #preloaded and dynamically loaded libraries such as #mathjs, #jstat, #tensorflow, #webppl, ...
#webppl/summarize #_code/util outputs most likely (discrete) values and their probabilities in in a format suitable for charting, e.g. using #macros/bar_chart.
function webppl_summarize(dist, prior=null, limit=10, format=".2f") {
dist = dist.getDist() // get value:prob object
const vJ = _.keys(dist).map((v) => v.toString())
const pJ = _.values(dist).map((p) => p.prob).map(d3.format(format))
let jJ = _.sortBy(_.range(pJ.length), (j) => -pJ[j])
jJ = _.take(jJ.filter((j) => pJ[j] > 0), limit)
let columns = _.zip(['x','p'], ...jJ.map((j)=>[vJ[j],pJ[j]]))
if (prior) {
prior = prior?.getDist()
const qJ = jJ.map((j) => prior[vJ[j]]?.prob || "0")
columns.push(['prior', ...qJ.map(d3.format(format))])
columns[1][0] = 'posterior'
}
return columns
}
#text/headings
#preloaded libraries are #lodash, #mathjax, #c3, #d3, and #graphviz. You can #load other libraries as needed.
#code/util
// from https://stackoverflow.com/a/175787
function isNumeric(str) {
if (typeof str != "string") return false
return !isNaN(str) && // require _entire_ string to be numeric
!isNaN(parseFloat(str)) // handle whitespace-only strings
}
#lodash is a utility library #preloaded as _ (or window._)
#mathjs #_load #_async is a math library. See #mathjs/example.
if (!window.math) {
await _load('https://cdn.jsdelivr.net/npm' +
'/mathjs@9.4.4/lib/browser/math.min.js')
}
#d3 is a data visualization library #preloaded as d3 (or window.d3).
#mathjax is a math rendering library #preloaded as MathJax (or window.MathJax).
#text/emphasis can be italic, bold, italic bold, or monospaced.
#text/lists