diff --git a/assets/berd-cursor-inverse.png b/assets/berd-cursor-inverse.png new file mode 100644 index 0000000..907878f Binary files /dev/null and b/assets/berd-cursor-inverse.png differ diff --git a/assets/berd-cursor-inverse.svg b/assets/berd-cursor-inverse.svg new file mode 100644 index 0000000..f397dcc --- /dev/null +++ b/assets/berd-cursor-inverse.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/source/arroost/components/dom.js b/source/arroost/components/dom.js index 01e346e..470784b 100644 --- a/source/arroost/components/dom.js +++ b/source/arroost/components/dom.js @@ -153,7 +153,7 @@ export class Dom extends Component { this.use(() => { const outOfView = this.outOfView.get() - container.style.display = outOfView ? "block" : "block" + container.style.display = outOfView ? "hidden" : "block" }, [this.outOfView, this.forceInView]) const element = this.getElement() diff --git a/source/arroost/components/infinite.js b/source/arroost/components/infinite.js index b78a961..bb328f0 100644 --- a/source/arroost/components/infinite.js +++ b/source/arroost/components/infinite.js @@ -1,4 +1,4 @@ -import { BLACK, RED, scale } from "../../../libraries/habitat-import.js" +import { BLACK, RED, rotate, scale } from "../../../libraries/habitat-import.js" import { shared } from "../../main.js" import { Dragging } from "../machines/input.js" import { Component } from "./component.js" @@ -8,12 +8,20 @@ import { Transform } from "./transform.js" import { Movement } from "./movement.js" import { Style } from "./style.js" import { c, getCell, getTemplate, iterateCells, iterateWires, t } from "../../nogan/nogan.js" -import { CHILD_SCALE, PARENT_SCALE, ZOOMING_IN_THRESHOLD, ZOOM_IN_THRESHOLD } from "../unit.js" +import { + CHILD_SCALE, + FULL, + PARENT_SCALE, + ZOOMING_IN_THRESHOLD, + ZOOM_IN_THRESHOLD, +} from "../unit.js" import { ArrowOfCreation } from "../entities/arrows/creation.js" -import { CELL_CONSTRUCTORS, WIRE_CONSTRUCTOR } from "./tunnel.js" +import { CELL_CONSTRUCTORS, Tunnel, WIRE_CONSTRUCTOR } from "./tunnel.js" import { Entity } from "../entities/entity.js" import { EASE, lerp } from "../../../libraries/lerp.js" import { Ellipse } from "../entities/shapes/ellipse.js" +import { ArrowOfConnection } from "../entities/arrows/connection.js" +import { ArrowOfDestruction } from "../entities/arrows/destruction.js" export function ilerp(x, a, b) { return (x - a) / (b - a) @@ -68,7 +76,7 @@ export class Infinite extends Component { // const parentBlur = lerp([0, 20], t, EASE.easeInOutExpo) this.dom?.style.opacity.set(childOpacity) - this.parent?.style.opacity.set(parentOpacity) + // this.parent?.style.opacity.set(parentOpacity) // this.dom?.style.blur.set(childBlur) // this.parent?.style.blur.set(parentBlur) @@ -89,7 +97,9 @@ export class Infinite extends Component { this.use(() => { switch (this.state.get()) { case "zooming-in": { - this.appendContentsForLevel(shared.level) + if (!this.dom?.input.entity?.tunnel) throw new Error("Missing tunnel") + const tunnel = this.dom.input.entity.tunnel + this.appendContentsForLevel(tunnel.id) break } case "none": { @@ -112,14 +122,21 @@ export class Infinite extends Component { } /** - * @param {number} level + * @param {number} newLevel */ - appendContentsForLevel(level) { + appendContentsForLevel(newLevel) { const cellEntities = [] const wireEntities = [] + const oldLevelCell = getCell(shared.nogan, shared.level) + const newLevelCell = getCell(shared.nogan, newLevel) + + if (!oldLevelCell || !newLevelCell) { + throw new Error("Missing level cell - this shouldn't happen") + } + for (const cell of iterateCells(shared.nogan)) { - if (cell.parent !== level) continue + if (cell.parent !== newLevel) continue const entity = CELL_CONSTRUCTORS[cell.type]({ id: cell.id, position: cell.position, @@ -139,7 +156,7 @@ export class Infinite extends Component { for (const wire of iterateWires(shared.nogan)) { const cells = wire.cells.map((id) => getCell(shared.nogan, id)) - if (!cells.every((cell) => cell?.parent === level)) continue + if (!cells.every((cell) => cell?.parent === newLevel)) continue const entity = WIRE_CONSTRUCTOR({ id: wire.id, @@ -156,7 +173,40 @@ export class Infinite extends Component { } } + if (cellEntities.length === 0) { + const distance = FULL * 2 + const angle = Math.random() * Math.PI * 2 + + /** @type {[number, number][]} */ + const positions = [rotate([distance, 0], angle), rotate([-distance, 0], angle)] + + const entity1 = new ArrowOfCreation({ + position: positions[0], + preview: true, + level: newLevel, + }) + cellEntities.push(entity1) + this.previews.add(entity1) + + const entity2 = new ArrowOfConnection({ + position: positions[1], + preview: true, + level: newLevel, + }) + cellEntities.push(entity2) + this.previews.add(entity2) + + // const entity3 = new ArrowOfDestruction({ + // position: positions[2], + // preview: true, + // level: newLevel, + // }) + // cellEntities.push(entity3) + // this.previews.add(entity3) + } + const background = new Ellipse() + this.background = background background.dom.style.fill.set(BLACK.toString()) const parent = this.parent diff --git a/source/arroost/components/style.js b/source/arroost/components/style.js index 1c4c14b..ddd412b 100644 --- a/source/arroost/components/style.js +++ b/source/arroost/components/style.js @@ -1,9 +1,10 @@ -import { glue, GREY, use } from "../../../libraries/habitat-import.js" +import { BLACK, glue, GREY, use } from "../../../libraries/habitat-import.js" import { shared } from "../../main.js" +import { theme } from "../../theme.js" import { Component } from "./component.js" import { Dom } from "./dom.js" -export const Style = class extends Component { +export class Style extends Component { static highestZIndex = 0 static lowestZIndex = 0 @@ -57,9 +58,15 @@ export const Style = class extends Component { /** @type {Signal} */ opacity = this.use(null) - static SHADOW = "0px 4px 8px rgba(0, 0, 0, 0.25), 0px 0px 4px rgba(0, 0, 0, 0.15)" - static SHADOW_FILTER = - "drop-shadow(0px 4px 8px rgba(0, 0, 0, 0.25)) drop-shadow(0px 0px 4px rgba(0, 0, 0, 0.15))" + static SHADOW_COLOUR = theme.get() === "dark" ? "0, 0, 0" : "0, 0, 20" + static SHADOW_OPACITY = theme.get() === "dark" ? 0.25 : 0.03 + + static SHADOW = `0px 4px 8px rgba(${Style.SHADOW_COLOUR}, ${ + Style.SHADOW_OPACITY + }), 0px 0px 4px rgba(${Style.SHADOW_COLOUR}, ${Style.SHADOW_OPACITY * 0.6})` + static SHADOW_FILTER = `drop-shadow(0px 4px 8px rgba(${Style.SHADOW_COLOUR}, ${ + Style.SHADOW_OPACITY + })) drop-shadow(0px 0px 4px rgba(${Style.SHADOW_COLOUR}, ${Style.SHADOW_OPACITY * 0.6}))` /** * @param {SVGElement} element @@ -153,3 +160,5 @@ export const Style = class extends Component { this.zIndex.set(zIndex) } } + +window["Style"] = Style diff --git a/source/arroost/components/tunnel.js b/source/arroost/components/tunnel.js index 9808823..15cd935 100644 --- a/source/arroost/components/tunnel.js +++ b/source/arroost/components/tunnel.js @@ -56,6 +56,26 @@ export class Tunnel extends Component { */ static inViewInfiniteTunnels = new Set() + static purgeOtherLevelInfiniteTunnels() { + for (const [key, tunnel] of Tunnel.tunnels) { + const cell = getCell(shared.nogan, tunnel.id) + if (!cell) throw new Error(`Tunnel: Can't find cell ${tunnel.id}`) + if (cell.parent !== shared.level) { + tunnel.entity.dispose() + Tunnel.tunnels.delete(key) + } + } + + for (const tunnel of Tunnel.inViewInfiniteTunnels) { + const cell = getCell(shared.nogan, tunnel.id) + if (!cell) throw new Error(`Tunnel: Can't find cell ${tunnel.id}`) + if (cell.parent !== shared.level) { + tunnel.entity.dispose() + Tunnel.inViewInfiniteTunnels.delete(tunnel) + } + } + } + /** * @param {Operation[]} operations */ @@ -209,6 +229,11 @@ export class Tunnel extends Component { } if (this.type === "cell") { + const cell = getCell(shared.nogan, this.id) + if (!cell) throw new Error(`Tunnel: Can't find cell ${this.id}`) + if (cell.type === "timing" || cell.type === "colour") { + return + } this.useCell({ dom: this.entity.dom, carry: this.entity.carry, @@ -229,10 +254,11 @@ export class Tunnel extends Component { /** * @param {CellId | WireId} id * @param {Map} store + * @param {boolean} quiet */ - static delete(id, store = Tunnel.tunnels) { + static delete(id, store = Tunnel.tunnels, quiet = false) { const tunnel = Tunnel.get(id, store) - if (!tunnel) throw new Error(`Tunnel: Can't find tunnel ${id} to delete`) + if (!tunnel && !quiet) throw new Error(`Tunnel: Can't find tunnel ${id} to delete`) store.delete(id) if (store === Tunnel.tunnels) { @@ -253,7 +279,7 @@ export class Tunnel extends Component { dispose() { super.dispose() const store = this.isPreview ? Tunnel.tunnelPreviews : Tunnel.tunnels - Tunnel.delete(this.id, store) + Tunnel.delete(this.id, store, true) } /** @@ -272,16 +298,11 @@ export class Tunnel extends Component { const cell = getCell(shared.nogan, this.id) if (!cell) throw new Error(`Tunnel: Can't find cell ${this.id}`) if (equals(cell.position, position)) return + if (!equals(velocity, [0, 0])) return Tunnel.apply(() => { return moveCell(shared.nogan, { id: this.id, position, - propogate: equals(velocity, [0, 0]), - filter: (id) => { - const cell = getCell(shared.nogan, id) - if (!cell) throw new Error(`Tunnel: Can't find cell ${id}`) - return cell.type === "slot" - }, }) }) }) @@ -457,8 +478,9 @@ export const CELL_CONSTRUCTORS = { // Must be called only after all relevant cells have been created export const WIRE_CONSTRUCTOR = ({ id, colour, timing, source, target, preview }) => { - const targetTunnel = Tunnel.get(target, Tunnel.tunnelPreviews) - const sourceTunnel = Tunnel.get(source, Tunnel.tunnelPreviews) + const store = preview ? Tunnel.tunnelPreviews : Tunnel.tunnels + const targetTunnel = Tunnel.get(target, store) + const sourceTunnel = Tunnel.get(source, store) const targetEntity = targetTunnel?.entity const sourceEntity = sourceTunnel?.entity @@ -477,3 +499,5 @@ export const WIRE_CONSTRUCTOR = ({ id, colour, timing, source, target, preview } preview, }) } + +window["Tunnel"] = Tunnel diff --git a/source/arroost/cursor.js b/source/arroost/cursor.js index a07eb2f..e1007d2 100644 --- a/source/arroost/cursor.js +++ b/source/arroost/cursor.js @@ -1,5 +1,6 @@ import { use } from "../../libraries/habitat-import.js" import { shared } from "../main.js" +import { theme } from "../theme.js" export function useCursor() { use(() => { @@ -12,7 +13,10 @@ export function useCursor() { } case "berd": { - const path = "/assets/berd-cursor.svg" + const path = + theme.get() === "dark" + ? "/assets/berd-cursor.svg" + : "/assets/berd-cursor-inverse.svg" document.body.style.cursor = `url(${path}) 0 20, auto` return } diff --git a/source/arroost/entities/arrows/colour.js b/source/arroost/entities/arrows/colour.js index ce3fbdd..a98727c 100644 --- a/source/arroost/entities/arrows/colour.js +++ b/source/arroost/entities/arrows/colour.js @@ -8,7 +8,7 @@ import { WHITE, equals, } from "../../../../libraries/habitat-import.js" -import { GREY_SILVER, shared } from "../../../main.js" +import { shared } from "../../../main.js" import { createCell, createWire, diff --git a/source/arroost/entities/arrows/connection.js b/source/arroost/entities/arrows/connection.js index 1760cbe..40a6a60 100644 --- a/source/arroost/entities/arrows/connection.js +++ b/source/arroost/entities/arrows/connection.js @@ -23,7 +23,8 @@ export class ArrowOfConnection extends Entity { pulling = this.use(false) constructor({ - id = createCell(shared.nogan, { parent: shared.level, type: "connection" }).id, + level = shared.level, + id = createCell(shared.nogan, { parent: level, type: "connection" }).id, position = t([0, 0]), preview = false, }) { diff --git a/source/arroost/entities/arrows/creation.js b/source/arroost/entities/arrows/creation.js index 089f158..f67cb87 100644 --- a/source/arroost/entities/arrows/creation.js +++ b/source/arroost/entities/arrows/creation.js @@ -25,7 +25,8 @@ export class ArrowOfCreation extends Entity { pulling = this.use(false) constructor({ - id = createCell(shared.nogan, { parent: shared.level, type: "creation" }).id, + level = shared.level, + id = createCell(shared.nogan, { parent: level, type: "creation" }).id, position = t([0, 0]), preview = false, }) { diff --git a/source/arroost/entities/arrows/destruction.js b/source/arroost/entities/arrows/destruction.js index 52e80db..3a7e248 100644 --- a/source/arroost/entities/arrows/destruction.js +++ b/source/arroost/entities/arrows/destruction.js @@ -34,7 +34,8 @@ export class ArrowOfDestruction extends Entity { pulling = this.use(false) constructor({ - id = createCell(shared.nogan, { parent: shared.level, type: "destruction" }).id, + level = shared.level, + id = createCell(shared.nogan, { parent: level, type: "destruction" }).id, position = t([0, 0]), preview = false, }) { diff --git a/source/arroost/entities/arrows/dummy.js b/source/arroost/entities/arrows/dummy.js index d5e6df8..05a3165 100644 --- a/source/arroost/entities/arrows/dummy.js +++ b/source/arroost/entities/arrows/dummy.js @@ -8,7 +8,7 @@ import { WHITE, equals, } from "../../../../libraries/habitat-import.js" -import { GREY_SILVER, shared } from "../../../main.js" +import { shared } from "../../../main.js" import { createCell, fireCell, diff --git a/source/arroost/entities/arrows/noise.js b/source/arroost/entities/arrows/noise.js index 11a2ff5..e6d0696 100644 --- a/source/arroost/entities/arrows/noise.js +++ b/source/arroost/entities/arrows/noise.js @@ -5,7 +5,7 @@ import { Input } from "../../components/input.js" import { setCellStyles } from "./shared.js" import { ArrowOfRecording } from "./recording.js" import { RectangleHtml } from "../shapes/rectangle-html.js" -import { GREY_SILVER, shared } from "../../../main.js" +import { shared } from "../../../main.js" import { GREY } from "../../../../libraries/habitat-import.js" import { FIFTH, FULL, HALF, TENTH, THIRD } from "../../unit.js" diff --git a/source/arroost/entities/arrows/reality.js b/source/arroost/entities/arrows/reality.js index 3d24b74..3be7617 100644 --- a/source/arroost/entities/arrows/reality.js +++ b/source/arroost/entities/arrows/reality.js @@ -8,7 +8,7 @@ import { WHITE, equals, } from "../../../../libraries/habitat-import.js" -import { GREY_SILVER, shared } from "../../../main.js" +import { shared } from "../../../main.js" import { createCell, fireCell, diff --git a/source/arroost/entities/arrows/recording.js b/source/arroost/entities/arrows/recording.js index 212158e..37c69ab 100644 --- a/source/arroost/entities/arrows/recording.js +++ b/source/arroost/entities/arrows/recording.js @@ -10,7 +10,7 @@ import { equals, subtract, } from "../../../../libraries/habitat-import.js" -import { GREY_SILVER, MIDDLE_C, shared } from "../../../main.js" +import { MIDDLE_C, shared } from "../../../main.js" import { SharedResource, createCell, diff --git a/source/arroost/entities/arrows/shared.js b/source/arroost/entities/arrows/shared.js index 3849ee9..af4da2b 100644 --- a/source/arroost/entities/arrows/shared.js +++ b/source/arroost/entities/arrows/shared.js @@ -1,5 +1,5 @@ import { WHITE, BLACK, GREY, SILVER } from "../../../../libraries/habitat-import.js" -import { GREY_SILVER } from "../../../main.js" +import { GREY_SILVER, theme } from "../../../theme.js" import { Dom } from "../../components/dom.js" import { Infinite } from "../../components/infinite.js" import { Input } from "../../components/input.js" @@ -41,7 +41,7 @@ export const setCellStyles = ({ }) back.style.shadow.set(true) - back.style.stroke.set("rgba(0, 0, 0, 0.25)") + back.style.stroke.set(theme.get() === "dark" ? "rgba(0, 0, 0, 0.25)" : "rgba(0, 0, 20, 0.02)") back.style.strokeWidth.set(2) // back.style.pointerEvents.set(infinite?.isPreview ? "none" : "all") diff --git a/source/arroost/entities/arrows/slot.js b/source/arroost/entities/arrows/slot.js index 47f003a..567944d 100644 --- a/source/arroost/entities/arrows/slot.js +++ b/source/arroost/entities/arrows/slot.js @@ -8,7 +8,7 @@ import { WHITE, equals, } from "../../../../libraries/habitat-import.js" -import { GREY_SILVER, shared } from "../../../main.js" +import { shared } from "../../../main.js" import { createCell, fireCell, @@ -70,8 +70,8 @@ export class ArrowOfSlot extends Entity { this.dom.append(this.front.dom) // Style elements - this.back.dom.transform.scale.set([2 / 3, 2 / 3]) - this.front.dom.transform.scale.set([1 / 3, 1 / 3]) + this.back.dom.transform.scale.set([1, 1]) + this.front.dom.transform.scale.set([1 / 2, 1 / 2]) setCellStyles({ back: this.back.dom, front: this.front.dom, diff --git a/source/arroost/entities/arrows/time.js b/source/arroost/entities/arrows/time.js index cdeb667..03d2950 100644 --- a/source/arroost/entities/arrows/time.js +++ b/source/arroost/entities/arrows/time.js @@ -9,7 +9,7 @@ import { subtract, } from "../../../../libraries/habitat-import.js" import { shared } from "../../../main.js" -import { createWire } from "../../../nogan/nogan.js" +import { createWire, getCell, getWire } from "../../../nogan/nogan.js" import { Dom } from "../../components/dom.js" import { Tunnel } from "../../components/tunnel.js" import { Entity } from "../entity.js" @@ -47,6 +47,9 @@ export class ArrowOfTime extends Entity { this.target = target this.isPreview = preview + let timingId = undefined + let colourId = undefined + // Setup tunnel if (id === undefined) { const { wire, operations } = createWire(shared.nogan, { @@ -62,12 +65,21 @@ export class ArrowOfTime extends Entity { Tunnel.apply(() => operations) } else { this.tunnel = this.attach(new Tunnel(id, { entity: this, forcePreview: this.isPreview })) + const wireWire = getWire(shared.nogan, id) + for (const cellId of wireWire.cells) { + const cell = getCell(shared.nogan, cellId) + if (cell?.type === "timing") { + timingId = cellId + } else if (cell?.type === "colour") { + colourId = cellId + } + } } // Render elements this.line = this.attach(new Line()) - this.flaps = this.attach(new ArrowOfTiming({ wire: this.tunnel.id })) - this.colour = this.attach(new ArrowOfColour({ wire: this.tunnel.id })) + this.flaps = this.attach(new ArrowOfTiming({ wire: this.tunnel.id, id: timingId })) + this.colour = this.attach(new ArrowOfColour({ wire: this.tunnel.id, id: colourId })) this.dom.append(this.line.dom) if (!this.isPreview) { diff --git a/source/arroost/entities/arrows/timing.js b/source/arroost/entities/arrows/timing.js index 40d69e2..e1da4d5 100644 --- a/source/arroost/entities/arrows/timing.js +++ b/source/arroost/entities/arrows/timing.js @@ -8,7 +8,7 @@ import { WHITE, equals, } from "../../../../libraries/habitat-import.js" -import { GREY_SILVER, shared } from "../../../main.js" +import { shared } from "../../../main.js" import { createCell, createWire, diff --git a/source/arroost/entities/arrows/typing.js b/source/arroost/entities/arrows/typing.js index b2e08dc..4dd4c35 100644 --- a/source/arroost/entities/arrows/typing.js +++ b/source/arroost/entities/arrows/typing.js @@ -8,7 +8,7 @@ import { WHITE, equals, } from "../../../../libraries/habitat-import.js" -import { GREY_SILVER, shared } from "../../../main.js" +import { shared } from "../../../main.js" import { createCell, fireCell, diff --git a/source/arroost/entities/scene.js b/source/arroost/entities/scene.js index 0be2ef7..ec47c87 100644 --- a/source/arroost/entities/scene.js +++ b/source/arroost/entities/scene.js @@ -1,4 +1,4 @@ -import { GREY_SILVER, shared } from "../../main.js" +import { shared } from "../../main.js" import { Entity } from "./entity.js" import { Dom } from "../components/dom.js" import { @@ -9,6 +9,7 @@ import { distanceBetween, equals, fireEvent, + rotate, scale, subtract, use, @@ -20,18 +21,30 @@ import { replenishUnlocks } from "./unlock.js" import { Title } from "./title.js" import { TextHtml } from "./shapes/text-html.js" import { Transform } from "../components/transform.js" -import { Tunnel } from "../components/tunnel.js" +import { CELL_CONSTRUCTORS, Tunnel, WIRE_CONSTRUCTOR } from "../components/tunnel.js" import { CHILD_SCALE, + FULL, PARENT_SCALE, ZOOMING_IN_THRESHOLD, ZOOMING_OUT_THRESHOLD, ZOOM_IN_THRESHOLD, } from "../unit.js" -import { c, getCell, t } from "../../nogan/nogan.js" +import { + c, + createCell, + getCell, + getTemplate, + iterateCells, + iterateWires, + t, +} from "../../nogan/nogan.js" import { Infinite } from "../components/infinite.js" import { checkUnderPointer, triggerSomethingHasMoved } from "../machines/hover.js" import { Marker } from "./debug/marker.js" +import { ArrowOfTiming } from "./arrows/timing.js" +import { ArrowOfColour } from "./arrows/colour.js" +import { GREY_SILVER } from "../../theme.js" const ZOOM_FRICTION = 0.75 @@ -306,6 +319,9 @@ export class Scene extends Entity { let distanceFromScreenCenter = Infinity let closestTunnel = null for (const tunnel of Tunnel.inViewInfiniteTunnels.values()) { + const cell = getCell(shared.nogan, tunnel.id) + if (!cell) throw new Error("No cell found for tunnel") + if (cell.parent !== shared.level) continue const { transform } = tunnel.entity.dom const position = transform.position.get() const distance = distanceBetween(position, this.bounds.get().center) @@ -373,9 +389,15 @@ export class Scene extends Entity { replaceLayer(tunnel) { for (const layerName of this.sceneLayerNames) { const layer = this.layer[layerName] - // layer.dispose() + layer.dispose() } + shared.level = tunnel.id + + Tunnel.purgeOtherLevelInfiniteTunnels() + this.recreateSceneLayers() + this.rebuildWorldFromNogan() + const { center } = this.bounds.get() const targetPosition = tunnel.entity.dom.transform.absolutePosition.get() const position = scale(subtract(center, targetPosition), PARENT_SCALE) @@ -391,9 +413,52 @@ export class Scene extends Entity { } replaceLayerBackwards() { + for (const layerName of this.sceneLayerNames) { + const layer = this.layer[layerName] + layer.dispose() + } + + const oldLevelCell = getCell(shared.nogan, shared.level) + if (!oldLevelCell) throw new Error("Missing level cell - this shouldn't happen") + shared.level = oldLevelCell.parent + + let cellToComeOutOf = oldLevelCell.id + // If we're at the top level, just find any cell on the top level + if (shared.level === 0 && oldLevelCell.id === 0) { + // Purge everything, because we're gonna remake it + shared.level = NaN + Tunnel.purgeOtherLevelInfiniteTunnels() + shared.level = 0 + + const newCell = createCell(shared.nogan, { + type: "creation", + }) + + cellToComeOutOf = newCell.id + + for (const cell of iterateCells(shared.nogan)) { + if (cell.id === 0) continue + if (cell.id === newCell.id) continue + if (cell.parent === 0) { + cell.parent = newCell.id + break + } + } + + // createCell(shared.nogan, { + // type: "connection", + // position: rotate([FULL * 2, 0], Math.random() * Math.PI * 2), + // }) + } + + Tunnel.purgeOtherLevelInfiniteTunnels() + this.recreateSceneLayers() + this.rebuildWorldFromNogan() + const { center } = this.bounds.get() - const cell = getCell(shared.nogan, 1) + const cell = getCell(shared.nogan, cellToComeOutOf) if (!cell) { + console.warn("Couldn't find cell to come out of") return // throw new Error("Couldn't find cell to come out of") } @@ -405,6 +470,59 @@ export class Scene extends Entity { this.setCameraCenter(position) this.dealWithInfinites() } + + rebuildWorldFromNogan() { + const wireEntities = [] + + /** @type { (Entity & {dom: Dom; infinite?: Infinite})[] } */ + const cellEntities = [] + + for (const cell of iterateCells(shared.nogan)) { + if (cell.parent !== shared.level) continue + const entity = CELL_CONSTRUCTORS[cell.type]({ + id: cell.id, + position: cell.position, + template: getTemplate(cell), + }) + + if (!entity) continue + cellEntities.push(entity) + } + + for (const wire of iterateWires(shared.nogan)) { + const cells = wire.cells.map((id) => getCell(shared.nogan, id)) + if (!cells.every((cell) => cell?.parent === shared.level)) continue + + const entity = WIRE_CONSTRUCTOR({ + id: wire.id, + colour: wire.colour, + timing: wire.timing, + source: wire.source, + target: wire.target, + preview: false, + }) + wireEntities.push(entity) + } + + for (const entity of wireEntities) { + this.layer.wire.append(entity.dom) + } + + for (const entity of cellEntities) { + if (entity instanceof ArrowOfTiming) { + this.layer.timing.append(entity.dom) + } else if (entity instanceof ArrowOfColour) { + this.layer.timing.append(entity.dom) + } else { + this.layer.cell.append(entity.dom) + if (entity.infinite) { + entity.infinite.background?.dom.style.sendToBack() + // entity.infinite.dom?.style.bringToFront() + entity.dom.style.bringToFront() + } + } + } + } } addEventListener("keydown", (e) => { diff --git a/source/arroost/entities/tool.js b/source/arroost/entities/tool.js index c580775..ff50564 100644 --- a/source/arroost/entities/tool.js +++ b/source/arroost/entities/tool.js @@ -14,6 +14,7 @@ export function selectTool(type) { let nearestDistance = Infinity for (const cell of iterateCells(shared.nogan)) { if (cell.type !== type) continue + if (cell.parent !== shared.level) continue const distance = distanceBetween(cell.position, pointer) if (distance < nearestDistance) { nearestDistance = distance diff --git a/source/clock.js b/source/clock.js index 7a7314c..fec7bb0 100644 --- a/source/clock.js +++ b/source/clock.js @@ -1,10 +1,10 @@ import { BLACK, GREY, VOID } from "../libraries/habitat-import.js" import { Tunnel } from "./arroost/components/tunnel.js" -import { GREY_BLACK, shared } from "./main.js" +import { shared } from "./main.js" import { getAdvanced } from "./nogan/nogan.js" class Clock { - bpm = 120 + bpm = 160 /** @type {"buildup" | "aftermath"} */ phase = "buildup" @@ -13,6 +13,10 @@ class Clock { queue = [] count = 0 + setBpm(bpm) { + this.bpm = bpm + Tone.Transport.bpm.value = bpm + } start() { const metronome = new Tone.PluckSynth().toDestination() diff --git a/source/main.js b/source/main.js index f3e3972..6833d07 100644 --- a/source/main.js +++ b/source/main.js @@ -22,6 +22,7 @@ import { getZoomer } from "./arroost/input/zoomer.js" import { clock } from "./clock.js" import { registerImporters } from "./arroost/machines/import.js" import { useCursor } from "./arroost/cursor.js" +import { useTheme } from "./theme.js" //======// // Tone // @@ -35,8 +36,7 @@ window["print"] = print window["dir"] = console.dir.bind(console) registerMethods() -export const GREY_SILVER = new Colour(83, 101, 147) -export const GREY_BLACK = new Colour(31, 39, 54) +useTheme() //==============// // Setup Engine // diff --git a/source/nogan/nogan.js b/source/nogan/nogan.js index 75ebbe3..5084e57 100644 --- a/source/nogan/nogan.js +++ b/source/nogan/nogan.js @@ -1334,12 +1334,13 @@ export const fullFireCell = ( /** * Clone a nogan, ending all fires of cells whose parents are firing. * @param {Nogan} nogan + * @param {GetPeakMemo | undefined} memo * @returns {{ * projection: Nogan, * operations: Operation[], * }} */ -export const getProjection = (nogan) => { +export const getProjection = (nogan, memo = undefined) => { const projection = getClone(nogan) const operations = [] @@ -1356,7 +1357,7 @@ export const getProjection = (nogan) => { if (!cell.fire.blue && !cell.fire.red && !cell.fire.green) continue // Don't unfire the cell if its parent isn't firing - if (!isRoot(parent) && !isPeakFiring(nogan, { id: parent })) continue + if (!isRoot(parent) && !isPeakFiring(nogan, { id: parent, memo })) continue cell.fire = createFire() const unfiredOperation = c({ type: "unfired", id: cell.id }) @@ -1465,6 +1466,24 @@ export const getPeak = ( const from = timing === 1 ? past : future // First, let's try to look in the known future/past + // But wait! only if my parent is firing + // const cell = getCell(nogan, id) + // if (!cell) throw new Error(`Couldn't find cell ${id} to get peak`) + // const { parent } = cell + // if ( + // !isRoot(parent) && + // !isPeakFiring(nogan, { + // id: parent, + // past, + // future, + // memo, + // timing: getFlippedTiming(timing), + // }) + // ) { + // return createPeak({ colour }) + // } + + // Parent is firing, so let's look in the known future/past! const [next, ...rest] = to if (next) { const peak = getPeakNow(next, { @@ -1479,7 +1498,7 @@ export const getPeak = ( } // Otherwise, let's prepare to imagine the future/past - const { projection } = getProjection(nogan) + const { projection } = getProjection(nogan, memo) // But wait! // Are we stuck in a loop? @@ -1536,6 +1555,24 @@ const getPeakNow = (nogan, { id, colour, past, future, memo = new GetPeakMemo() }) if (!inputPeak.result) continue + + // Check if the input's parent is firing + const inputCell = getCell(nogan, wire.source) + if (!inputCell) throw new Error(`Couldn't find input cell ${wire.source}`) + const { parent } = inputCell + if ( + !isRoot(parent) && + !isPeakFiring(nogan, { + id: parent, + timing: getFlippedTiming(wire.timing), + past, + future, + memo, + }) + ) { + continue + } + peak = getBehavedPeak({ nogan, source: wire.source, @@ -1571,12 +1608,16 @@ export const isFiring = (nogan, { id }) => { * @param {Nogan} nogan * @param {{ * id: CellId, + * timing?: Timing, + * past?: Nogan[], + * future?: Nogan[], + * memo?: GetPeakMemo, * }} options * @returns {boolean} */ -export const isPeakFiring = (nogan, { id }) => { +export const isPeakFiring = (nogan, options) => { for (const colour of PULSE_COLOURS) { - const peak = getPeak(nogan, { id, colour }) + const peak = getPeak(nogan, { ...options, colour }) if (peak.result) return true } return false @@ -1639,6 +1680,12 @@ export const refresh = ( let memo = new GetPeakMemo() for (const id of iterateCellIds(snapshot)) { if (filter && !filter(id)) continue + const cell = getCell(snapshot, id) + if (!cell) throw new Error(`Couldn't find cell ${id} to refresh`) + // if (!isRoot(cell.parent) && !isPeakFiring(nogan, { id: cell.parent, memo, past, future })) { + // continue + // } + for (const colour of PULSE_COLOURS) { const peak = getPeak(snapshot, { id, @@ -1658,7 +1705,10 @@ export const refresh = ( // If we've changed the nogan, we need to refresh the cache // (as things might be different now) - memo = new GetPeakMemo() + + // COMMENT OUT FOR BUGS BUT HUGE PERFORMANCE INCREASE + // memo = new GetPeakMemo() + operations.push(...firedOperations) } } diff --git a/source/nogan/nogan.test.js b/source/nogan/nogan.test.js index f5a992a..7235e5c 100644 --- a/source/nogan/nogan.test.js +++ b/source/nogan/nogan.test.js @@ -1723,3 +1723,64 @@ describe("memoisation", () => { assertEquals(operations.length, 3) // Should have fired all three slots }) }) + +describe("infinity", () => { + it("deals with wires inside cells", () => { + const nogan = createNogan() + const cell1 = createCell(nogan, { type: "slot" }) + const cell2 = createCell(nogan, { type: "slot" }) + createWire(nogan, { source: cell1.id, target: cell2.id, timing: 1 }) + createWire(nogan, { source: cell2.id, target: cell1.id, timing: 1 }) + + const child1 = createCell(nogan, { type: "slot", parent: cell1.id }) + const child2 = createCell(nogan, { type: "slot", parent: cell1.id }) + createWire(nogan, { source: child1.id, target: child2.id, timing: 1 }) + + const advanced0 = getAdvanced(nogan).advanced + const cell1after0 = getCell(advanced0, cell1.id) + const cell2after0 = getCell(advanced0, cell2.id) + const child1after0 = getCell(advanced0, child1.id) + const child2after0 = getCell(advanced0, child2.id) + + assertEquals(cell1after0?.fire.blue, null) + assertEquals(cell2after0?.fire.blue, null) + assertEquals(child1after0?.fire.blue, null) + assertEquals(child2after0?.fire.blue, null) + + fireCell(nogan, { id: child1.id }) + + assertEquals(cell1.fire.blue, null) + assertEquals(cell2.fire.blue, null) + assertEquals(child1.fire.blue, { type: "raw" }) + assertEquals(child2.fire.blue, null) + + const advanced1 = getAdvanced(nogan).advanced + const cell1after1 = getCell(advanced1, cell1.id) + const cell2after1 = getCell(advanced1, cell2.id) + const child1after1 = getCell(advanced1, child1.id) + const child2after1 = getCell(advanced1, child2.id) + + assertEquals(cell1after1?.fire.blue, null) + assertEquals(cell2after1?.fire.blue, null) + assertEquals(child1after1?.fire.blue, { type: "raw" }) + assertEquals(child2after1?.fire.blue, null) + + fireCell(nogan, { id: cell1.id }) + + assertEquals(cell1.fire.blue, { type: "raw" }) + assertEquals(cell2.fire.blue, null) + assertEquals(child1.fire.blue, { type: "raw" }) + assertEquals(child2.fire.blue, null) + + const advanced2 = getAdvanced(nogan).advanced + const cell1after2 = getCell(advanced2, cell1.id) + const cell2after2 = getCell(advanced2, cell2.id) + const child1after2 = getCell(advanced2, child1.id) + const child2after2 = getCell(advanced2, child2.id) + + assertEquals(cell1after2?.fire.blue, null) + assertEquals(cell2after2?.fire.blue, { type: "raw" }) + assertEquals(child1after2?.fire.blue, null) + assertEquals(child2after2?.fire.blue, { type: "raw" }) + }) +}) diff --git a/source/theme.js b/source/theme.js new file mode 100644 index 0000000..0731db1 --- /dev/null +++ b/source/theme.js @@ -0,0 +1,72 @@ +import { + add, + BLACK, + Colour, + COLOURS, + GREY, + SILVER, + use, + WHITE, +} from "../libraries/habitat-import.js" + +export const GREY_SILVER = new Colour(83, 101, 147) +export const GREY_BLACK = new Colour(31, 39, 54) + +/** @type {Signal<"light" | "dark">} */ +export const theme = use("light") + +const BASE = { + WHITE, + BLACK, + GREY, + GREY_SILVER, + GREY_BLACK, + SILVER, +} + +window["BASE"] = BASE + +const DARK = { + WHITE: [...WHITE], + BLACK: [...BLACK], + GREY: [...GREY], + GREY_SILVER: [...GREY_SILVER], + GREY_BLACK: [...GREY_BLACK], +} + +const LIGHT = { + WHITE: [...BLACK], + BLACK: [...new Colour(255, 255, 255)], + GREY: [...SILVER], + GREY_SILVER: [...SILVER], + GREY_BLACK: [...SILVER], +} + +export function useTheme() { + addEventListener("keydown", (event) => { + if (event.key === "Enter") { + theme.set(theme.get() === "light" ? "dark" : "light") + } + }) + + use(() => { + switch (theme.get()) { + case "light": { + for (const key in LIGHT) { + BASE[key][0] = LIGHT[key][0] + BASE[key][1] = LIGHT[key][1] + BASE[key][2] = LIGHT[key][2] + } + break + } + case "dark": { + for (const key in DARK) { + BASE[key][0] = DARK[key][0] + BASE[key][1] = DARK[key][1] + BASE[key][2] = DARK[key][2] + } + break + } + } + }, [theme]) +}