diff --git a/README.md b/README.md
index 5dc758987..c0878a928 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,7 @@ A project or branch of a project on GitHub can be checked out in multiple direct
- [lively4-core](https://lively-kernel.org/lively4/lively4-core/start.html)
- [lively4-stable](https://lively-kernel.org/lively4/lively4-stable/start.html)
- [lively4-jens](https://lively-kernel.org/lively4/lively4-jens/start.html)
+- [aexpr](https://lively-kernel.org/lively4/aexpr/start.html)
The Lively4 server and GitHub sync tools can check out arbitrary projects, such as the code of [lively4-server](https://lively-kernel.org/lively4/lively4-server/) itself, or the source of a paper hosted by overleaf.
@@ -35,4 +36,4 @@ The Lively4 server and GitHub sync tools can check out arbitrary projects, such
- Reactive Programming [RP 2018](https://lively-kernel.org/lively4/lively4-seminars/RP2018/index.md)
-### [Imprint](imprint.md)
\ No newline at end of file
+### [Imprint](imprint.md)
diff --git a/demos/stefan/untitled-board-game/ubg-card.js b/demos/stefan/untitled-board-game/ubg-card.js
index ab5a8bed3..645ae396f 100644
--- a/demos/stefan/untitled-board-game/ubg-card.js
+++ b/demos/stefan/untitled-board-game/ubg-card.js
@@ -90,10 +90,23 @@ export default class Card {
this.ensureUnprintedVersion();
if (notes === undefined) {
- delete this.versions.last.notes;
+ delete this.notes;
} else {
this.notes = notes;
- this.versions.last.notes = notes;
+ }
+ }
+
+ getRating() {
+ return this.versions.last.rating;
+ }
+
+ setRating(rating) {
+ this.ensureUnprintedVersion();
+
+ if (rating === undefined || rating === 'unset') {
+ delete this.versions.last.rating;
+ } else {
+ this.versions.last.rating = rating;
}
}
diff --git a/demos/stefan/webxr/README.md b/demos/stefan/webxr/README.md
new file mode 100644
index 000000000..3f0e0f681
--- /dev/null
+++ b/demos/stefan/webxr/README.md
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+# Immersive VR Session
+
+
+
+ Immersive VR Session
+
+ This sample demonstrates how to use an 'immersive-vr' XRSession to
+ present a simple WebGL scene to an XR device. The scene is not
+ rendered to the page.
+ Back
+
+
+
+
+
Click 'Enter XR' to see content
+
+
diff --git a/demos/stefan/webxr/webxr.js b/demos/stefan/webxr/webxr.js
new file mode 100644
index 000000000..05bd91ec0
--- /dev/null
+++ b/demos/stefan/webxr/webxr.js
@@ -0,0 +1,173 @@
+import {WebXRButton} from 'https://raw.githubusercontent.com/immersive-web/webxr-samples/main/js/util/webxr-button.js';
+import {Scene} from 'https://raw.githubusercontent.com/immersive-web/webxr-samples/main/js/render/scenes/scene.js';
+import {Renderer, createWebGLContext} from 'https://raw.githubusercontent.com/immersive-web/webxr-samples/main/js/render/core/renderer.js';
+import {Gltf2Node} from 'https://raw.githubusercontent.com/immersive-web/webxr-samples/main/js/render/nodes/gltf2.js';
+import {SkyboxNode} from 'https://raw.githubusercontent.com/immersive-web/webxr-samples/main/js/render/nodes/skybox.js';
+import {QueryArgs} from 'https://raw.githubusercontent.com/immersive-web/webxr-samples/main/js/util/query-args.js';
+
+// If requested, use the polyfill to provide support for mobile devices
+// and devices which only support WebVR.
+import WebXRPolyfill from 'https://raw.githubusercontent.com/immersive-web/webxr-samples/main/js/third-party/webxr-polyfill/build/webxr-polyfill.module.js';
+if (QueryArgs.getBool('usePolyfill', true)) {
+ let polyfill = new WebXRPolyfill();
+}
+
+// XR globals.
+let xrButton = null;
+let xrRefSpace = null;
+
+// WebGL scene globals.
+let gl = null;
+let renderer = null;
+let scene = new Scene();
+scene.addNode(new Gltf2Node({url: 'https://raw.githubusercontent.com/immersive-web/webxr-samples/main/media/gltf/space/space.gltf'}));
+scene.addNode(new SkyboxNode({url: 'https://raw.githubusercontent.com/immersive-web/webxr-samples/main/media/textures/milky-way-4k.png'}));
+
+// Checks to see if WebXR is available and, if so, queries a list of
+// XRDevices that are connected to the system.
+export default function initXR(context) {
+ // Adds a helper button to the page that indicates if any XRDevices are
+ // available and let's the user pick between them if there's multiple.
+ xrButton = new WebXRButton({
+ onRequestSession: onRequestSession,
+ onEndSession: onEndSession
+ });
+ lively.query(context, 'header').appendChild(xrButton.domElement);
+
+ // Is WebXR available on this UA?
+ if (navigator.xr) {
+ // If the device allows creation of exclusive sessions set it as the
+ // target of the 'Enter XR' button.
+ navigator.xr.isSessionSupported('immersive-vr').then((supported) => {
+ xrButton.enabled = supported;
+ });
+ }
+}
+
+// Called when the user selects a device to present to. In response we
+// will request an exclusive session from that device.
+function onRequestSession() {
+ return navigator.xr.requestSession('immersive-vr').then(onSessionStarted);
+}
+
+// Called when we've successfully acquired a XRSession. In response we
+// will set up the necessary session state and kick off the frame loop.
+function onSessionStarted(session) {
+ // This informs the 'Enter XR' button that the session has started and
+ // that it should display 'Exit XR' instead.
+ xrButton.setSession(session);
+
+ // Listen for the sessions 'end' event so we can respond if the user
+ // or UA ends the session for any reason.
+ session.addEventListener('end', onSessionEnded);
+
+ // Create a WebGL context to render with, initialized to be compatible
+ // with the XRDisplay we're presenting to.
+ gl = createWebGLContext({
+ xrCompatible: true
+ });
+
+ // Create a renderer with that GL context (this is just for the samples
+ // framework and has nothing to do with WebXR specifically.)
+ renderer = new Renderer(gl);
+
+ // Set the scene's renderer, which creates the necessary GPU resources.
+ scene.setRenderer(renderer);
+
+ // Use the new WebGL context to create a XRWebGLLayer and set it as the
+ // sessions baseLayer. This allows any content rendered to the layer to
+ // be displayed on the XRDevice.
+ session.updateRenderState({ baseLayer: new XRWebGLLayer(session, gl) });
+
+ // Get a frame of reference, which is required for querying poses. In
+ // this case an 'local' frame of reference means that all poses will
+ // be relative to the location where the XRDevice was first detected.
+ session.requestReferenceSpace('local').then((refSpace) => {
+ xrRefSpace = refSpace;
+
+ // Inform the session that we're ready to begin drawing.
+ session.requestAnimationFrame(onXRFrame);
+ });
+}
+
+// Called when the user clicks the 'Exit XR' button. In response we end
+// the session.
+function onEndSession(session) {
+ session.end();
+}
+
+// Called either when the user has explicitly ended the session (like in
+// onEndSession()) or when the UA has ended the session for any reason.
+// At this point the session object is no longer usable and should be
+// discarded.
+function onSessionEnded(event) {
+ xrButton.setSession(null);
+
+ // In this simple case discard the WebGL context too, since we're not
+ // rendering anything else to the screen with it.
+ renderer = null;
+}
+
+// Called every time the XRSession requests that a new frame be drawn.
+function onXRFrame(t, frame) {
+ let session = frame.session;
+
+ // Per-frame scene setup. Nothing WebXR specific here.
+ scene.startFrame();
+
+ // Inform the session that we're ready for the next frame.
+ session.requestAnimationFrame(onXRFrame);
+
+ // Get the XRDevice pose relative to the Frame of Reference we created
+ // earlier.
+ let pose = frame.getViewerPose(xrRefSpace);
+
+ // Getting the pose may fail if, for example, tracking is lost. So we
+ // have to check to make sure that we got a valid pose before attempting
+ // to render with it. If not in this case we'll just leave the
+ // framebuffer cleared, so tracking loss means the scene will simply
+ // disappear.
+ if (pose) {
+ let glLayer = session.renderState.baseLayer;
+
+ // If we do have a valid pose, bind the WebGL layer's framebuffer,
+ // which is where any content to be displayed on the XRDevice must be
+ // rendered.
+ gl.bindFramebuffer(gl.FRAMEBUFFER, glLayer.framebuffer);
+
+ // Clear the framebuffer
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
+
+ // Loop through each of the views reported by the frame and draw them
+ // into the corresponding viewport.
+ for (let view of pose.views) {
+ let viewport = glLayer.getViewport(view);
+ gl.viewport(viewport.x, viewport.y,
+ viewport.width, viewport.height);
+
+ // Draw this view of the scene. What happens in this function really
+ // isn't all that important. What is important is that it renders
+ // into the XRWebGLLayer's framebuffer, using the viewport into that
+ // framebuffer reported by the current view, and using the
+ // projection matrix and view transform from the current view.
+ // We bound the framebuffer and viewport up above, and are passing
+ // in the appropriate matrices here to be used when rendering.
+ scene.draw(view.projectionMatrix, view.transform);
+ }
+ } else {
+ // There's several options for handling cases where no pose is given.
+ // The simplest, which these samples opt for, is to simply not draw
+ // anything. That way the device will continue to show the last frame
+ // drawn, possibly even with reprojection. Alternately you could
+ // re-draw the scene again with the last known good pose (which is now
+ // likely to be wrong), clear to black, or draw a head-locked message
+ // for the user indicating that they should try to get back to an area
+ // with better tracking. In all cases it's possible that the device
+ // may override what is drawn here to show the user it's own error
+ // message, so it should not be anything critical to the application's
+ // use.
+ }
+
+ // Per-frame scene teardown. Nothing WebXR specific here.
+ scene.endFrame();
+}
diff --git a/src/components/widgets/ubg-cards-editor.html b/src/components/widgets/ubg-cards-editor.html
index 9cd7e3294..136d8dc34 100644
--- a/src/components/widgets/ubg-cards-editor.html
+++ b/src/components/widgets/ubg-cards-editor.html
@@ -128,7 +128,7 @@
#form-layout {
display: grid;
grid-template-columns: min-content auto 2.5in;
- grid-template-rows: repeat(7, auto) 1fr auto .5fr auto;
+ grid-template-rows: repeat(7, auto) 1fr auto auto .5fr auto;
grid-template-areas:
"isPrinted-key isPrinted-value preview"
"id-key id-value preview"
@@ -139,6 +139,7 @@
"vp-key vp-value preview"
"text-key text-value preview"
"tags-key tags-value preview"
+ "rating-key rating-value preview"
"notes-key notes-value preview"
"art-key art-value preview"
;
@@ -217,6 +218,11 @@
.tag:focus {
background: #32cd3280;
}
+ #rating {
+ height:
+ }
+
+
is printed
@@ -235,14 +241,26 @@
text
+ tags
+ rating
+
notesart
- tags
+
diff --git a/src/components/widgets/ubg-cards-editor.js b/src/components/widgets/ubg-cards-editor.js
index 47500a362..e8a356521 100644
--- a/src/components/widgets/ubg-cards-editor.js
+++ b/src/components/widgets/ubg-cards-editor.js
@@ -30,7 +30,15 @@ export default class UBGCardsEditor extends Morph {
}
this.$text.addEventListener('keydown', evt => this.keydown$text(evt), false);
this.$tagsInput.addEventListener('keydown', evt => this.keydown$tagInput(evt), false);
-
+ this.get('#rating').addEventListener('change', evt => {
+ if (evt.target.name === 'rating') {
+ this.modify$rating(evt)
+ }
+ });
+ }
+
+ initSlider() {
+
}
get ubg() {
@@ -227,6 +235,9 @@ export default class UBGCardsEditor extends Morph {
get $tagsList() {
return this.get('#tags-list');
}
+ get $rating() {
+ return this.get('#rating');
+ }
get $notes() {
return this.get('#notes');
}
@@ -497,6 +508,27 @@ export default class UBGCardsEditor extends Morph {
}
}
+ modify$rating(evt) {
+ const rating = evt.target.value;
+ if (rating === '') {
+ this.card.setRating();
+ } else {
+ this.card.setRating(rating);
+ }
+
+ this.propagateChange()
+ }
+ display$rating() {
+ const rating = this.card.getRating() || 'unset';
+
+ const selectedOption = this.$rating.querySelector(`[value='${rating}']`)
+ if (selectedOption) {
+ selectedOption.checked = true;
+ } else {
+ lively.warn('Unknown rating ' + rating)
+ }
+ }
+
modify$notes(evt) {
const notes = this.$notes.value;
if (notes === '') {
@@ -570,6 +602,7 @@ export default class UBGCardsEditor extends Morph {
this.display$vp();
this.display$text();
this.display$tags();
+ this.display$rating();
this.display$notes();
this.display$art();
this.display$isPrinted();
diff --git a/src/components/widgets/ubg-cards-entry.js b/src/components/widgets/ubg-cards-entry.js
index 8d84c85f3..011fa7e9c 100644
--- a/src/components/widgets/ubg-cards-entry.js
+++ b/src/components/widgets/ubg-cards-entry.js
@@ -116,7 +116,13 @@ export default class UBGCardEntry extends Morph {
const v = card.versions.last;
- this.get('#id').innerHTML = card.id || '???';
+ const id = this.get('#id')
+ id.style.borderLeft = '5px solid ' + ({
+ keep: 'green',
+ unsure: 'yellow',
+ remove: 'red',
+ }[card.getRating()] || 'gray');
+ id.innerHTML = card.id || '???';
const type = v.type && v.type.toLowerCase();
this.get('#type').className = {
diff --git a/src/components/widgets/ubg-cards.js b/src/components/widgets/ubg-cards.js
index f5ac00032..eb4fc9289 100644
--- a/src/components/widgets/ubg-cards.js
+++ b/src/components/widgets/ubg-cards.js
@@ -235,6 +235,52 @@ const castIcon = do {
${mainElements}`, bounds);
}
+
+const hedronSVG = do {
+ function point(pt) {
+ return `${pt.x} ${pt.y}`;
+ }
+
+ const topB = lively.pt(11.5, 14.401);
+ const topL = topB.addXY(-11.5, -4.758);
+ const topT = topL.addXY(11.5, -9.66);
+ const topR = topT.addXY(11.5, 9.66);
+ const topB2 = topR.addXY(-11.5, 4.758);
+ const topLeftData = `M${point(topB)} L ${point(topL)} ${point(topT)} z`;
+ const topRightData = `M${point(topB)} L ${point(topT)} ${point(topR)} z`;
+
+ const bottomB = lively.pt(11.5, 16.036);
+ const bottomL = bottomB.addXY(-11.5, -5.050);
+ const bottomT = bottomL.addXY(11.5, 12.030);
+ const bottomR = bottomT.addXY(11.5, -12.030);
+ const bottomB2 = bottomR.addXY(-11.5, 5.050);
+ const bottomLeftData = `M${point(bottomB)} L ${point(bottomL)} ${point(bottomT)} z`;
+ const bottomRightData = `M${point(bottomB)} L ${point(bottomT)} ${point(bottomR)} ${point(bottomB2)} z`;
+
+ ;
+};
+
+{
+ const hedronTemp = document.getElementById('hedron')
+ if (hedronTemp) {
+ hedronTemp.remove()
+ }
+ document.body.insertAdjacentHTML("afterbegin", hedronSVG.outerHTML)
+}
+
+
class FileCache {
constructor() {
@@ -364,12 +410,15 @@ ${SVG.elementSymbol(others[2], lively.pt(12.5, 8.5), 1.5)}`, lively.rect(0, 0, 1
printedRules = printedRules.replace(/manaCost(fire|water|earth|wind|gray)/gmi, (match, pElement, offset, string, groups) => {
return this.manaCost(pElement);
});
+
printedRules = this.renderElementIcon(printedRules)
printedRules = this.renderVPIcon(printedRules)
printedRules = this.renderCoinIcon(printedRules)
printedRules = this.renderBracketIcon(printedRules)
+ printedRules = this.renderHedronIcon(printedRules)
+
return this.renderToDoc(ruleBox, insetTextBy, printedRules, beforeRenderRules, doc)
}
@@ -439,7 +488,7 @@ ${SVG.elementSymbol(others[2], lively.pt(12.5, 8.5), 1.5)}`, lively.rect(0, 0, 1
if (elements.length === 0 || (elements.length === 1 && elements.first === 'gray')) {
elementString = 'this card\'s element';
} else if (elements.length === 1) {
- elementString = elements.first;
+ elementString = elements.first;
} else {
elementString = `${elements.slice(0, -1).join(', ')} or ${elements.last}`;
}
@@ -516,11 +565,16 @@ ${SVG.elementSymbol(others[2], lively.pt(12.5, 8.5), 1.5)}`, lively.rect(0, 0, 1
return 'To cycle a card, trash it to gain a card of equal or lower cost.'
},
- cycling: (cost) => {
+ cycling: (cost, who) => {
+ let whoToPrint = 'this'
+ if (who === 'acard') {
+ whoToPrint = 'a card'
+ }
+
if (cost) {
- return `Passive As a free action, you may pay (${cost}) and trash this to gain a card of equal or lower cost.`
+ return `Passive As a free action, you may pay (${cost}) and trash ${whoToPrint} to gain a card of equal or lower cost.`
}
- return `Passive As a free action, you may trash this to gain a card of equal or lower cost.`
+ return `Passive As a free action, you may trash ${whoToPrint} to gain a card of equal or lower cost.`
},
upgrade: (diff, who) => {
@@ -532,6 +586,10 @@ ${SVG.elementSymbol(others[2], lively.pt(12.5, 8.5), 1.5)}`, lively.rect(0, 0, 1
return `To upgrade, trash ${whoText} to gain a card costing up to (${diff}) more.`
},
+ discover: (howMany) => {
+ return `To discover ${howMany}, reveal top ${howMany} cards of any piles. Add 1 to your hand, trash the rest.`
+ },
+
evoke: (cost) => {
return `As a free action, pay (${cost}) and trash this from hand to exec its blitz effects.`
},
@@ -557,6 +615,14 @@ ${SVG.elementSymbol(others[2], lively.pt(12.5, 8.5), 1.5)}`, lively.rect(0, 0, 1
return printedRules.replace(/(fire|water|earth|wind|gray)/gmi, (match, pElement, offset, string, groups) => inlineElement(pElement));
}
+ static renderHedronIcon(printedRules) {
+ function inlineHedron() {
+ return SVG.inlineSVG(hedronSVG.innerHTML, lively.rect(0, 0, 23, 23), 'x="10%" y="10%" width="80%" height="80%"', '')
+ }
+
+ return printedRules.replace(/hedron/gmi, (match, pElement, offset, string, groups) => inlineHedron());
+ }
+
static renderVPIcon(printedRules) {
function printVP(vp) {
@@ -575,12 +641,16 @@ ${SVG.inlineSVG(`
static renderCoinIcon(printedRules) {
function coin(text) {
const center = lively.pt(5, 5);
+ let textToPrint = `${text}`;
+ if (text.includes('hedron')) {
+ textToPrint = text
+ }
return SVG.inlineSVG(`${SVG.circle(center, 5, `fill="goldenrod"`)}
${SVG.circleRing(center, 4.75, 5, `fill="darkviolet"`)}
-${text}`);
+${textToPrint}`);
}
- return printedRules.replace(/\(([*0-9xy+-]*)\)/gmi, function replacer(match, p1, offset, string, groups) {
+ return printedRules.replace(/\(((?:[*0-9xy+-]|hedron)*)\)/gmi, function replacer(match, p1, offset, string, groups) {
return coin(p1);
});
}
@@ -591,7 +661,7 @@ ${SVG.circleRing(center, 4.75, 5, `fill="darkviolet"`)}
return SVG.inlineSVG(`
-${text}`);
+${text}`, undefined, undefined, 'transform:scale(1);');
}
return printedRules.replace(/\[([*0-9x+-]*)\]/gmi, function replacer(match, p1, offset, string, groups) {
@@ -1864,6 +1934,9 @@ export default class Cards extends Morph {
if (cardDesc.hasTag('deprecated')) {
slash('#ff00ff', 2, lively.pt(2, 2))
}
+ if (cardDesc.getRating() === 'remove') {
+ slash('#999999', 5, lively.pt(-5, -5))
+ }
}
renderVersionIndicator(doc, cardDesc, outsideBorder) {