FILE: App_Levels_constantLake.js const CONSTANT_LAKE = { type: 'ConstantLake', name: 'Constant Lake', nick: 'CONSTANT_LAKE', biome: 'westernSlopes', axesEnabled: false, runAsCutscene: true, x: 10, y: 10, requirements: ['SLOPE_SHALLOWER', 'SLOPE_LOWER'], defaultExpression: '\\frac{3}{1+e^{-\\frac{x+22}{2}}}-\\frac{5}{1+e^{-\\frac{x-2}{4}}}+\\frac{8}{1+e^{-\\frac{x-22}{5}}}+\\frac{-8}{1+\\frac{1}{e^{x-40}}}', assets: { images: { western_slopes_background_no_sky: '.webp', }, sounds: { music: { constant_lake: { base: 'base_compressed.mp3', pad_1_loopable: 'pad_1_loopable_compressed.mp3', pad_2_loopable: 'pad_2_loopable_compressed.mp3', pad_3_loopable: 'pad_3_loopable_compressed.mp3', pad_swell_1: 'pad_swell_1_compressed.mp3', wind: 'wind_compressed.mp3', }, }, }, }, directors: [ { type: 'lerp', point0: [-18, 0], point1: [0, 0], state0: { position: [-20, 3.5], fov: 5, }, state1: { position: [2, 8], fov: 10, }, }, { type: 'lerp', point0: [6, 0], point1: [30, 0], state0: { position: [2, 8], fov: 10, }, state1: { position: [34, 12], fov: 12, }, }, ], texts: [ { x: -22, y: 0, size: 0.6, align: 'left', // fill: '#fff', content: 'Click/arrows to move left+right', }, ], goals: [], sledders: [], sky: { asset: 'images.western_slopes_background_no_sky', margin: 1, }, bubble: { sky: { asset: 'images.western_slopes_background', margin: 1, }, }, backgroundMusic: { asset: 'sounds.music.constant_lake.base', volume: 1, }, sounds: [ { asset: 'sounds.music.constant_lake.pad_1_loopable', loop: true, volume: 1, domain: [-20, 0], }, { asset: 'sounds.music.constant_lake.pad_2_loopable', domain: [0, 8], // fadeOut: 3000, loop: true, volume: 1, // duration: 33000, }, { asset: 'sounds.music.constant_lake.pad_3_loopable', domain: [14, 18], // fadeOut: 3000, // duration: 33000, loop: true, volume: 1, }, { asset: 'sounds.music.constant_lake.pad_swell_1', domain: [10, 12], // fadeOut: 3000, // duration: 20000, // loop: true, volume: 1, }, ], dialogue: [ { speaker: 'Ada', content: "We can't win at this pace.", direction: 'up-up-left', distance: 1.2, speech: { content: "Let's speed up.", direction: 'up', x: -0.5, y: 1.3, }, }, { speaker: 'Jack', content: "I'm just here for the views", direction: 'up-up-left', speech: { speaker: 'Jack', content: "Winning isn't everything.", direction: 'up', x: -0.6, y: 1.3, distance: 1.2, }, }, { speaker: 'Jack', content: 'And the yous <3', direction: 'up-up-right', gap: 1, length: 2, }, { speaker: 'Ada', content: 'Sweet but wrong.', direction: 'up-up-left', gap: 1, length: 2, }, { speaker: 'Ada', content: 'Winning is the point of racing.', direction: 'up-up-right', gap: 1, // length: 2, }, { speaker: 'Jack', content: "I've *always* wanted to see the north rim.", direction: 'up-up-left', length: 4, gap: 1, speech: { content: "But there's so much to see… like, Eternal Canyon.", direction: 'up', x: -0.6, y: 1.3, distance: 1.2, }, }, { speaker: 'Ada', content: "It's not on the route, Jack.", direction: 'up-up-left', gap: 0, }, { speaker: 'Jack', content: 'It would be a crime not to.', direction: 'up-up-right', speech: { content: 'Can we at least stop for the sunset?', direction: 'up', x: 0.6, y: 1.3, distance: 1, }, }, { speaker: 'Ada', color: '#fff', content: 'We can watch while we trek.', direction: 'up-up-left', }, { speaker: 'Jack', color: '#fff', content: 'We mean no disrespect, great dayball!', direction: 'up-right', length: 4, speech: { content: "It's not good to anger the sun, Ada.", direction: 'up-up-right', color: '#fff', }, }, { speaker: 'Ada', color: '#fff', content: '…', direction: 'up-up-left', length: 2, gap: 1, }, { speaker: 'Ada', color: '#fff', content: "…you're such a dork.", direction: 'up-up-left', gap: 0, }, { speaker: 'Ada', color: '#fff', content: 'I still love you though.', gap: 4, }, // { // speaker: 'Ada', // color: '#fff', // content: 'I do love you, though.', // direction: 'up-up-left', // gap: 5, // }, ], walkers: [ { name: 'Ada', asset: 'images.sam_float', bobSpeed: 0.918218, x: -22, victoryX: 36, range: [-24, 37], walkers: { name: 'Jack', asset: 'images.jack_float', x: -24, }, }, ], sprites: [ { asset: 'images.tree_1', flipX: false, drawOrder: LAYERS.foreSprites, size: 4.8, x: -8, y: 0, offset: [0, 0.9], anchored: true, }, { asset: 'images.bush_2', flipX: false, size: 3.4, x: -7, y: 0, offset: [0, 0.9], anchored: true, }, { asset: 'images.tree_3', flipX: true, size: 4.1, x: -4.2, y: 0, offset: [0, 0.9], anchored: true, }, { asset: 'images.tree_2', flipX: true, size: 5.6, x: -1.6, y: 0, offset: [0, 0.9], anchored: true, }, { asset: 'images.tree_3', flipX: true, drawOrder: LAYERS.foreSprites, size: 4.4, x: 3.7, y: 0, offset: [0, 0.9], anchored: true, }, { asset: 'images.bush_1', flipX: false, size: 2.4, x: 4.8, y: 0, offset: [0, 0.7], anchored: true, }, { asset: 'images.tree_2', flipX: false, size: 5, x: 6.3, y: 0, offset: [0, 0.9], anchored: true, }, { asset: 'images.tree_1', flipX: true, drawOrder: LAYERS.foreSprites, size: 5, x: 7.4, y: 0, offset: [0, 0.9], anchored: true, }, { asset: 'images.bush_2', flipX: true, drawOrder: LAYERS.foreSprites, size: 2.8, x: 11, y: 0, offset: [0, 0.9], anchored: true, }, { asset: 'images.tree_2', flipX: true, size: 5.4, x: 13.2, y: 0, offset: [0, 0.9], anchored: true, }, { asset: 'images.tree_1', flipX: false, drawOrder: LAYERS.foreSprites, size: 4.3, x: 15, y: 0, offset: [0, 0.9], anchored: true, }, { asset: 'images.rock_2', flipX: true, drawOrder: LAYERS.foreSprites, size: 3, x: -3, offset: [0, 0.6], anchored: true, }, { asset: 'images.rock_3', flipX: false, drawOrder: LAYERS.backSprites, size: 3, x: 9.1, offset: [0, 0.6], anchored: true, }, { asset: 'images.fox', flipX: false, drawOrder: LAYERS.backSprites, size: 1, x: 9.1, offset: [0, 2.8], anchored: true, }, { asset: 'images.tree_1', flipX: false, drawOrder: LAYERS.backSprites, size: 7.3, x: -24, offset: [0, 1], anchored: true, }, { asset: 'images.bush_2', flipX: false, size: 1.4, x: -22.7, y: 0, offset: [0, 0.7], anchored: true, }, { asset: 'images.bush_1', flipX: false, size: 3.4, x: -21, y: 0, offset: [0, 0.7], anchored: true, }, { asset: 'images.rock_2', flipX: false, drawOrder: LAYERS.backSprites, size: 2.3, x: -24, offset: [0, 0.7], anchored: true, }, { asset: 'images.tree_1', flipX: false, drawOrder: LAYERS.backSprites, size: 4.3, x: -22, offset: [0, 0.9], anchored: true, }, { asset: 'images.rock_3', flipX: false, drawOrder: LAYERS.backSprites, size: 1.3, x: -21, offset: [0, 0.7], anchored: true, }, { asset: 'images.tree_2', flipX: false, drawOrder: LAYERS.backSprites, size: 6.3, x: -13, offset: [0, 0.9], anchored: true, }, { asset: 'images.rock_2', flipX: false, drawOrder: LAYERS.backSprites, size: 2.3, x: -14, offset: [0, 0.7], anchored: true, }, { asset: 'images.rock_3', flipX: false, drawOrder: LAYERS.backSprites, size: 2.3, x: -13, offset: [0, 0.7], anchored: true, }, { asset: 'images.bush_1', flipX: false, size: 2.4, x: 21.7, y: 0, offset: [0, 0.7], anchored: true, }, { asset: 'images.rock_2', flipX: false, size: 3.4, x: 22, y: 0, offset: [0, 0.7], anchored: true, }, { asset: 'images.tree_2', flipX: false, size: 4.3, x: 28, y: 0, offset: [0, 0.9], anchored: true, }, { asset: 'images.bush_1', flipX: false, size: 2.4, x: 28.5, y: 0, offset: [0, 0.7], anchored: true, }, { asset: 'images.tree_3', flipX: false, size: 4.3, x: 34, y: 0, offset: [0, 0.9], anchored: true, }, { asset: 'images.bush_1', flipX: false, size: 4.4, x: 35, y: 0, offset: [0, 0.7], anchored: true, }, ], water: { x: 38, y: 17, size: 30, }, } FILE: App_Levels_convergence.js const CONVERGENCE = [ { name: 'Convergence 1', nick: 'CONVERGENCE_1', biome: 'hilbertDelta', x: 0, y: -20, requirements: ['TWO_BELOW'], defaultExpression: '0', goals: [ { x: -9, y: 0, order: 'A', type: 'dynamic', }, { x: -6, y: 0, order: 'B', type: 'dynamic', }, { x: -3, y: 0, order: 'C', type: 'dynamic', }, { x: 3, y: 0, order: 'C', type: 'dynamic', }, { x: 6, y: 0, order: 'B', type: 'dynamic', }, { x: 9, y: 0, order: 'A', type: 'dynamic', }, ], sledders: [ { x: 0, y: 0 }, // { x: -12, y: 0, asset: 'images.sam_sled' }, // { x: 12, y: 0, asset: 'images.lunchbox_sled' }, ], }, { name: 'Convergence 2', nick: 'CONVERGENCE_2', biome: 'hilbertDelta', x: -10, y: -10, requirements: [null], defaultExpression: '0', goals: [ { x: -9, y: 0, order: 'A', type: 'dynamic', }, { x: -6, y: 0, order: 'C', type: 'dynamic', }, { x: -3, y: 0, order: 'B', type: 'dynamic', }, { x: 3, y: 0, order: 'B', type: 'dynamic', }, { x: 6, y: 0, order: 'C', type: 'dynamic', }, { x: 9, y: 0, order: 'A', type: 'dynamic', }, ], sledders: [ { x: 0, y: 0 }, // { x: -12, y: 0, asset: 'images.sam_sled' }, // { x: 12, y: 0, asset: 'images.lunchbox_sled' }, ], }, { name: 'Convergence 3', nick: 'CONVERGENCE_3', biome: 'hilbertDelta', x: 10, y: 0, requirements: ['CONVERGENCE_1'], defaultExpression: '0', goals: [ { x: -16, y: 0, order: 'A', type: 'dynamic', }, { x: -8, y: 0, order: 'B', type: 'dynamic', }, { x: 8, y: 0, order: 'A', type: 'dynamic', }, { x: 16, y: 0, order: 'B', type: 'dynamic', }, ], sledders: [ { x: 0, y: 0 }, // { x: -12, y: 0, asset: 'images.sam_sled' }, // { x: 12, y: 0, asset: 'images.lunchbox_sled' }, ], }, { name: 'Convergence 4', nick: 'CONVERGENCE_4', biome: 'hilbertDelta', x: -10, y: -10, requirements: [null], defaultExpression: '0', goals: [ { x: -16, y: 0, order: 'A', type: 'dynamic', }, { x: -12, y: 0, order: 'B', type: 'dynamic', }, { x: -8, y: 0, order: 'C', type: 'dynamic', }, { x: 8, y: 0, order: 'A', type: 'dynamic', }, { x: 12, y: 0, order: 'C', type: 'dynamic', }, { x: 16, y: 0, order: 'B', type: 'dynamic', }, ], sledders: [ { x: 0, y: 0 }, // { x: -12, y: 0, asset: 'images.sam_sled' }, // { x: 12, y: 0, asset: 'images.lunchbox_sled' }, ], }, ] FILE: App_Levels_credits.js const CREDITS = { name: 'Credits', nick: 'CREDITS', biome: 'home', axesEnabled: false, runAsCutscene: true, x: -10, y: 10, requirements: ['VOLCANO'], defaultExpression: '\\frac{1.5}{1+e^{{x+64}\\cdot 2}}+\\frac{-3}{1+e^{x+32}}+\\frac{-2}{1+e^{-x+5}}+\\frac{-2}{1+\\left(x-30\\right)^2}', clouds: {}, assets: { sounds: { music: { credits: '.mp3', }, }, }, directors: [ { type: 'lerp', point0: [-76, 0], point1: [-30, 0], state0: { position: [-73, 2], fov: 9, }, state1: { position: [-34, 3], fov: 8, }, }, { type: 'lerp', point0: [-30, 0], point1: [-14, 0], state0: { position: [-34, 3], fov: 8, }, state1: { position: [-10, 4], fov: 8, }, }, { type: 'lerp', point0: [-14, 0], point1: [-6, 0], state0: { position: [-10, 4], fov: 8, }, state1: { position: [10, 13], fov: 18, }, }, { type: 'lerp', point0: [-4, 0], point1: [0, 0], state0: { position: [10, 13], fov: 18, }, state1: { position: [10, -11], fov: 20, }, }, ], dialogue: [ { speaker: 'Ada', color: '#FFF', direction: 'up-up-right', content: 'Well I have to admit, that was fun.', }, { speaker: 'Jack', color: '#FFF', direction: 'up-up-left', content: 'I found it vaguely traumatizing…', speech: { speaker: 'Jack', color: '#FFF', direction: 'up', content: 'Really?', }, }, { speaker: 'Ada', color: '#FFF', direction: 'up-up-right', content: "Aren't you supposed to be the adventurer?", }, { speaker: 'Jack', color: '#FFF', direction: 'up-up-left', content: 'A lava monster almost ate us whole.', }, { speaker: 'Ada', color: '#FFF', direction: 'up-up-left', content: "Sounds like you're just being negative.", }, { speaker: 'Jack', color: '#FFF', direction: 'up', content: 'I deserved that.', }, { speaker: 'Ada', color: '#FFF', direction: 'up-up-right', content: "I'm sorry if I was a little… intense today.", }, { speaker: 'Jack', color: '#FFF', direction: 'up-up-left', content: "I'm sorry I made you wait", }, { speaker: 'Ada', color: '#FFF', direction: 'up-up-right', content: 'And almost lose.', gap: 0.5, }, { speaker: 'Ada', color: '#FFF', direction: 'up-up-right', content: 'And we won!', speech: { content: 'But you redeemed yourself.', direction: 'up', color: '#FFF', }, }, { speaker: 'Jack', color: '#FFF', direction: 'up-up-left', content: 'We did win.', }, { speaker: 'Ada', color: '#FFF', direction: 'up-up-right', content: "I'm glad you're here <3", }, { speaker: 'Jack', color: '#FFF', direction: 'up-up-left', content: "I'm glad you're here too, Ada.", }, { speaker: 'Ada', color: '#FFF', direction: 'up-up-right', content: 'Tomorrow is another day!', speech: { content: "Now let's go to bed.", direction: 'up', color: '#FFF', }, length: 4, }, ], texts: [ { x: 6, y: 26, size: 1.5, align: 'right', fill: '#222', content: 'Thank you for playing', }, { x: 16.5, y: 20, size: 1, fill: '#222', content: 'A game built with love by teenagers at Hack Club.', }, ], goals: [], sledders: [], sounds: [ { asset: 'sounds.music.credits', domain: [-12, -0], // fadeOut: 800, // duration: 20000, }, ], walkers: [ { name: 'Ada', // x: -10, x: -80, victoryX: 26, asset: 'images.sam_float', bobSpeed: 0.918218, range: [NINF, 0], walkers: { name: 'Jack', x: -6, asset: 'images.jack_float', }, }, ], sprites: [ { asset: 'images.tree_home_2', flipX: true, size: 7, x: -69, y: 0, offset: Vector2(0, 0.48), anchored: true, }, { asset: 'images.tree_home_3', flipX: true, size: 7, x: -54, y: 0, offset: Vector2(0, 0.5), anchored: true, }, { asset: 'images.tree_home_2', //flipX: '*', size: 6.7, x: -25, y: 0, offset: Vector2(0, 0.4), anchored: true, }, { asset: 'images.tree_home_1', //flipX: '*', size: 7.5, x: -17, y: 0, offset: Vector2(0, 0.4), anchored: true, }, { asset: 'images.tree_home_3', //flipX: '*', size: 6.7, x: -15, y: 0, offset: Vector2(0, 0.4), anchored: true, }, { asset: 'images.crow', flipX: '*', size: 1, x: -15.5, y: 0, anchored: true, }, { asset: 'images.credits', drawOrder: LAYERS.text, size: 35, x: 10, y: 0, offset: Vector2(0, -0.9), }, { asset: 'images.tree_home_1', drawOrder: LAYERS.backSprites, // flipX: '*', size: 6.7, x: -4, y: 0, offset: Vector2(0, 0.4), anchored: true, }, { asset: 'images.cabin_1', drawOrder: LAYERS.foreSprites, flipX: '*', size: 6.1, x: -1, y: -1, offset: Vector2(0, 0.7), anchored: true, }, { asset: 'images.tree_home_2', drawOrder: LAYERS.foreSprites, // flipX: '*', size: 6.1, x: 2, y: 0, offset: Vector2(0, 0.5), anchored: true, }, { asset: 'images.tree_home_3', flipX: false, size: 6.4, x: 8, y: 0, offset: Vector2(0, 0.5), anchored: true, }, { asset: 'images.logo_text', drawOrder: LAYERS.foreSprites, size: 20, x: 17, y: 23, anchored: false, }, { asset: 'images.windmill', drawOrder: LAYERS.background + 1, anchored: false, fixed: true, y: 0.204, x: 0.1215, size: 400, rotatingSpeed: 0.2, }, ], } FILE: App_Levels_desert.js const DESERT = { name: 'Desert', nick: 'DESERT', biome: 'sinusoidalDesert', axesEnabled: false, runAsCutscene: true, x: -10, y: -10, camera: { x: 2, y: -2, fov: 7, }, requirements: ['COS'], defaultExpression: '\\sin \\left(\\frac{x}{10}\\right)+\\sin \\left(\\frac{x}{20}+\\frac{pi}{3}\\right)+\\sin \\left(\\frac{x}{53}+\\frac{pi}{2}\\right)', assets: { sounds: { music: { desert: { western_intro: '.mp3', western_loop: '.mp3', }, }, }, }, directors: [ { type: 'lerp', point0: [0, 0], point1: [25, 0], state0: { position: [-2, 4], fov: 5, }, state1: { position: [30, 8], fov: 12, }, }, { type: 'lerp', point0: [35, 0], point1: [75, 0], state0: { position: [30, 8], fov: 12, }, state1: { position: [74, 5], fov: 8, }, }, { type: 'lerp', point0: [75, 0], point1: [90, 0], state0: { position: [74, 5], fov: 8, }, state1: { position: [85, 5], fov: 8, }, }, ], goals: [], sky: { asset: 'images.sinusoidal_desert_background', margin: 1, }, dialogue: [ { speaker: 'Ada', color: '#FFF', content: 'Well, how was it?', direction: 'up-up-left', }, { speaker: 'Jack', color: '#FFF', content: 'What?', direction: 'up-up-right', }, { speaker: 'Ada', color: '#FFF', content: 'Your canyon.', direction: 'up-up-left', }, { speaker: 'Jack', color: '#FFF', content: 'Scary. Hard.', direction: 'up-up-right', speech: [ { color: '#FFF', content: 'Oh.', direction: 'up', distance: 0.8, }, ], }, { speaker: 'Ada', color: '#FFF', content: "Well, that's why it's not on the route.", direction: 'up-up-left', }, { speaker: 'Jack', color: '#FFF', content: 'I did a backflip!', direction: 'up-up-right', speech: [ { speaker: 'Jack', color: '#FFF', content: 'I still had fun.', direction: 'up', }, ], }, { speaker: 'Ada', color: '#FFF', content: "No, you didn't.", direction: 'up-up-left', }, { speaker: 'Jack', color: '#FFF', content: 'Yes I did!', direction: 'up-up-left', gap: 1, length: 2, }, { speaker: 'Ada', color: '#FFF', content: 'Spinning in place is not a backflip.', direction: 'up-left', length: 4, speech: [ { speaker: 'Ada', color: '#FFF', content: 'Jack… we are ghosts. We can float.', direction: 'up-up-left', }, ], }, { speaker: 'Jack', color: '#FFF', content: "You're being negative again.", direction: 'up-up-left', }, { speaker: 'Ada', color: '#FFF', content: "It's just the truth!!", direction: 'up-left', speech: [ { speaker: 'Ada', color: '#FFF', content: "The truth isn't negative!", direction: 'up', }, ], }, { speaker: 'Jack', color: '#FFF', content: 'Are you mad at me?', direction: 'up-up-left', }, { speaker: 'Ada', color: '#FFF', content: 'Obviously, yes!', direction: 'up-up-left', gap: 1, }, { speaker: 'Ada', color: '#FFF', content: 'And probably the race.', direction: 'up-up-left', speech: [ { speaker: 'Ada', color: '#FFF', content: 'Your detour cost us time.', direction: 'up', }, ], }, { speaker: 'Jack', color: '#FFF', content: "You're right, I'm sorry.", direction: 'up-up-right', }, { speaker: 'Jack', color: '#FFF', speakerx: -4, content: 'Let me make it up to you. Watch this', direction: 'up-up-left', }, { speaker: 'Ada', color: '#FFF', content: '...how is this helping', direction: 'up-up-left', }, { speaker: 'Jack', color: '#FFF', content: 'Because it made you smile.', direction: 'up-up-left', }, { speaker: 'Ada', color: '#FFF', content: 'You do have a talent for that.', direction: 'up-up-left', }, ], walkers: [ { name: 'Ada', x: -4, victoryX: 90, asset: 'images.sam_float', range: [-6, PINF], bobSpeed: 0.918218, walkers: { name: 'Jack', x: -6, backflip: [72, 76], asset: 'images.jack_float', }, }, ], sprites: [ { asset: 'images.grass_2', flipX: true, size: 1.4, x: -6, y: 0, offset: [0, 0.5], anchored: true, }, { asset: 'images.cactus', flipX: true, size: 1.4, x: -5, y: 0, offset: [0, 0.5], anchored: true, }, { asset: 'images.grass_2', flipX: true, drawOrder: LAYERS.foreSprites, size: 1, x: -4.7, y: 0, offset: [0, 0], anchored: true, }, { asset: 'images.grass_2', flipX: true, drawOrder: LAYERS.foreSprites, size: 1.5, x: -4.3, y: 0, offset: [0, 0], anchored: true, }, { asset: 'images.grass_2', flipX: true, drawOrder: LAYERS.foreSprites, size: 1.5, x: 5.3, y: 0, offset: [0, 0], anchored: true, }, { asset: 'images.grass_2', flipX: true, drawOrder: LAYERS.foreSprites, size: 1.5, x: 5, y: 0, offset: [0, 0], anchored: true, }, { asset: 'images.grass_2', flipX: true, drawOrder: LAYERS.foreSprites, size: 1.5, x: 5.4, y: 0, offset: [0, 0], anchored: true, }, { asset: 'images.grass_2', flipX: true, drawOrder: LAYERS.foreSprites, size: 1.5, x: 6, y: 0, offset: [0, 0], anchored: true, }, { asset: 'images.tumbleweed', flipX: true, drawOrder: LAYERS.foreSprites, size: 1.1, x: 1, y: 0, offset: [0, 1], velocity: [1.4, 0], angularVelocity: 0, anchored: true, sloped: true, }, { asset: 'images.cactus', flipX: true, drawOrder: LAYERS.foreSprites, size: 1.7, x: 7.2, y: 0, offset: [0, 0, 5], anchored: true, }, { asset: 'images.lsys_red_joshua_tree', flipX: true, drawOrder: LAYERS.foreSprites, size: 3, x: 14, y: 0, offset: [0, 0, 5], anchored: true, }, { asset: 'images.tumbleweed', flipX: true, size: 1, x: -65.4, y: 0, offset: [0, 1], velocity: [1.3, 0], angularVelocity: 0, anchored: true, sloped: true, }, { asset: 'images.cactus', flipX: true, size: 1.4, x: 19.5, y: 0, offset: [0, 0.5], anchored: true, }, { asset: 'images.desert_tree', flipX: true, size: 2.4, x: 16, y: 0, offset: [0, 1], anchored: true, }, { asset: 'images.grass_2', flipX: true, drawOrder: LAYERS.foreSprites, size: 1.4, x: 18, y: 0, offset: [0, 0.5], anchored: true, }, { asset: 'images.grass_2', flipX: true, drawOrder: LAYERS.foreSprites, size: 1.4, x: 20.5, y: 0, offset: [0, 0.5], anchored: true, }, { asset: 'images.cactus', flipX: true, size: 1.4, x: 25, y: 0, offset: [0, 0.5], anchored: true, }, { asset: 'images.grass_2', flipX: true, drawOrder: LAYERS.foreSprites, size: 2.4, x: 24, y: 0, offset: [0, 0.5], anchored: true, }, { asset: 'images.grass_2', flipX: true, size: 2.4, drawOrder: LAYERS.foreSprites, x: 26, y: 0, offset: [0, 0.5], anchored: true, }, { asset: 'images.cactus', flipX: false, size: 1.4, x: 33, y: 0, offset: [0, 0.5], anchored: true, }, { asset: 'images.cactus', draworder: LAYERS.foreSprites, flipX: true, size: 0.6, x: 29, y: 0, offset: [0, 0.5], anchored: true, }, { asset: 'images.tumbleweed', drawOrder: LAYERS.foreSprites, flipX: true, size: 1.2, x: -50, y: 0, offset: [0, 1], velocity: [1.5, 0], angularVelocity: 0, anchored: true, sloped: true, }, { asset: 'images.blooming_joshua_tree', drawOrder: LAYERS.foreSprites, flipX: false, size: 2.4, x: 38.6, y: 0, offset: [0, 0.77], anchored: true, }, { asset: 'images.lsys_red_joshua_tree', flipX: true, size: 3, x: 39, y: 0, offset: [0, 0, 5], anchored: true, }, { asset: 'images.tumbleweed', drawOrder: LAYERS.foreSprites, flipX: true, size: 1.4, x: 45, y: 0, offset: [0, 0.5], anchored: true, }, { asset: 'images.grass_2', drawOrder: LAYERS.foreSprites, flipX: true, size: 1.4, x: 40, y: 0, offset: [0, 0.5], anchored: true, }, { asset: 'images.grass_2', drawOrder: LAYERS.foreSprites, flipX: true, size: 1.4, x: 47, y: 0, offset: [0, 0.5], anchored: true, }, { asset: 'images.desert_tree', drawOrder: LAYERS.foreSprites, flipX: true, size: 1.4, x: 47, y: 0, offset: [0, 0.1], anchored: true, }, { asset: 'images.grass_2', drawOrder: LAYERS.foreSprites, flipX: true, size: 1.4, x: 48, y: 0, offset: [0, 0.5], anchored: true, }, { asset: 'images.grass_2', flipX: true, size: 1.4, x: 49, y: 0, offset: [0, 0.5], anchored: true, }, { asset: 'images.lsys_red_joshua_tree', flipX: true, drawOrder: LAYERS.foreSprites, size: 3, x: 49, y: 0, offset: [0, 0, 5], anchored: true, }, { asset: 'images.grass_2', flipX: true, size: 1.4, x: 50, y: 0, offset: [0, 0.5], anchored: true, }, { asset: 'images.cactus', flipX: false, size: 1.4, x: 53, y: 0, offset: [0, 0.5], anchored: true, }, { asset: 'images.blooming_joshua_tree', drawOrder: LAYERS.foreSprites, flipX: true, size: 2.4, x: 55.2, y: 0, offset: [0, 0.77], anchored: true, }, { asset: 'images.cactus', flipX: false, size: 1.4, x: 53, y: 0, offset: [0, 0.5], anchored: true, }, { asset: 'images.lsys_red_joshua_tree', flipX: true, drawOrder: LAYERS.foreSprites, size: 3, x: 61.3, y: 0, offset: [0, 0, 5], anchored: true, }, { asset: 'images.lsys_red_joshua_tree', flipX: true, drawOrder: LAYERS.foreSprites, size: 3, x: 61.3, y: 0, offset: [0, 0, 5], anchored: true, }, { asset: 'images.blooming_joshua_tree', drawOrder: LAYERS.foreSprites, flipX: true, size: 2.4, x: 72, y: 0, offset: [0, 0.77], anchored: true, }, { asset: 'images.desert_tree', drawOrder: LAYERS.foreSprites, flipX: true, size: 4.4, x: 70, y: 0, offset: [0, 0.1], anchored: true, }, { asset: 'images.blooming_joshua_tree', drawOrder: LAYERS.foreSprites, flipX: true, size: 3.4, x: 85, y: 0, offset: [0, 0.77], anchored: true, }, { asset: 'images.lsys_red_joshua_tree', flipX: true, size: 3, x: 84, y: 0, offset: [0, 0, 5], anchored: true, }, { asset: 'images.blooming_joshua_tree', drawOrder: LAYERS.foreSprites, flipX: false, size: 4.4, x: 87, y: 0, offset: [0, 0.77], anchored: true, }, { asset: 'images.blooming_joshua_tree', drawOrder: LAYERS.foreSprites, flipX: true, size: 3.4, x: 79, y: 0, offset: [0, 0.77], anchored: true, }, { asset: 'images.lsys_red_joshua_tree', flipX: true, size: 3, x: 77.3, y: 0, offset: [0, 0, 5], anchored: true, }, { asset: 'images.cactus', drawOrder: LAYERS.foreSprites, flipX: true, size: 1.4, x: 81, y: 0, offset: [0, 0.5], anchored: true, }, { asset: 'images.desert_tree', drawOrder: LAYERS.foreSprites, flipX: true, size: 3.4, x: 82, y: 0, offset: [0, 0.77], anchored: true, }, { asset: 'images.blooming_joshua_tree', drawOrder: LAYERS.foreSprites, flipX: false, size: 3.4, x: 84.3, y: 0, offset: [0, 0.77], anchored: true, }, { asset: 'images.grass_2', drawOrder: LAYERS.foreSprites, flipX: true, size: 1.4, x: 84.3, y: 0, offset: [0, 0.77], anchored: true, }, { asset: 'images.blooming_joshua_tree', drawOrder: LAYERS.foreSprites, flipX: true, size: 2.4, x: 88, y: 0, offset: [0, 0.0], anchored: true, }, { asset: 'images.desert_tree', drawOrder: LAYERS.foreSprites, flipX: true, size: 3.4, x: 47, y: 0, offset: [0, 0.1], anchored: true, }, { asset: 'images.desert_tree', drawOrder: LAYERS.foreSprites, flipX: true, size: 4, x: 80, y: 0, offset: [0, 1], anchored: true, }, { asset: 'images.cactus', flipX: false, size: 1.4, x: 85, y: 0, offset: [0, 0.5], anchored: true, }, { asset: 'images.grass_2', flipX: false, size: 2.4, x: 84, y: 0, offset: [0, 0.5], anchored: true, }, { asset: 'images.grass_2', flipX: false, size: 2.4, x: 86, y: 0, offset: [0, 0.5], anchored: true, }, { asset: 'images.desert_tree', flipX: false, size: 3.4, x: -3, y: 0, offset: [0, 1], anchored: true, }, { asset: 'images.grass_2', drawOrder: LAYERS.foreSprites, flipX: true, size: 1.4, x: 3, y: 0, offset: [0, 0.0], anchored: true, }, { asset: 'images.blooming_joshua_tree', flipX: false, drawOrder: LAYERS.foreSprites, size: 3.4, x: 3, y: 0, offset: [0, 0.77], anchored: true, }, { asset: 'images.blooming_joshua_tree', drawOrder: LAYERS.foreSprites, flipX: true, size: 2.4, x: 17, y: 0, offset: [0, 0.77], anchored: true, }, { asset: 'images.desert_tree', drawOrder: LAYERS.foreSprites, flipX: true, size: 3.4, x: 12.5, y: 0, offset: [0, 0.0], anchored: true, }, { asset: 'images.grass_2', drawOrder: LAYERS.foreSprites, flipX: true, size: 1.4, x: 54.6, y: 0, offset: [0, 0.0], anchored: true, }, { asset: 'images.grass_2', drawOrder: LAYERS.foreSprites, flipX: true, size: 1.4, x: 56.6, y: 0, offset: [0, 0.0], anchored: true, }, { asset: 'images.cactus', drawOrder: LAYERS.foreSprites, flipX: true, size: 1.4, x: 62.9, y: 0, offset: [0, 0.5], anchored: true, }, { asset: 'images.grass_2', drawOrder: LAYERS.foreSprites, flipX: true, size: 1.4, x: 54.6, y: 0, offset: [0, 0.0], anchored: true, }, { asset: 'images.desert_tree', drawOrder: LAYERS.foreSprites, flipX: true, size: 3.4, x: 23.1, y: 0, offset: [0, 0.0], anchored: true, }, { asset: 'images.blooming_joshua_tree', drawOrder: LAYERS.foreSprites, flipX: true, size: 3.4, x: 34.6, y: 0, offset: [0, 0.77], anchored: true, }, { asset: 'images.grass_2', drawOrder: LAYERS.foreSprites, flipX: true, size: 1.4, x: 34.9, y: 0, offset: [0, 0.0], anchored: true, }, { asset: 'images.desert_tree', drawOrder: LAYERS.foreSprites, flipX: true, size: 2.4, x: 42, y: 0, offset: [0, 0.0], anchored: true, }, { asset: 'images.cactus', drawOrder: LAYERS.foreSprites, flipX: true, size: 1.4, x: 44.4, y: 0, offset: [0, 0.5], anchored: true, }, { asset: 'images.blooming_joshua_tree', drawOrder: LAYERS.foreSprites, flipX: true, size: 3, x: 55, y: 0, offset: [0, 0.0], anchored: true, }, { asset: 'images.blooming_joshua_tree', drawOrder: LAYERS.foreSprites, flipX: true, size: 2.4, x: 57, y: 0, offset: [0, 0.0], anchored: true, }, ], backgroundMusic: { asset: 'sounds.music.desert.western_loop', volume: 1, }, } FILE: App_Levels_editor.js const EDITOR = [ { name: 'Level Editor', nick: 'LEVEL_EDITOR', colors: Colors.biomes.alps, x: 10, y: -50, requirements: [], sledders: [ { asset: 'images.ada_jack_sled', x: 1, y: 0, }, ], defaultExpression: '-\\frac{2}{1+e^{-\\left(x-5\\right)}}', camera: { x: 0, y: 0, fov: 7, }, assets: { sounds: { music: { credits: '.mp3', valley_parabola_puzzle: '.mp3', western_slopes_puzzle: '.mp3', sinusoidal_desert_puzzle: '.mp3', eternal_canyon_puzzle: '.mp3', hilbert_delta_puzzle: '.mp3', logistic_dunes_puzzle: '.mp3', }, }, }, sky: { asset: 'images.western_slopes_background', margin: 1, }, }, ] FILE: App_Levels_logistic.js const LOGISTIC = [ { name: 'logistic', nick: 'LOGISTIC', biome: 'logisticDunes', x: 0, y: -30, requirements: ['SIN_TRANSLATE_SCALE_X_Y'], defaultExpression: '\\frac{1}{1+\\left(x+4\\right)^2}', // slider: { // expression: '\\frac{1}{1+\\left(x+$\\right)^2}', // bounds: [-3, 1, -1], // }, goals: [ { order: 'C', x: -5, y: 0, }, { order: 'B', x: -3, y: 0, }, { order: 'A', x: -1, y: 0, }, { order: 'A', x: 6, y: 0, }, { order: 'B', x: 8, y: 0, }, { order: 'C', x: 10, y: 0, }, ], sledders: [ { asset: 'images.sam_sled', x: 1, y: 0, speech: [ { x: 0.2, y: 0.6, content: 'Beautiful!', direction: 'up-up-left', distance: 1.4, }, ], }, { asset: 'images.lunchbox_sled', x: 3, y: 0, speech: [ { x: 0.3, y: 0.7, content: 'Look at that beach!!', direction: 'up-up-right', distance: 0.9, }, ], }, ], }, { name: 'logistic', nick: 'LOGISTIC_PARABOLA', biome: 'logisticDunes', x: -10, y: -10, requirements: ['LOGISTIC'], defaultExpression: '\\frac{1}{1+x^2}+\\left(\\frac{x}{2}\\right)^2', goals: [ { order: 'C', x: -8, y: 6, }, { order: 'B', x: -6, y: 4, }, { order: 'A', x: -4, y: 2, }, { order: 'A', x: 4, y: 2, }, { order: 'B', x: 6, y: 4, }, { order: 'C', x: 8, y: 6, }, ], sledders: [ { asset: 'images.sam_sled', x: -1, y: 0, }, { asset: 'images.lunchbox_sled', x: 1, y: 0, speech: { x: 0.3, y: 0.7, content: '…hi!', direction: 'up-up-right', }, }, ], sprites: [ { asset: 'images.benny_float', x: -4, y: 0, anchored: true, sloped: true, size: 1.7, offset: [0, 0.7], speech: { x: 0, y: 1.1, content: 'hi!', direction: 'up-up-left', }, }, ], }, { name: 'logistic', nick: 'LOGISTIC_TRAJECTORY', biome: 'logisticDunes', x: -20, y: 0, requirements: [null], defaultExpression: '\\left(\\frac{x}{4}\\right)^2+\\frac{1}{(1+x^2)}', goals: [ { type: 'path', expression: '-((x-5)/2)^2+11', x: 0, y: 0, pathX: 8, }, ], sledders: [ { asset: 'images.ada_jack_sled', x: -12, y: 0, speech: { x: -0.1, y: 0.6, content: '…where did you come from?', direction: 'up-up-right', }, }, ], sprites: [ { asset: 'images.benny_float', x: 6, y: 0, anchored: true, sloped: true, size: 1.7, offset: [0, 0.7], speech: { x: 0, y: 1.1, content: 'Cleveland!', direction: 'up-up-left', }, }, ], }, { name: 'logistic reorder', nick: 'LOGISTIC_REORDER', biome: 'logisticDunes', x: -10, y: 0, requirements: ['LOGISTIC'], defaultExpression: '\\frac{1}{1+\\left(x+8\\right)^2}+\\frac{1}{1+\\left(x+1\\right)^2}', goals: [ { order: 'B', x: -2, y: 0.5, }, { type: 'dynamic', order: 'A', x: 0, y: 0.5, }, { order: 'C', x: 2, y: 0.5, }, ], sledders: [ { x: -8, y: 0, speech: [ { speakerX: -0.3, speakerY: 0.7, content: 'Hmm?', direction: 'up-up-left', distance: 2.3, }, { speakerX: 0.5, speakerY: 0.7, content: 'Like, it could be flat.', direction: 'up-right', distance: 2, speech: { content: 'Why are there dunes instead of nothing?', direction: 'up-up-right', distance: 1.2, }, }, ], }, ], }, { name: 'logistic jump', nick: 'LOGISTIC_JUMP', biome: 'logisticDunes', x: -10, y: 0, requirements: [null], defaultExpression: '\\frac{1}{1+\\left(x+8\\right)^2}-\\frac{1}{1+x^2}+\\frac{1}{1+\\left(x-8\\right)^2}', goals: [ { order: 'A', x: -4, y: 0.5, }, { order: 'C', x: 2, y: 0.5, }, { order: 'B', x: 6, y: 0.5, }, ], sledders: [ { x: -8, y: 0, speech: [ { speakerX: -0.4, speakerY: 0.55, content: 'The arc of the universe is long…', direction: 'up-up-left', distance: 2.2, }, { speakerX: 0.5, speakerY: 0.65, content: '…but it bends toward dunes.', direction: 'up-right-right', distance: 2.6, }, ], }, ], }, ] FILE: App_Levels_ooo.js const OOO = [ { name: 'Out of Order 1', nick: 'OUT_OF_ORDER_1', biome: 'hilbertDelta', x: 10, y: -10, requirements: ['TWO_BESIDE'], defaultExpression: '0', goals: [ { x: 16, y: 0, order: 'A', }, { x: 8, y: 0, order: 'B', }, ], sledders: [{}], }, { name: 'Out of Order 2', nick: 'OUT_OF_ORDER_2', biome: 'hilbertDelta', x: 0, y: -20, requirements: null, defaultExpression: '0', goals: [ { x: 24, y: 0, order: 'A', }, { x: 8, y: 0, order: 'B', }, { x: 16, y: 0, order: 'C', }, ], sledders: [{}], }, { name: 'Out of Order 3', nick: 'OUT_OF_ORDER_3', biome: 'hilbertDelta', x: 10, y: 0, requirements: null, defaultExpression: '0', goals: [ { x: 18, y: 0, order: 'A', }, { x: 6, y: 0, order: 'B', }, { x: 24, y: 0, order: 'C', }, { x: 12, y: 0, order: 'D', }, ], sledders: [{}], }, { name: 'Out of Order 4', nick: 'OUT_OF_ORDER_4', biome: 'hilbertDelta', x: -10, y: -10, requirements: ['OUT_OF_ORDER_2'], defaultExpression: '0', goals: [ { x: -8, y: 0, order: 'A', }, { x: -4, y: 0, order: 'B', }, { x: 4, y: 0, order: 'B', }, { x: 8, y: 0, order: 'A', }, ], sledders: [ { speech: [ { distance: 1.5, speakerX: 0.5, speakerY: 0.7, color: '#FFF', direction: 'up-up-right', content: 'Pi creature!', }, { distance: 2.2, speakerX: -0.3, speakerY: 0.55, color: '#FFF', content: 'omigosh!!', direction: 'up-up-left', align: 'right', }, ], }, ], sprites: [ { asset: 'images.pi_dance_kick', flipX: false, drawOrder: LAYERS.foreSprites, size: 1.4, x: 6, y: 0, offset: [0, 0.9], sloped: true, anchored: true, speech: { x: 0.2, y: 1.2, color: '#FFF', content: '*meep*', direction: 'up-up-right', distance: 0.8, }, }, ], }, { name: 'Out of Order 5', nick: 'OUT_OF_ORDER_5', biome: 'hilbertDelta', x: -10, y: 0, requirements: null, defaultExpression: '0', goals: [ { x: -12, y: 0, order: 'C', }, { x: -8, y: 0, order: 'A', }, { x: -4, y: 0, order: 'B', }, { x: 4, y: 0, order: 'B', }, { x: 8, y: 0, order: 'A', }, { x: 12, y: 0, order: 'C', }, ], sledders: [ { speech: [ { distance: 2.2, speakerX: 0.5, speakerY: 0.7, color: '#FFF', direction: 'up-up-right', content: 'How majestic…', }, { distance: 1.5, speakerX: -0.3, speakerY: 0.55, color: '#FFF', content: 'eee!!', direction: 'up-up-left', align: 'right', }, ], }, ], sprites: [ { asset: 'images.pi_confused', flipX: false, drawOrder: LAYERS.foreSprites, size: 1.4, x: 10, y: 0, offset: [0, 0.9], sloped: true, anchored: true, // speech: { // x: 0.2, // y: 1.2, // color: '#FFF', // content: '…', // direction: 'up-up-right', // distance: 0.8, // }, }, ], }, { name: 'Out of Order 6', nick: 'OUT_OF_ORDER_6', biome: 'hilbertDelta', x: 10, y: -10, requirements: ['OUT_OF_ORDER_2'], defaultExpression: '0', goals: [ { x: -6, y: 0, order: 'C', }, { x: -3, y: 0, order: 'A', }, { x: 0, y: 0, order: 'B', }, { x: 3, y: 0, order: 'A', }, { x: 6, y: 0, order: 'C', }, ], sledders: [ { x: -12, y: 0, asset: 'images.sam_sled' }, { x: 12, y: 0, asset: 'images.lunchbox_sled' }, ], }, ] FILE: App_Levels_parabola.js const PARABOLA = [ { name: 'Way too steep for us.', nick: 'PARABOLA_NEGATE', biome: 'valleyParabola', x: 20, y: 10, requirements: ['SLOPE_SCALE_TRANSLATE'], defaultExpression: '-x^2', hint: 'do as she says!', slider: { expression: '$\\cdot x^2', bounds: [-1, 0, -1], }, goals: [ { type: 'path', expression: 'x^2', pathX: 3, x: -1.5, y: 0, }, ], sledders: [ { asset: 'images.sam_sled', x: -2.5, y: 0, speech: [ { x: 0.5, y: 0.6, content: 'Be positive.', direction: 'up-up-right', distance: 1, }, ], }, ], sprites: [], }, { name: 'Translate Y', nick: 'PARABOLA_TRANSLATE_Y', biome: 'valleyParabola', x: 10, y: 0, requirements: ['PARABOLA_NEGATE'], defaultExpression: 'x^2-1', hint: 'hint: subtract from (everything)', slider: { expression: 'x^2+$', bounds: [-2, 2, -1], }, goals: [ { type: 'path', expression: 'x^2-3', pathX: 3, x: -1.5, y: 0, }, ], sledders: [ { asset: 'images.sam_sled', speech: [ { x: 0.4, y: 0.6, content: 'So much faster without him!', direction: 'up-up-right', distance: 1, }, ], x: -2, y: 0, }, ], }, { name: 'Translate X', nick: 'PARABOLA_TRANSLATE_X', biome: 'valleyParabola', x: 10, y: 10, camera: { x: -2, y: 5, fov: 8, }, requirements: null, defaultExpression: '\\left(x-1\\right)^2', hint: 'hint: add to (x)', slider: { expression: '\\left(x+$\\right)^2', bounds: [-2, 2, 0], }, goals: [ { type: 'path', expression: '(x+3)^2', pathX: -3, x: -1.5, y: 0, }, ], sledders: [ { asset: 'images.sam_sled', speech: [ { x: 0.3, y: 0.6, content: "It's so much… quieter now!", direction: 'up', distance: 1.3, }, ], x: -1, y: 0, }, ], }, { name: 'Translate XY', nick: 'PARABOLA_TRANSLATE_X_Y', biome: 'valleyParabola', x: 10, y: 10, camera: { x: -2, y: 0, fov: 8, }, requirements: null, defaultExpression: '(x-1)^2+2', hint: 'seeing a pattern yet?', goals: [ { type: 'path', expression: '(x+3)^2-4', pathX: -3, x: -1.5, y: 0, }, ], sledders: [ { asset: 'images.sam_sled', speech: [ { x: 0.4, y: 0.6, content: 'Quiet is good.', direction: 'up-up-right', distance: 1, }, ], x: -1, y: 0, }, ], }, { name: 'Way too steep for us.', nick: 'PARABOLA_SCALE_Y', biome: 'valleyParabola', x: 0, y: 10, requirements: ['PARABOLA_NEGATE'], defaultExpression: '\\left(\\frac{x}{4}\\right)^2', slider: { expression: '\\left(\\frac{x}{$}\\right)^2', bounds: [1, 6, 4], }, goals: [ { type: 'path', expression: '(x/8)^2', x: -8, y: 0, pathX: 16, }, ], sledders: [ { asset: 'images.sam_sled', speech: [ { x: 0.4, y: 0.65, content: "Even if he'll probably get lost.", direction: 'up-up-right', distance: 1.2, speech: [ { content: 'He can make his own decisions…', direction: 'up', distance: 1, }, ], }, ], x: -12, y: 0, }, ], }, { name: 'Scale X', nick: 'PARABOLA_SCALE_X', biome: 'valleyParabola', x: 10, y: 10, requirements: null, slider: { expression: '\\left(\\frac{x}{$}\\right)^2', bounds: [3, 5, 4], }, defaultExpression: 'x^2', hint: '(x/4)^2 = x^2/4^2 = x^2/16', goals: [ { type: 'path', expression: '(x/2)^2', x: -4, y: 0, pathX: 8, }, ], sledders: [ { asset: 'images.sam_sled', // speech: [ // { // x: 0.5, // y: 0.6, // content: 'Jack would go and check...', // direction: 'up', // distance: 1, // speech: { // content: 'Ok also why are the islands floating?', // distance: 1, // direction: 'up', // }, // }, // ], speech: [ { x: 0.3, y: 0.7, content: 'I need to stay focused.', direction: 'up-up-right', distance: 1.2, }, ], x: -5, y: 0, }, ], }, { name: 'Translate Scale X', nick: 'PARABOLA_TRANSLATE_SCALE_X', biome: 'valleyParabola', x: -10, y: 10, requirements: ['PARABOLA_TRANSLATE_X_Y', 'PARABOLA_SCALE_X'], defaultExpression: '\\left(\\frac{x-5}{4}\\right)^2', hint: '(x/4)^2 = x^2/4^2 = x^2/16', goals: [ { type: 'path', expression: '((x+3)/2)^2', x: 1, y: 0, pathX: -8, }, ], sledders: [ { asset: 'images.sam_sled', speech: [ { x: 0.4, y: 0.6, content: '…', direction: 'up-up-right', distance: 1, }, ], x: 3, y: 0, }, ], }, { name: 'Translate Scale XY', nick: 'PARABOLA_TRANSLATE_SCALE_X_Y', biome: 'valleyParabola', x: 20, y: 0, requirements: null, defaultExpression: '\\left(\\frac{x+5}{4}\\right)^2', goals: [ { type: 'path', expression: '((x-8)/2)^2-4', x: 4, y: 0, pathX: 8, }, ], sledders: [ { asset: 'images.sam_sled', x: 2, y: 0, speech: [ { speakerX: 0.4, speakerY: 0.7, content: 'I do miss him.', direction: 'up-up-right', distance: 1.5, }, ], }, ], }, ] FILE: App_Levels_slope.js const SLOPE = [ { name: 'A Solid Slope', nick: 'SLOPE_POSITIVE', biome: 'westernSlopes', x: 20, y: 0, requirements: ['HELLO_WORLD'], flashMathField: true, defaultExpression: '-x', goals: [ { x: -2, y: -2, }, { x: -4, y: -4, }, { x: -6, y: -6, }, ], sledders: [ { // speech: [ // { // speakerX: -0.4, // content: 'Remember, follow the WHOLE path.', // direction: 'up-up-left', // distance: 1.5, // }, // { // speakerX: 0.3, // content: 'I know how to do this, Ada!', // direction: 'right-up', // distance: 1, // }, // ], }, ], texts: [ { x: -1.5, y: 0.6, size: 0.6, align: 'right', // fill: '#fff', content: 'Edit the function to sled through the squares', }, ], slider: { expression: '$\\cdot x', bounds: [-1, 1, -1], }, tips: [ { content: 'Edit function here', domSelector: '#expression-envelope', place: 'top-right', destroyOnClick: true, index: 0, }, { content: 'Click here to sled', domSelector: '#run-button', place: 'top-left', destroyOnClick: true, index: 1, style: { visibility: 'hidden' }, // style: { fontSize: '1.1rem' }, }, // { // content: 'Click here ', // domSelector: '#dotted-math-button', // place: 'top-left', // destroyOnClick: true, // }, ], sprites: [], }, { name: 'Try facing forwards?', nick: 'SLOPE_NEGATIVE', biome: 'westernSlopes', x: 10, y: 0, requirements: null, defaultExpression: 'x', hint: 'hint: go negative', goals: [ { type: 'path', expression: '-x', pathX: 6, x: 2, y: 0, }, ], sledders: [ { speech: [ { speakerX: -0.25, speakerY: 0.6, content: '…forward this time? Please?', direction: 'up-up-left', distance: 2, }, { speakerX: 0.35, speakerY: 0.6, content: 'Wuss.', direction: 'up-up-right', distance: 1.2, }, ], }, ], texts: [ { x: 3, y: 0.6, size: 0.6, align: 'left', // fill: '#fff', content: 'Well done! Now sled through this path', }, ], slider: { expression: '$\\cdot x', bounds: [-1, 1, 1] }, sprites: [], }, { name: 'A real steep hill', nick: 'SLOPE_STEEPER', biome: 'westernSlopes', x: 10, y: -10, requirements: ['SLOPE_NEGATIVE'], defaultExpression: '-5x', slider: { expression: '$x', bounds: [-6, -3, -5], }, texts: [ { x: 2, y: 4, size: 1, align: 'left', content: 'ABC goals must be hit in order', }, ], goals: [ { order: 'A', x: 2, y: -4, }, { order: 'B', x: 4, y: -8, }, { order: 'C', x: 6, y: -12, }, ], sledders: [ { speech: [ { x: -0.3, y: 0.6, content: 'Steep!', direction: 'up-up-left', distance: 1.2, }, // { // speakerX: 0.4, // speakerY: 0.7, // content: 'Steep.', // direction: 'right-up', // distance: 1.4, // // speech: { // // content: 'This seems dangerous.', // // distance: 1, // // }, // }, ], }, ], sprites: [], }, { name: 'The bunny slope', nick: 'SLOPE_SHALLOWER', biome: 'westernSlopes', x: 10, y: 0, requirements: null, defaultExpression: '\\frac{x}{2}', slider: { expression: '-\\frac{x}{$}', bounds: [1, 2, 1], }, goals: [ { x: 4, y: -1, order: 'A', }, { type: 'path', expression: '-x/4', pathX: 6, x: 6, y: 0, order: 'B', }, { x: 14, y: -3.5, order: 'C', }, ], sledders: [ { speech: [ { speakerX: -0.4, speakerY: 0.7, content: '…yes.', direction: 'up-up-left', distance: 1.4, speech: { content: 'No.', direction: 'up', distance: 0.8, }, }, { speakerX: 0.7, speakerY: 0.7, content: 'Do you want my scarf?', direction: 'right-up-up', distance: 2.1, speech: { content: 'Are you cold?', direction: 'up-up-left', distance: 1.2, }, }, ], }, ], sprites: [], }, { name: 'Moving up in the world', nick: 'SLOPE_HIGHER', biome: 'westernSlopes', x: 10, y: 10, camera: { x: 5, y: 2, fov: 9, }, requirements: ['SLOPE_NEGATIVE'], defaultExpression: '-x-3', slider: { expression: '-x+$', bounds: [-3, 3, -3], }, goals: [ { type: 'path', expression: '-x+8', pathX: 4, x: 3, y: 0, }, ], sledders: [ { speech: [ { y: 0.6, speakerX: -0.4, // speakerY: 0.8, content: 'Sorry.', direction: 'up-left', distance: 1.2, speech: { content: "You're not steering right", distance: 1, }, }, { y: 0.7, speakerX: 0.6, // speakerY: 0.7, content: 'No backseat sledding!', direction: 'up-right', distance: 2, }, ], }, ], sprites: [], }, { name: 'About halfway down', nick: 'SLOPE_LOWER', biome: 'westernSlopes', x: 10, y: 0, requirements: null, defaultExpression: '-x+1', goals: [ { type: 'path', expression: '-x-3', pathX: 4, x: 3, y: 0, }, ], sledders: [ { speech: [ { speakerX: -0.3, speakerY: 0.65, content: 'Love you.', direction: 'up', distance: 2.2, }, { speakerX: 0.5, speakerY: 0.7, content: 'Love you too', direction: 'up-right', distance: 2.1, }, ], }, ], slider: { expression: '-x+$', bounds: [-1, 2, 1] }, sprites: [], }, CONSTANT_LAKE, { name: "We're at the bottom", nick: 'SLOPE_SCALE_TRANSLATE', biome: 'westernSlopes', x: 10, y: 10, backgroundMusic: 'sounds.music.birds', requirements: ['CONSTANT_LAKE'], defaultExpression: '-\\frac{x}{2}', hint: 'put it all together!', goals: [ { type: 'path', expression: '-x/3-5', pathX: 14, x: 3, y: 0, }, ], sprites: [ { x: 3, size: 2, asset: 'images.sam_float', speech: [ { speakerX: 0.3, speakerY: -0.4, content: 'I have a race to win.', direction: 'up-right-right', distance: 1, speech: { content: '…do whatever you want.', direction: 'up-up-right', distance: 0.8, }, }, ], }, ], sledders: [ { asset: 'images.benny_sled', speech: [ { speakerX: 0.2, y: 0.7, content: 'We never get to do my things.', direction: 'up-up-left', distance: 1.6, speech: { content: 'Ok, seriously. I want to see this canyon.', direction: 'up', distance: 1, }, }, ], }, ], }, ] FILE: App_Levels_swamp.js const SWAMP = [ { name: 'Two Beside', nick: 'TWO_BESIDE', biome: 'hilbertDelta', x: -30, y: 0, requirements: ['TIME_PARABOLA_CIRCLE'], defaultExpression: '0', goals: [ { x: 12, y: 0, }, { x: 6, y: -6, }, ], sledders: [ { speech: [ { speakerX: -0.4, speakerY: 0.7, content: 'This place creeps me out.', direction: 'up-up-left', distance: 2.2, color: '#FFF', }, { speakerX: 0.5, speakerY: 0.65, content: 'Yeah...', color: '#FFF', direction: 'up-up-right', distance: 1.6, }, ], }, ], sprites: [], }, { name: 'Two Below', nick: 'TWO_BELOW', biome: 'hilbertDelta', x: -10, y: 0, requirements: null, defaultExpression: '0', goals: [ { x: 8, y: -8, }, { x: -8, y: -8, }, ], sprites: [], sledders: [ { speech: [ { distance: 1.6, speakerX: 0.6, speakerY: 0.6, color: '#FFF', direction: 'up-up-right', content: 'Just follow me!!', speech: { distance: 1, color: '#FFF', direction: 'up', content: 'Come on. I have a shortcut.', }, }, { distance: 2.3, speakerX: -0.3, speakerY: 0.5, color: '#FFF', content: 'Jack…', direction: 'up-up-left', align: 'right', }, ], }, ], }, { name: 'Four Around', nick: 'FOUR_AROUND', biome: 'hilbertDelta', x: -10, y: -10, requirements: null, defaultExpression: '0', goals: [ { x: 0, y: -4, }, { x: 4, y: 0, }, { x: 0, y: 8, }, { x: -4, y: 0, }, ], sledders: [ { speech: [ { distance: 2, speakerX: 0.5, speakerY: 0.7, color: '#FFF', direction: 'up-up-right', content: 'But first, a little more exploring…', }, { distance: 1.4, speakerX: -0.3, speakerY: 0.55, color: '#FFF', content: '…', direction: 'up-up-left', align: 'right', }, ], }, ], }, { name: 'False Cubic', nick: 'FALSE_CUBIC', biome: 'hilbertDelta', x: -10, y: 0, requirements: null, defaultExpression: '0', goals: [ { x: 8, y: -8, order: 'A', }, { x: 16, y: -8, order: 'B', }, { x: 24, y: -16, order: 'C', }, ], sledders: [{}], }, { name: 'Zig Zag', nick: 'ZIG_ZAG', biome: 'hilbertDelta', x: -10, y: -10, requirements: null, defaultExpression: '0', goals: [ { x: 16, y: -4, order: 'A', }, { x: 0, y: -8, order: 'B', }, { x: 16, y: -12, order: 'C', }, ], sledders: [{}], }, { name: 'Big Bump', nick: 'BIG_BUMP', biome: 'hilbertDelta', x: 0, y: -10, requirements: ['FALSE_CUBIC'], defaultExpression: '0', goals: [ { x: 8, y: 4, }, { x: 16, y: 0, }, ], sledders: [{}], }, { name: 'Dip Traverse', nick: 'DIP_TRAVERSE', biome: 'hilbertDelta', x: -10, y: -10, requirements: null, defaultExpression: '0', goals: [ { x: 16, y: 0, order: 'A', }, { x: 8, y: -8, order: 'B', }, ], sledders: [{}], }, ...OOO, ...CONVERGENCE, ] FILE: App_Levels_time.js const TIME = [ { name: 'Time', nick: 'TIME_NEGATE', biome: 'eternalCanyon', x: 20, y: -10, requirements: ['SLOPE_SCALE_TRANSLATE'], defaultExpression: '-t', goals: [ { x: 0, y: 4, order: 'A', }, { x: 0, y: 6, order: 'B', }, { x: 0, y: 8, order: 'C', }, ], tips: [ { content: 't represents time', domSelector: '#expression-envelope', place: 'right', destroyOnClick: true, style: {fontSize: '0.9rem'}, index: 0, }, { content: 'Move this to preview different values of t', domSelector: '#t-variable-container', place: 'top-right', destroyOnClick: true, style: { fontSize: '0.9rem', visibility: 'hidden', left: 'calc(50% + 50px)'}, index: 1, }, { content: 'See what happens!', domSelector: '#run-button', place: 'top-left', style: { fontSize: '0.9rem', visibility: 'hidden' }, index: 2, }, ], sledders: [ { asset: 'images.lunchbox_sled', speech: [ { x: 0.3, y: 0.7, content: '"do whatever you want."', direction: 'up-up-right', distance: 0.8, }, ], }, ], }, { name: 'Time Constant', nick: 'TIME_CONSTANT', biome: 'eternalCanyon', x: 10, y: 0, requirements: null, defaultExpression: 't-1', goals: [ { x: 0, y: 6, order: 'A', }, { x: 0, y: 4, order: 'B', }, { x: 0, y: -4, order: 'C', }, { x: 0, y: -6, order: 'D', }, ], sledders: [ { asset: 'images.lunchbox_sled', speech: [ { x: 0.3, y: 0.7, content: 'This is what I want. Adventure!', direction: 'up-up-right', distance: 0.8, }, ], }, ], }, { name: 'Time Cool', nick: 'TIME_COOL', biome: 'eternalCanyon', x: -10, y: -10, requirements: null, defaultExpression: '\\left(x+1\\right)\\cdot \\left(t-1\\right)', goals: [ { x: -4, y: 0, }, { x: -4, y: 1, }, { x: -4, y: 2, }, { x: 4, y: 0, }, { x: 4, y: 1, }, { x: 4, y: 2, }, ], sledders: [ { asset: 'images.lunchbox_sled', x: 0, y: 0, speech: [ { x: 0.3, y: 0.7, content: 'Wee…', direction: 'up', distance: 0.8, }, ], }, ], }, { name: 'Time Constant', nick: 'TIME_SLOPE_UP', biome: 'eternalCanyon', x: 10, y: 0, requirements: ['TIME_CONSTANT'], defaultExpression: '-\\frac{x}{3}+t', goals: [ { type: 'path', expression: 'x/2', x: 2, y: 0.5, pathX: 8, }, ], sledders: [ { asset: 'images.lunchbox_sled', speech: [ { x: 0.3, y: 0.7, content: 'Just the void.', direction: 'up-up-right', distance: 0.8, speech: [ { content: "But now there's nobody to talk to…", direction: 'up', distance: 0.8, }, ], }, ], }, ], }, { name: 'Time Translate Y', nick: 'TIME_PARABOLA_TRANSLATE_Y', biome: 'eternalCanyon', x: 10, y: 0, requirements: null, defaultExpression: '\\left(x-1\\right)^2+t-1', goals: [ { x: 3, y: 3, order: 'C', }, { x: 3, y: 0, order: 'B', }, { x: 3, y: -3, order: 'A', }, ], sledders: [ { x: 0, y: 0, asset: 'images.lunchbox_sled', }, ], }, { name: 'Time Translate X', nick: 'TIME_PARABOLA_TRANSLATE_X', biome: 'eternalCanyon', x: 0, y: 10, requirements: null, defaultExpression: '\\left(x-3-\\frac{t}{2}\\right)^2', goals: [ { x: 1.5, y: 0.5, order: 'C', }, { x: 2.5, y: 0.5, order: 'B', }, { x: 3.5, y: 0.5, order: 'A', }, ], sledders: [ { x: 0, y: 0, asset: 'images.lunchbox_sled', speech: [ { x: 0.3, y: 0.7, content: '*sigh*', color: '#FFF', direction: 'up', distance: 0.8, }, ], }, ], }, { name: 'Time Translate XY', nick: 'TIME_PARABOLA_TRANSLATE_X_Y', biome: 'eternalCanyon', x: 10, y: 0, requirements: [null], defaultExpression: '\\left(x-t-2\\right)^2+t', goals: [ { x: -3, y: -6, order: 'C', }, { x: 0, y: -3, order: 'B', }, { x: 3, y: 0, order: 'A', }, // { // type: 'path', // expression: 'x', // x: 2, // y: 2, // pathX: 6, // }, ], sledders: [ { x: 0, y: 0, asset: 'images.lunchbox_sled', speech: [ { x: 0.25, y: 0.7, content: 'Being alone sucks.', color: '#FFF', direction: 'up', distance: 1.5, }, ], }, ], }, { name: 'Time Hard', nick: 'TIME_HARD', biome: 'eternalCanyon', x: 0, y: 10, requirements: [null], defaultExpression: `4\\cdot \\sin \\left(x-\\sin \\left(\\frac{t}{2}\\right)\\cdot 11\\right)\\cdot \\cos \\left(\\frac{t}{3}\\right)+8\\cdot \\sin \\left(\\frac{t+\\sin \\left(t\\cdot e\\right)}{2}\\right)`, goals: [ { type: 'dynamic', x: 2, y: 1, order: 'A', }, { type: 'dynamic', x: 12, y: 4, order: 'B', }, { type: 'dynamic', x: -8, y: 4, order: 'B', }, { type: 'dynamic', x: -7, y: 4, order: 'C', }, { x: -2, y: 8, order: 'C', }, { x: -12, y: -4, order: 'B', }, ], texts: [ { x: 0, y: 16, size: 1, fill: '#fff', content: '(this level is impossible)', }, ], sledders: [ { x: 0, y: 0, asset: 'images.lunchbox_sled', speech: [ { x: 0.25, y: 0.7, content: 'ok lol this is too hard', direction: 'up', distance: 1, }, ], // speech: { // speakerX: 0.3, // speakerY: 0.8, // content: "I don't want to be a constant disappointment.", // direction: 'up', // distance: 2, // }, }, ], }, { name: 'time vertical oscillator', nick: 'TIME_SIN', biome: 'eternalCanyon', x: -20, y: 0, requirements: ['SIN_TRANSLATE_X_Y'], defaultExpression: '\\sin \\left(t\\right)', goals: [ { x: 0, y: -4, order: 'A', }, { x: 0, y: 4, order: 'B', }, ], sledders: [ { x: 0, y: 0, speech: [ { distance: 2, speakerX: 0.4, speakerY: 0.7, content: 'How do the islands float?', direction: 'up-up-right', align: 'right', }, { distance: 1.5, speakerX: -0.25, speakerY: 0.6, content: 'Noogler hats.', direction: 'up-left', }, ], }, ], }, { name: 'time parabola vertical oscillator', nick: 'TIME_PARABOLA_HORIZONTAL_OSCILLATOR', biome: 'eternalCanyon', x: -10, y: -10, requirements: null, defaultExpression: '\\left(x-2\\sin \\left(t\\right)\\right)^2', goals: [ { x: -5, y: 0, order: 'A', }, { x: 5, y: 0, order: 'B', }, ], sledders: [ { x: 0, y: 0, speech: [ { speakerX: 0.4, speakerY: 0.7, content: "Maybe they're ghost islands?", direction: 'up-up-right', distance: 2, }, ], }, ], }, { name: 'time parabola vertical oscillator', nick: 'TIME_PARABOLA_CIRCLE', biome: 'eternalCanyon', x: -20, y: 0, requirements: null, defaultExpression: '\\left(x-2\\sin \\left(t\\right)\\right)^2-2\\cos \\left(t\\right)', goals: [ { x: 0, y: 6, order: 'A', }, { x: 6, y: 0, order: 'B', }, { x: 0, y: -6, order: 'C', }, { x: -6, y: 0, order: 'D', }, ], sledders: [ { x: 0, y: 0, speech: [ { x: -0.2, y: 0.65, content: 'This place is cool.', direction: 'up-up-right', distance: 1, speech: [ { content: 'You were right.', direction: 'up', }, ], }, ], }, ], }, { name: 'time parabola vertical oscillator', nick: 'TIME_PARABOLA_WAVE', biome: 'eternalCanyon', x: 0, y: -10, requirements: null, defaultExpression: '-\\cos \\left(2x-2t\\right)+\\left(\\frac{x}{3}\\right)^2', goals: [ { x: 3, y: 6, type: 'dynamic', order: 'B', }, { type: 'path', expression: 'x', order: 'A', x: 6, y: 0, pathX: 4, }, ], sledders: [ { x: 0, y: 0, }, ], }, { name: 'time parabola vertical oscillator', nick: 'TIME_PARABOLA_RISER', biome: 'eternalCanyon', x: 0, y: -20, requirements: ['TIME_SIN'], defaultExpression: '\\left(\\frac{x}{4}\\right)^2-t-1', goals: [ { x: 4, y: 0, order: 'A', }, { x: 0, y: 4, order: 'B', }, ], sledders: [ { x: -4, y: 0, asset: 'images.ada_jack_sled', }, ], }, { name: 'time parabola negator', nick: 'TIME_PARABOLA_NEGATOR', biome: 'eternalCanyon', x: -10, y: -10, requirements: [null], defaultExpression: '\\left(\\frac{x}{4}\\right)^2\\cdot \\left(t-1\\right)', goals: [ { x: -9, y: 3, order: 'B', }, { x: -6, y: -1, order: 'A', }, { x: 0, y: 0, order: 'C', }, { x: 9, y: 3, order: 'B', }, { x: 6, y: -1, order: 'A', }, ], sledders: [ { x: 3, y: 0, asset: 'images.lunchbox_sled', }, { x: -3, y: 0, asset: 'images.sam_sled', }, ], }, { name: 'sin time oscillate parabola', nick: 'TIME_SIN_PARABOLA_OSCILLATOR', biome: 'eternalCanyon', x: -10, y: -10, requirements: ['TIME_PARABOLA_HORIZONTAL_OSCILLATOR'], defaultExpression: '\\left(\\frac{x}{4}-\\sin \\left(t\\right)\\right)^2', goals: [ { x: -6, y: 6, order: 'D', }, { x: -6, y: 0, order: 'C', }, { x: 6, y: 6, order: 'B', }, { x: 6, y: 0, order: 'A', }, ], sledders: [ { x: 0, y: 0, asset: 'images.lunchbox_sled', }, ], }, { name: 'sin time oscillate expand', nick: 'TIME_SIN_EXPANDING_OSCILLATOR', biome: 'eternalCanyon', x: 0, y: -10, requirements: [null], defaultExpression: '\\left(x-\\sin \\left(t\\right)\\right)^2', goals: [ { x: -2, y: 0, order: 'A', }, { x: -3, y: 0, order: 'B', }, { x: -4, y: 0, order: 'C', }, { x: 2, y: 0, order: 'A', }, { x: 3, y: 0, order: 'B', }, { x: 4, y: 0, order: 'C', }, ], sledders: [ { x: 0, y: 0, }, ], }, ] FILE: App_Levels_volcano.js const VOLCANO = (() => { const SLEDDER_START = -45 return { name: 'Volcano', nick: 'VOLCANO', colors: Colors.biomes.volcano, runAsCutscene: true, axesEnabled: false, radius: 3, x: -50, y: 0, camera: { x: 2, y: 0, }, victoryX: 760, requirements: ['TWO_BELOW'], defaultExpression: '-10+\\frac{10}{1+e^{\\frac{\\left(x-200\\right)}{16}}}+\\left(1-\\frac{1}{1+\\left(\\frac{x-150}{150}\\right)^{8}}\\right)\\cdot\\left(\\left(\\sin\\left(\\frac{x}{19}\\right)+2\\right)\\cdot\\sin\\left(\\frac{x}{11}\\right)+\\sin\\left(\\frac{x}{17}\\right)\\right)+\\frac{40}{1+\\left(\\frac{x-200}{100}\\right)^{6}}-\\frac{60}{1+\\left(\\frac{x-200}{30}\\right)^{6}}+\\frac{120}{1+e^{-\\frac{\\left(-x+60\\right)}{12}}}+\\left(\\frac{x-240}{100}\\right)^{2}', assets: { sounds: { music: { volcano: { start: '.mp3', middle_buildup: '.mp3', jump: '.mp3', middle_guitar: '.mp3', }, }, lava_monster_roar: '.mp3', }, images: { volcano_rock_1: '.webp', volcano_rock_2: '.webp', volcano_rock_4: '.webp', volcano_trees: '.webp', volcano_tree_1: '.webp', volcano_tree_2: '.webp', volcano_background: '.webp', volcano_background_tall: '.webp', volcano_background_medium: '.webp', // TODO: Clean up lavamonster background // and convert png -> webp lavamonster: 'lavamonster_no_bg.png', lavamonster_jaw: '.png', }, }, directors: [ { type: 'tracking', minFov: 8, minFovMargin: 7, transitions: [ { domain: [10, 135], properties: { verticalBias: 0, transitionSmoothing: 0.005, smoothing: 0.05, }, }, { domain: [135, 250], properties: { verticalBias: 0, minFov: 45, }, }, ], }, ], goals: [], sky: { asset: 'images.volcano_background_tall', margin: 1, }, bubble: { sky: { asset: 'images.western_slopes_background', margin: 1, }, }, dialogue: [ { speaker: 'Jack', color: '#FFF', content: 'See? The finish line!', direction: 'up-left', }, { speaker: 'Jack', content: '…must be where the little people are going.', color: '#FFF', direction: 'up-up-right', }, { speaker: 'Ada', color: '#FFF', content: "Shoot! They're barely ahead of us!", direction: 'up-up-left', }, { speaker: 'Ada', color: '#FFF', content: 'But not anymore.', direction: 'up-up-left', length: 4, speech: { speaker: 'Ada', color: '#FFF', content: 'We probably COULD have won.', direction: 'up', }, }, { speaker: 'Jack', color: '#FFF', content: '…I have a crazy idea.', direction: 'up-up-right', }, { speaker: 'Jack', color: '#FFF', content: 'So utterly, indubitably insane it just…', direction: 'up-up-right', }, { speaker: 'Jack', color: '#FFF', content: 'might…', direction: 'up-up-right', gap: 0, }, { speaker: 'Ada', color: '#FFF', content: 'Stop stalling! Tell me your stupid idea already.', direction: 'up-up-left', gap: 0.5, length: 4, }, { speaker: 'Jack', color: '#FFF', content: 'We go over it.', direction: 'up-up-left', speech: { speaker: 'Jack', color: '#FFF', content: "We don't go around the volcano.", direction: 'up', }, }, { speaker: 'Ada', color: '#FFF', content: 'Over it??', direction: 'up-up-left', }, { speaker: 'Jack', color: '#FFF', content: 'We jump it.', direction: 'up-up-left', gap: 1, }, { speaker: 'Ada', color: '#FFF', content: '…', direction: 'up-up-left', gap: 2, }, { speaker: 'Ada', color: '#FFF', content: 'That sounds dangerous.', direction: 'up-up-left', gap: 0, }, { speaker: 'Jack', color: '#FFF', content: 'You want to win, right?', direction: 'up-up-left', }, { speaker: 'Ada', color: '#FFF', content: "...let's go", direction: 'up-up-left', }, ], walkers: [ { // x: -50, x: -118, range: [-130, 10000], asset: 'images.jack_float', name: 'Jack', transition: { name: 'SLEDDER_1', toggleRunning: true, xRequirements: [SLEDDER_START], }, // range: [-34, 1], transitionX: 0, bobSpeed: 0.918218, speech: [], walkers: { x: -120, name: 'Ada', asset: 'images.ada_float', transition: { name: 'SLEDDER_1', xRequirements: [-33, -34.5], runningAfterTransition: true, }, // range: [-33, 0], transitionX: 0, speech: [], }, }, ], lava: { x: 155, y: 55, size: 90, }, sprites: [ { asset: 'images.volcano_tree_1', flipX: true, size: 10, x: -122, offset: [0, 0.9], anchored: true, }, { asset: 'images.volcano_rock_2', flipX: true, drawOrder: LAYERS.foreSprites, size: 3, x: -120, offset: [0, 0.0], anchored: true, }, { asset: 'images.volcano_tree_2', flipX: true, drawOrder: LAYERS.foreSprites, size: 10, x: -120, offset: [0, 0.9], anchored: true, }, { asset: 'images.volcano_rock_1', flipX: true, drawOrder: LAYERS.foreSprites, size: 2.5, x: -117, offset: [0, 0], anchored: true, }, { asset: 'images.volcano_tree_1', flipX: true, size: 10, x: -105, offset: [0, 0.9], anchored: true, }, { asset: 'images.volcano_rock_1', flipX: true, drawOrder: LAYERS.foreSprites, size: 2.5, x: -104.9, offset: [0, 0], anchored: true, }, { asset: 'images.big_rock', flipX: true, drawOrder: LAYERS.foreSprites, size: 7, x: SLEDDER_START - 2, offset: [0, 0.38], anchored: true, sloped: true, }, { asset: 'images.volcano_tree_1', flipX: true, drawOrder: LAYERS.foreSprites, size: 10, x: -93, offset: [0, 0.9], anchored: true, }, { asset: 'images.volcano_tree_2', flipX: true, drawOrder: LAYERS.foreSprites, size: 10, x: -89, offset: [0, 0.9], anchored: true, }, { asset: 'images.volcano_tree_2', flipX: true, drawOrder: LAYERS.foreSprites, size: 10, x: -58, offset: [0, 0.9], anchored: true, }, { asset: 'images.volcano_tree_1', flipX: true, drawOrder: LAYERS.foreSprites, size: 10, x: -54, offset: [0, 0.9], anchored: true, }, { asset: 'images.volcano_rock_1', flipX: true, drawOrder: LAYERS.foreSprites, size: 3.4, x: -88, offset: [0, 0], anchored: true, }, { asset: 'images.volcano_tree_1', flipX: true, size: 10, x: -69, offset: [0, 0.9], anchored: true, }, { asset: 'images.volcano_rock_1', flipX: true, drawOrder: LAYERS.foreSprites, size: 3.2, x: -88, offset: [0, 0.2], anchored: true, }, { asset: 'images.volcano_rock_1', flipX: true, drawOrder: LAYERS.foreSprites, size: 4.5, x: -87, offset: [0, 0.2], anchored: true, }, { asset: 'images.volcano_rock_1', flipX: true, drawOrder: LAYERS.foreSprites, size: 4.5, x: -93, offset: [0, 0.2], anchored: true, }, { asset: 'images.volcano_rock_1', flipX: false, drawOrder: LAYERS.foreSprites, size: 4.5, x: -95, offset: [0, 0.2], anchored: true, }, { asset: 'images.volcano_rock_1', flipX: true, drawOrder: LAYERS.foreSprites, size: 4, x: -83, offset: [0, 0.3], anchored: true, }, { asset: 'images.volcano_rock_1', flipX: true, drawOrder: LAYERS.foreSprites, size: 5, x: -82, offset: [0, 0.3], anchored: true, }, { asset: 'images.volcano_rock_1', flipX: true, drawOrder: LAYERS.foreSprites, size: 3, x: -53, offset: [0, 0.3], anchored: true, }, { asset: 'images.volcano_tree_2', flipX: true, drawOrder: LAYERS.foreSprites, size: 10, x: -10, offset: [0, 0.3], anchored: true, }, { asset: 'images.volcano_tree_1', flipX: true, drawOrder: LAYERS.foreSprites, size: 10, x: -15, offset: [0, 0.3], anchored: true, }, { asset: 'images.volcano_rock_1', flipX: true, drawOrder: LAYERS.foreSprites, size: 3, x: -8, offset: [0, 0.4], anchored: true, }, { asset: 'images.volcano_rock_1', flipX: true, drawOrder: LAYERS.foreSprites, size: 3, x: -10, offset: [0, 0.4], anchored: true, }, { asset: 'images.goalpost_left', drawOrder: LAYERS.backSprites, size: 8, x: 753.8, offset: [0, 0.25], anchored: true, }, { asset: 'images.goalpost_right', drawOrder: LAYERS.foreSprites, size: 6, x: 753, offset: [0, 0.6], anchored: true, }, { asset: 'images.volcano_tree_1', flipX: true, drawOrder: LAYERS.foreSprites, size: 10, x: 54, offset: [0, 0.6], anchored: true, }, { asset: 'images.volcano_tree_2', flipX: true, drawOrder: LAYERS.foreSprites, size: 10, x: 40, offset: [0, 0.6], anchored: true, }, { asset: 'images.volcano_tree_1', flipX: true, drawOrder: LAYERS.foreSprites, size: 10, x: 25, offset: [0, 0.6], anchored: true, }, { asset: 'images.volcano_tree_1', flipX: true, drawOrder: LAYERS.foreSprites, size: 10, x: 3, offset: [0, 0.3], anchored: true, }, { asset: 'images.volcano_tree_2', flipX: true, drawOrder: LAYERS.foreSprites, size: 12, x: 48, offset: [0, 0.6], anchored: true, }, { asset: 'images.volcano_tree_1', flipX: true, drawOrder: LAYERS.foreSprites, size: 12, x: 74, offset: [0, 0.6], anchored: true, }, { asset: 'images.volcano_tree_2', flipX: true, drawOrder: LAYERS.foreSprites, size: 12, x: 69, offset: [0, 0.6], anchored: true, }, { asset: 'images.dante', flipX: true, drawOrder: LAYERS.foreSprites, size: 1.6, x: -137.5, offset: [0, 1], anchored: true, }, { asset: 'images.big_rock', drawOrder: LAYERS.foreSprites, size: 3, x: -139, offset: [0, 0.38], anchored: true, sloped: true, }, { asset: 'images.volcano_rock_1', flipX: true, drawOrder: LAYERS.foreSprites, size: 2.5, x: -137.5, offset: [0, 0.4], anchored: true, }, { asset: 'images.volcano_tree_1', flipX: true, drawOrder: LAYERS.foreSprites, size: 12, x: 530, offset: [0, 0.2], anchored: true, }, { asset: 'images.volcano_rock_1', flipX: true, drawOrder: LAYERS.foreSprites, size: 2.5, x: 530.3, offset: [0, 0.9], anchored: true, }, { asset: 'images.benny_sam_sled', drawOrder: LAYERS.foreSprites, size: 3, x: 540, offset: [0, 1], anchored: true, sloped: true, }, { asset: 'images.nicky_sledders', drawOrder: LAYERS.foreSprites, size: 1.7, x: 620, offset: [0, 1], anchored: true, sloped: true, }, { asset: 'images.xkcd_sledders', drawOrder: LAYERS.foreSprites, size: 2, x: 700, offset: [0, 1], anchored: true, sloped: true, }, { asset: 'images.volcano_tree_2', flipX: true, drawOrder: LAYERS.foreSprites, size: 12, x: 590, offset: [0, 0.2], anchored: true, }, { asset: 'images.volcano_tree_2', flipX: true, drawOrder: LAYERS.foreSprites, size: 12, x: 620, offset: [0, 0.2], anchored: true, }, { asset: 'images.volcano_tree_1', flipX: true, drawOrder: LAYERS.foreSprites, size: 12, x: 650, offset: [0, 0.2], anchored: true, }, { asset: 'images.volcano_tree_1', flipX: true, drawOrder: LAYERS.foreSprites, size: 12, x: 680, offset: [0, 0.2], anchored: true, }, { asset: 'images.volcano_tree_1', flipX: true, drawOrder: LAYERS.foreSprites, size: 12, x: 710, offset: [0, 0.2], anchored: true, }, { asset: 'images.volcano_tree_1', flipX: true, drawOrder: LAYERS.foreSprites, size: 12, x: 740, offset: [0, 0.2], anchored: true, }, { asset: 'images.volcano_rock_1', flipX: false, drawOrder: LAYERS.foreSprites, size: 4.5, x: 670, offset: [0, -1], anchored: true, }, { asset: 'images.volcano_rock_2', flipX: false, drawOrder: LAYERS.foreSprites, size: 4.5, x: 750, offset: [0, 0.2], anchored: true, }, { asset: 'images.volcano_rock_1', flipX: false, drawOrder: LAYERS.foreSprites, size: 4.5, x: 570, offset: [0, 0.2], anchored: true, }, ], sledders: [ { name: 'SLEDDER_1', active: false, x: -45, }, ], backgroundMusic: { asset: 'sounds.music.volcano.start', volume: 1, domain: [-1001, -1000, SLEDDER_START, SLEDDER_START + 30], }, sounds: [ { asset: 'sounds.music.volcano.jump', loop: false, volume: 1, domain: [SLEDDER_START, SLEDDER_START + 30, 200, 240], track: 'sledders', }, // { // asset: 'sounds.music.volcano.middle_buildup', // loop: true, // volume: 1, // domain: [-3, 10, 100, 140], // track: 'sledders' // }, // { // asset: 'sounds.music.volcano.jump', // volume: 1, // domain: [120, 150], // track: 'sledders' // }, { asset: 'sounds.music.volcano.middle_guitar', volume: 1, domain: [240, 300], track: 'sledders', }, ], } })() FILE: App_Levels_wave.js const WAVE = [ { name: 'Sin', nick: 'SIN', biome: 'sinusoidalDesert', x: 20, y: -20, requirements: [ 'PARABOLA_TRANSLATE_SCALE_X_Y', 'TIME_PARABOLA_TRANSLATE_X_Y', ], defaultExpression: '\\sin \\left(x\\right)', slider: { expression: '$\\cdot \\sin \\left(x\\right)', bounds: [-1, 1, 1], }, goals: [ { type: 'path', expression: '-sin(x)', x: 1, y: 0, pathX: 2.5, }, { type: 'path', expression: '-sin(x)', x: TAU + 1, y: 0, pathX: 2.5, }, ], sledders: [ { asset: 'images.lunchbox_sled', x: 0, y: 0, speech: [ { x: 0.3, y: 0.65, content: 'Hey…', color: '#FFF', direction: 'up-up-right', distance: 1, }, ], }, { asset: 'images.sam_sled', x: TAU, y: 0, speech: [ { x: 0.3, y: 0.7, content: "You're late.", color: '#FFF', direction: 'up-up-left', distance: 1, }, ], }, ], }, { name: 'Cos', nick: 'COS', biome: 'sinusoidalDesert', x: 20, y: 0, requirements: [null], defaultExpression: '\\cos \\left(x\\right)', goals: [ { type: 'path', expression: 'cos(x)+6', x: 1, y: 0, pathX: 4.5, }, { type: 'path', expression: 'cos(x)+6', x: TAU + 1, y: 0, pathX: 4.5, }, ], sledders: [ { asset: 'images.sam_sled', x: 0, y: 0, speech: [ { x: 0.3, y: 0.65, content: 'I have waited *ten* hours.', color: '#FFF', direction: 'up-up-left', distance: 1.3, speech: { content: 'Ten hours.', color: '#FFF', direction: 'up', distance: 0.7, }, }, ], }, { asset: 'images.lunchbox_sled', x: TAU, y: 0, speech: [ { x: 0.1, y: 0.45, content: "…and I'm really sorry.", color: '#FFF', direction: 'up-left', distance: 0.8, speech: [ { y: 0.8, content: 'I know.', color: '#FFF', direction: 'up', distance: 0.6, }, ], }, ], }, ], }, DESERT, { name: 'Cos', nick: 'COS_SCALE_X_Y', biome: 'sinusoidalDesert', x: 10, y: -10, requirements: ['DESERT'], defaultExpression: '\\cos \\left(x\\right)', goals: [ { type: 'path', expression: '-cos(x/2)', x: -TAU + 1.5, y: 0, pathX: 4.5, }, { type: 'path', expression: '-cos(x/2)', x: TAU - 1.5, y: 0, pathX: -4.5, }, ], sledders: [ { asset: 'images.sam_sled', x: -TAU + 0.1, y: 0, speech: [ { x: 0.3, y: 0.65, content: "I'm still mad at you.", color: '#FFF', direction: 'up-up-right', distance: 1.2, }, ], }, { asset: 'images.lunchbox_sled', x: TAU - 0.1, y: 0, speech: [ { x: 0.3, y: 0.65, content: 'I know.', color: '#FFF', direction: 'up-up-left', distance: 0.8, }, ], }, ], }, { name: 'Sin Translate X', nick: 'SIN_TRANSLATE_X', biome: 'sinusoidalDesert', x: -10, y: -10, requirements: ['DESERT'], defaultExpression: '\\sin \\left(x+\\frac{pi}{4}\\right)', slider: { expression: '\\sin \\left(x+$\\right)', bounds: [-2, 1, 0], }, goals: [ { type: 'path', expression: 'cos(x)', x: 1, y: 0, pathX: 4, }, { type: 'path', expression: 'cos(x)', x: TAU + 1, y: 0, pathX: 4, }, ], sledders: [ { asset: 'images.sam_sled', x: 0, y: 0, speech: [ { x: 0.3, y: 0.65, content: "Yeah. We're not gonna win.", color: '#FFF', direction: 'up-right', distance: 1, speech: { direction: 'up-up-right', content: 'May as well take our time now.', distance: 0.8, color: '#FFF', }, }, ], }, { asset: 'images.lunchbox_sled', speech: [ { x: 0.3, y: 0.65, content: 'You sure?', color: '#FFF', direction: 'up-up-right', distance: 1.4, }, ], x: TAU, y: 0, }, ], }, { name: 'Sin Translate XY', nick: 'SIN_TRANSLATE_X_Y', biome: 'sinusoidalDesert', x: 0, y: -20, requirements: [null], defaultExpression: '\\cos \\left(x\\right)', goals: [ { type: 'path', expression: 'cos(x-1)+7', x: 2, y: 0, pathX: 4, }, { type: 'path', expression: 'cos(x-1)+7', x: TAU + 2, y: 0, pathX: 4, }, ], sledders: [ { asset: 'images.sam_sled', x: 1, y: 0, speech: [ { x: 0.3, y: 0.65, content: 'Certainly both.', color: '#FFF', direction: 'up-left', distance: 0.9, speech: { content: 'Somehow I identify with cacti…', distance: 1, color: '#FFF', direction: 'up-up-left', }, }, ], }, { asset: 'images.lunchbox_sled', speech: [ { x: 0.3, y: 0.65, color: '#FFF', content: 'Because of the spines, or the flowers?', direction: 'up-up-right', distance: 1.4, }, ], x: TAU + 1, y: 0, }, ], }, { name: 'Sin Scale X', nick: 'SIN_SCALE_X', biome: 'sinusoidalDesert', x: 0, y: -20, requirements: ['DESERT'], defaultExpression: '\\sin \\left(\\frac{x}{4}\\right)', slider: { expression: '\\sin \\left(\\frac{x}{$}\\right)', bounds: [2.5, 5, 4], }, goals: [ { type: 'path', expression: 'sin(x/2)', x: -1, y: 0, pathX: -5, }, { type: 'path', expression: 'sin(x/2)', x: TAU + 1, y: 0, pathX: 5, }, ], sledders: [ { asset: 'images.lunchbox_sled', speech: [ { x: 0.3, y: 0.65, content: 'Can we share a sled again?', color: '#FFF', direction: 'up-up-right', distance: 1.2, }, ], x: 0, y: 0, }, { asset: 'images.sam_sled', speech: [ { x: 0.3, y: 0.65, content: '…fine.', color: '#FFF', direction: 'up-up-left', distance: 0.8, }, ], x: TAU, y: 0, }, ], }, { name: 'Sin Scale XY', nick: 'SIN_SCALE_X_Y', biome: 'sinusoidalDesert', x: 0, y: -20, requirements: [null, 'SIN_TRANSLATE_X_Y'], defaultExpression: '\\sin \\left(\\frac{x}{2}\\right)', slider: { expression: '$\\cdot \\sin \\left(\\frac{x}{2}\\right)', bounds: [-2, 2, 1], }, goals: [ { type: 'path', expression: '-sin(x/3)*3', x: TAU - 1, y: 0, pathX: -5, }, { type: 'path', expression: '-sin(x/3)*3', x: TAU + 1, y: 0, pathX: 5, }, ], sledders: [ { x: (-TAU * 2) / 3, y: 0, speech: [ { x: -0.15, y: 0.6, content: 'This is cozy.', color: '#FFF', direction: 'up-up-right', distance: 1, }, ], }, ], }, { name: 'Sin Translate Scale XY', nick: 'SIN_TRANSLATE_SCALE_X_Y', biome: 'sinusoidalDesert', x: 10, y: 10, requirements: [null, 'COS_SCALE_X_Y'], defaultExpression: '3\\cos \\left(\\frac{x+3}{2}\\right)-1', goals: [ { type: 'path', expression: 'cos((x+4)/3)*2-4', x: -2, y: 0, pathX: 15, }, ], sledders: [ { x: -4, y: 0, speech: [ { speakerX: -0.25, speakerY: 0.6, content: 'Indeed.', color: '#FFF', direction: 'up-up-left', distance: 1.4, speech: { color: '#FFF', content: 'Love sure is complicated.', }, }, { speakerX: 0.4, speakerY: 0.65, content: 'Best faced together', color: '#FFF', // direction: 'up-right', direction: Vector2(0.5, 0.6), distance: 2.6, }, ], }, ], }, ] FILE: App_biomes.js const BIOMES = { home: { colors: Colors.biomes.home, clouds: {}, backgroundMusic: 'sounds.music.birds', snow: { density: 0.1, maxHeight: 8, velocity: { x: 0.2, y: 0.4, }, }, sky: { asset: 'images.initial_bg', margin: 1, }, assets: { sounds: { music: { birds: '.mp3', }, }, images: { initial_bg: 'initial-bg.webp', }, }, }, westernSlopes: { colors: Colors.biomes.alps, backgroundMusic: 'sounds.music.western_slopes_puzzle', sky: { asset: 'images.western_slopes_background', margin: 1, }, assets: { images: { western_slopes_background: '.webp', }, sounds: { music: { western_slopes_puzzle: '.mp3', }, }, }, }, valleyParabola: { colors: Colors.biomes.champlain, backgroundMusic: 'sounds.music.valley_parabola_puzzle', sky: { asset: 'images.valley_parabola_background', margin: 1, }, assets: { sounds: { music: { valley_parabola_puzzle: '.mp3', }, }, images: { valley_parabola_background: '.webp', }, }, }, eternalCanyon: { colors: Colors.biomes.gunnison, backgroundMusic: 'sounds.music.eternal_canyon_puzzle', sky: { asset: 'images.eternal_canyon_background', margin: 1, }, assets: { sounds: { music: { eternal_canyon_puzzle: '.mp3', }, }, images: { eternal_canyon_background: '.webp', }, }, }, sinusoidalDesert: { colors: Colors.biomes.arches, backgroundMusic: 'sounds.music.sinusoidal_desert_puzzle', sky: { asset: 'images.sinusoidal_desert_background', margin: 1, }, assets: { sounds: { music: { sinusoidal_desert_puzzle: '.mp3', }, }, images: { sinusoidal_desert_background: '.webp', }, }, }, logisticDunes: { colors: Colors.biomes.mojave, backgroundMusic: 'sounds.music.logistic_dunes_puzzle', sky: { asset: 'images.logistic_dunes_background', margin: 1, }, assets: { sounds: { music: { logistic_dunes_puzzle: '.mp3', }, }, images: { logistic_dunes_background: '.webp', }, }, }, hilbertDelta: { colors: Colors.biomes.everglades, backgroundMusic: 'sounds.music.hilbert_delta_puzzle', sky: { asset: 'images.hilbert_delta_background', margin: 1, }, assets: { sounds: { music: { hilbert_delta_puzzle: '.mp3', }, }, images: { hilbert_delta_background: '.webp', }, }, }, } FILE: App_colors.js const Colors = { biomes: { home: { sky: [ [0, '#3366ff'], [1, '#55aaff'], ], groundFill: '#371f40', groundPattern: '#1a0d1c', groundStroke: '#AD8CB7', groundStrokeWidth: 8, }, alps: { sky: [ [0, '#3366ff'], [1, '#55aaff'], ], groundFill: '#ffffff', groundPattern: '#118de677', groundStroke: '#222222', groundStrokeWidth: 8, }, volcano: { sky: [ [0, '#4488ff'], [1, '#66ddff'], ], groundFill: '#555555', groundPattern: '#222222', groundStroke: '#666666', groundStrokeWidth: 32, }, champlain: { sky: [ [0, '#8048ff'], [1, '#55aaff'], ], groundFill: '#5c845e', groundPattern: '#104642', groundStroke: '#beb759', groundStrokeWidth: 12, }, wetlands: { sky: [ [0, '#55aaff'], [1, '#88ccff'], ], groundFill: '#552211', groundPattern: '#118de677', groundStroke: '#bbbbbb', groundStrokeWidth: 16, }, yosemite: { sky: [ [0, '#55aaff'], [1, '#88ccff'], ], groundFill: '#115522', groundPattern: '#118de677', groundStroke: '#ccffcc', groundStrokeWidth: 24, }, mojave: { sky: [ [0, '#55aaff'], [1, '#4488ff'], ], groundFill: '#fff8dd', groundPattern: '#eecc66', groundStroke: '#EFDB95', groundStrokeWidth: 12, }, arches: { sky: [ [0, '#4488ff'], [1, '#66ccff'], ], groundFill: '#ffaa11', groundPattern: '#e06608', groundStroke: '#ffdd88', groundStrokeWidth: 12, }, gunnison: { sky: [ [0, '#4488ff'], [1, '#66ddff'], ], groundFill: '#566c78', groundPattern: '#2d383e', groundStroke: '#cfdee6', groundStrokeWidth: 12, }, everglades: { sky: [ [0, '#4488ff'], [1, '#66ddff'], ], groundFill: '#566c78', groundPattern: '#332d2f', groundStroke: '#302727', groundStrokeWidth: 12, }, }, } /* Colors.champlain = Colors.alps Colors.wetlands = Colors.alps Colors.gunnison = Colors.alps Colors.yosemite = Colors.alps Colors.mojave = Colors.alps Colors.gunnison = Colors.alps */ FILE: App_dinoegg.js if (IS_PRODUCTION) { console.log( '🥚 This dino egg is a sign from… %cHack Club 🦖', 'font-weight: bold; font-size: 25px; color: red; text-shadow: 1px 1px 0 rgb(217,31,38) , 2px 2px 0 rgb(226,91,14) , 3px 3px 0 rgb(245,221,8) , 4px 4px 0 rgb(5,148,68) , 5px 5px 0 rgb(2,135,206) , 6px 6px 0 rgb(4,77,145) , 7px 7px 0 rgb(42,21,113); margin-bottom: 4px; padding: 2%', '\nCLICK HERE 👉 ' + location.origin + '/App/dinoegg.js', ) console.log() /** * __ __ _______ _______ ___ _ _______ ___ __ __ _______ * | | | || _ || || | | | | || | | | | || _ | * | |_| || |_| || || |_| | | || | | | | || |_| | * | || || || _| | || | | |_| || | * | || || _|| |_ | _|| |___ | || _ | * | _ || _ || |_ | _ | | |_ | || || |_| | * |__| |__||__| |__||_______||___| |_| |_______||_______||_______||_______| * * __ __ __ __ * / _) / _) / _) / _) * _.----._/ / _.----._/ / _.----._/ / _.----._/ / * / / / / / / / / * __/ ( | ( | __/ ( | ( | __/ ( | ( | __/ ( | ( | * /__.-'|_|--|_| /__.-'|_|--|_| /__.-'|_|--|_| /__.-'|_|--|_| * * Congratulations! You found the dinosaur egg! You little rapscallion you... * * Are you a curious teenage developer, snooping around the innards of random websites? * Well, you're in luck! SineRider is an open source project created by teens like you * all over the world. Learn more and get involved at github.com/hackclub/sinerider * * SineRider is 100% inspectable. You can see all of our source code, including comments, * right here in the browser console. Have a look around! */ } FILE: App_eden.js worldData = [] worldData.push({ version: '0.0.0', name: 'Eden', assets: { images: { initial_bg: 'initial-bg.webp', western_slopes_background: '.webp', western_slopes_background_no_sky: '.webp', valley_parabola_background: '.webp', eternal_canyon_background: '.webp', sinusoidal_desert_background: '.webp', logistic_dunes_background: '.webp', hilbert_delta_background: '.webp', volcano_background_tall: '.webp', volcano_rock_1: '.webp', volcano_tree_1: '.webp', volcano_tree_2: '.webp', ada_sled: '.webp', jack_sled: '.webp', ada_jack_sled: '.webp', lunchbox_sam_sled: 'ada_jack_sled.webp', lunchbox_sled: 'jack_sled.webp', benny_sled: 'jack_sled.webp', jack_float: 'jack_float_right.webp', benny_float: 'benny_float.webp', sam_sled: 'ada_sled.webp', ada_float: 'ada_float_right.webp', sam_float: 'ada_float_right.webp', sam_stand_snowball: 'sam_float_left.svg', cabin_1: '.webp', world_map: '.svg', goalpost_left: '.webp', goalpost_right: '.webp', cloud_1: '.webp', cloud_2: '.webp', cloud_3: '.webp', cloud_4: '.webp', cloud_5: '.webp', cloud_6: '.webp', cloud_7: '.webp', cloud_8: '.webp', cloud_9: '.webp', cloud_10: '.webp', tree_1: '.webp', tree_2: '.webp', tree_3: '.webp', fox: '.webp', slime: '.webp', crow: '.webp', tree_home_1: '.webp', tree_home_2: '.webp', tree_home_3: '.webp', logo_text: '.webp', rock_1: '.webp', rock_2: '.webp', rock_3: '.webp', big_rock: '.webp', bush_1: '.webp', bush_2: '.webp', grass_1: '.webp', plant_1: '.webp', plant_2: '.webp', plant_3: '.webp', plant_4: '.webp', plant_5: '.webp', plant_clump_1: '.webp', plant_clump_2: '.webp', plant_clump_3: '.webp', plant_clump_4: '.webp', plant_clump_5: '.webp', grass_2: '.webp', tumbleweed: '.webp', cactus: '.webp', blooming_joshua_tree: '.webp', desert_tree: '.webp', dante: '.webp', windmill: '.png', lsys_red_joshua_tree: '.webp', nicky_sledders: '.webp', pi_dance_kick: '.webp', pi_confused: '.webp', xkcd_sledders: '.webp', benny_sam_sled: '.svg', credits: '.webp', }, sounds: { // sounds.music loaded in levels music: { birds: '.mp3', intro: '.mp3', }, map_button: 'map_button_compressed.mp3', next_button: 'next_button_compressed.mp3', enter_level: 'enter_level_compressed.mp3', goal_fail: { src: 'goal_fail_compressed.mp3', volume: 0.1, }, goal_success: 'goal_success_compressed.mp3', level_success: 'level_success_compressed.mp3', restart_button: 'restart_button_compressed.mp3', start_running: 'start_running_compressed.mp3', stop_running: 'stop_running_compressed.mp3', map_zoom_in: { src: 'woosh_out.mp3', rate: 1.2, }, map_zoom_out: { src: 'woosh_out.mp3', rate: 0.8, }, map_zoom_highlighted: { src: 'woosh_out.mp3', rate: 0.6, }, map_zoom_show_all: { src: 'woosh_out.mp3', rate: 0.4, }, path_goal_start: 'path_goal_start_compressed.mp3', path_goal_continue: 'path_goal_continue_compressed.mp3', }, shaders: { blend_frag: 'blend.frag', points_frag: 'points.frag', points_vert: 'points.vert', lake_frag: 'lake.frag', quad_frag: 'quad.frag', quad_vert: 'quad.vert', sunset_frag: 'sunset.frag', sunset_vert: 'sunset.vert', volcano: { gaussian_x: '.frag', gaussian_y: '.frag', output: '.frag', source: '.frag', lava: '.frag', volcano_blend: '.frag', volcano_stars_frag: 'volcano_stars.frag', volcano_stars_vert: 'volcano_stars.vert', volcano_sunset: '.frag', }, }, }, levelData: [ { name: 'Welcome', nick: 'HELLO_WORLD', biome: 'home', x: 0, y: 0, requirements: [], runMusic: 'sounds.music.intro', flashRunButton: true, defaultExpression: '\\frac{-2}{1+e^{-x+5}}+\\frac{-2}{1+\\left(x-30\\right)^2}', goals: [ { type: 'dynamic', x: 6.7, y: 0, }, ], sledders: [ { x: 0, asset: 'images.benny_sled', speech: { x: 0.4, y: 0.6, content: 'snow!!', direction: Vector2(0.5, 1), distance: 1, color: '#fff', }, }, ], walkers: [ { x: 7.6, victoryX: 10, followFlip: false, asset: 'images.sam_float', range: [7.6, 7.6], size: 2, flipX: true, sloped: true, speech: [ { x: -0.3, y: 0.7, content: 'now come, we have a race to win', direction: 'up-left', distance: 1, color: '#fff', speech: [ { x: -1, content: 'yes, snow.', direction: 'up-up-left', distance: 0.8, color: '#fff', speech: [], }, ], }, ], }, ], sprites: [ { asset: 'images.tree_home_1', drawOrder: LAYERS.backSprites, // flipX: '*', size: 6.7, x: -4, y: 0, offset: Vector2(0, 0.4), anchored: true, }, { asset: 'images.cabin_1', drawOrder: LAYERS.backSprites, flipX: '*', size: 6.1, x: -1, y: -1, offset: Vector2(0, 0.7), anchored: true, }, { asset: 'images.tree_home_2', drawOrder: LAYERS.foreSprites, // flipX: '*', size: 6.1, x: 2, y: 0, offset: Vector2(0, 0.5), anchored: true, }, { asset: 'images.tree_home_3', flipX: false, size: 6.4, x: 8, y: 0, offset: Vector2(0, 0.5), anchored: true, }, { asset: 'images.crow', flipX: '*', size: 1, x: 32, y: 0, anchored: true, }, { asset: 'images.logo_text', drawOrder: LAYERS.foreSprites, size: 20, x: 20, y: 15, anchored: false, }, { asset: 'images.windmill', drawOrder: LAYERS.background + 1, anchored: false, fixed: true, y: 0.204, x: 0.1215, size: 400, rotatingSpeed: 0.2, }, ], texts: [ { x: 15, y: -6, size: 1.5, fill: '#c4acd4', content: 'A game about love and graphing.', }, ], tips: [ { content: 'click here!', domSelector: '#run-button', place: 'top-left', style: { fontSize: '1.1rem' }, }, ], } /*{ name: 'Random', nick: 'RANDOM', colors: Colors.biomes.alps, x: 35, y: -25, requirements: [], defaultExpression: '0', hint: 'Soft eyes, grasshopper.', goals: [ { type: 'dynamic', x: 6.7, y: 0 }, ], sledders: [{ x: 0, }], },*/, ...SLOPE, ...EDITOR, //...POLAR, ...PARABOLA, ...WAVE, ...LOGISTIC, ...TIME, ...SWAMP, VOLCANO, CREDITS, ], }) // Allows you to leave requirements as null to signify dependence on the previous level for (world of worldData) { const levelData = world.levelData for (let i = 1; i < levelData.length; i++) { const d = levelData[i] if (d.requirements == null) { d.requirements = [levelData[i - 1].nick] continue } for (let j = 0; j < d.requirements.length; j++) { if (d.requirements[j] == null) { d.requirements[j] = [levelData[i - 1].nick] } } } } FILE: App_layers.js // Draw layer enum for layering convenience const LAYERS = { sky: -100, background: -90, axes: -80, backSprites: -10, walkers: 1, sledders: 2, foreSprites: 10, snow: 20, graph: 30, hintGraph: 40, clouds: 60, lighting: 70, gridlines: 79, goals: 80, text: 90, navigator: 100, speech: 110, map: 150, arrows: 160, levelBubbles: 170, darkness: 100, volcanoPostProcessing: 180, level: 10000, ui: Infinity, } FILE: App_main.js // Welcome to main.js, where we set up the SineRider engine basics const stepping = false // Core constants const ui = { menuBar: $('#menu-bar'), editButton: $('#edit-button'), levelText: $('#level-text'), levelButton: $('#level-button'), levelButtonString: $('#level-button > .string'), resetButton: $('#reset-button'), resetProgressConfirmationDialog: $('#reset-progress-confirmation-dialog'), resetProgressConfirmButton: $('#reset-progress-confirmation-yes'), resetProgressCancelButton: $('#reset-progress-confirmation-no'), resetConfirmationDialog: $('#reset-confirmation-dialog'), resetConfirmButton: $('#reset-confirmation-yes'), resetCancelButton: $('#reset-confirmation-no'), tryAgainButton: $('#try-again-button'), veil: $('#veil'), loadingVeil: $('#loading-veil'), loadingVeilString: $('#loading-string'), levelLoadingVeil: $('#level-loading-veil'), resetSolutionsString: $('#reset-string'), loadingProgressBarContainer: $('#loading-progress-bar-container'), loadingProgressBar: $('#loading-progress-bar'), twitterLinkRedirect: $('#twitter-link'), redditLinkRedirect: $('#reddit-link'), githubLinkRedirect: $('#github-link'), bubblets: $('.bubblets'), topBar: $('#top-bar'), navigatorButton: $('#navigator-button'), victoryBar: $('#victory-bar'), victoryLabel: $('#victory-label'), victoryLabelString: $('#victory-label > .string'), timeTaken: $('#time-taken'), charCount: $('#character-count'), nextButton: $('#next-button'), nextButtonText: $('#next-button-text'), messageBar: $('#message-bar'), messageBarString: $('#message-bar > .string'), tSliderContainer: $('#t-variable-container'), tSlider: $('#t-variable-slider'), variablesBar: $('#variables-bar'), timeString: $('#time-string'), completionTime: $('#completion-time'), redditOpenModal: $('#reddit-open-bar'), redditOpenCommand: $('#reddit-open-command'), redditOpenCloseButton: $('#close-reddit-open-button'), controlBar: $('#controls-bar'), expressionText: $('#expression-text'), expressionEnvelope: $('#expression-envelope'), mathFieldLabel: $('#variable-label > .string'), _mathField: $('#math-field'), mathField: $('#math-field'), mathFieldStatic: $('#math-field-static'), dottedMathContainer: $('#dotted-math-container'), dottedMathFieldStatic: $('#dotted-math-field-static'), dottedMathField: $('#dotted-math-field-static'), dottedSlider: $('#dotted-slider'), dottedHintButton: $('#dotted-math-button'), volumeSlider: $('#volume-slider'), variableLabel: $('#variable-label'), runButton: $('#run-button'), runButtonString: $('#run-button > .string'), stopButton: $('#stop-button'), stopButtonString: $('#stop-button > .string'), navigatorFloatingBar: $('#navigator-floating-bar'), showAllButton: $('#show-all-button'), showAllConfirmationDialog: $('#show-all-confirmation-dialog'), showAllConfirmButton: $('#show-all-yes'), showAllCancelButton: $('#show-all-no'), editorInspector: { panel: $('#editor-inspector'), inputs: { order: $('#editor-order-input'), timer: $('#editor-timer-input'), x: $('#editor-x-input'), y: $('#editor-y-input'), start: $('#editor-start-input'), end: $('#editor-end-input'), }, labels: { order: $('#editor-order-label'), timer: $('#editor-timer-label'), position: $('#editor-position-label'), start: $('#editor-start-label'), end: $('#editor-end-label'), }, deleteSelectionButton: $('#editor-inspector-delete'), }, editorSpawner: { panel: $('#editor-spawner'), addFixed: $('#editor-spawner-fixed'), addDynamic: $('#editor-spawner-dynamic'), addPath: $('#editor-spawner-path'), }, editorLevelConfigurationButton: $('#editor-level-configuration-button'), editorLevelConfigurationDialog: $('#editor-level-configuration-dialog'), // editorLevelConfigurationIsPolarCheckbox: $( // '#editor-level-configuration-is-polar-checkbox', // ), editorLevelConfigurationBiomeSelect: $( '#editor-level-configuration-biome-select', ), editorLevelConfigurationSledderSelect: $( '#editor-level-configuration-sledder-select', ), settingsButton: $('#settings-button'), graphicsSettingsDialog: $('#graphics-settings-dialog'), graphicsSettingsCloseButton: $('#graphics-settings-close-button'), setResolutionButton: $('#set-resolution-button'), setSampleDensityButton: $('#set-sample-density-button'), setTerrainLayersButton: $('#set-terrain-layers-button'), // closeGraphicsButton: $('#close-graphics-button'), levelInfoDiv: $('#lvl-debug-info'), levelInfoNameStr: $('#lvl-name-str'), levelInfoNickStr: $('#lvl-nick-str'), levelInfoFpsStr: $('#lvl-fps-str'), hideLevelInfoButton: $('#button-hide-level-info'), skipCutsceneButton: $('#skip-cutscene-button'), editorSharingLinkDialog: $('#editor-share-dialog'), // editorSharingLink: $('#editor-sharing-link'), // editorCopySharingLinkButton: $('#editor-copy-sharing-link-button'), shareButton: $('#share-button'), puzzleLink: $('#puzzle-link'), copyEditorLinkButton: $('#editor-copy-editor-link'), copyPuzzleLinkButton: $('#editor-copy-puzzle-link'), } ui.levelText.setAttribute('hide', true) ui.veil.setAttribute('hide', true) // Detect mobile devices and show the warning const ua = navigator.userAgent || navigator.vendor || window.opera let MOBILE = false if ( /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test( ua, ) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test( ua.substr(0, 4), ) ) { MOBILE = true } if (MOBILE) { alert( "This beta does not yet support mobile devices (we are working on that). Please use a device with a keyboard. We promise it's worth it!", ) } const canvas = $('#canvas') let canvasIsDirty = true const urlParams = new URLSearchParams(window.location.search) const ticksPerSecondOverridden = urlParams.has('ticksPerSecond') // 60 ticks per second default, but overridable via query param const ticksPerSecond = ticksPerSecondOverridden ? urlParams.get('ticksPerSecond') : 60 // This is deliberately decoupled from 'ticksPerSecond' such that we can keep consistent // predictable results while replaying the game simulation at higher-than-realtime speeds. const tickDelta = 1.0 / 60.0 const startTime = Date.now() // A positive integer that modifies how much the game draws. A setting of 1 would result // in all frames rendered, 2 would draw every other, etc... const drawModulo = urlParams.has('drawModulo') ? urlParams.get('drawModulo') : 1 const screen = Screen({ canvas, }) let w = worldData[0] const IS_PRODUCTION = true const IS_DEVELOPMENT = !IS_PRODUCTION // Don't show debug info in production if (IS_PRODUCTION) ui.levelInfoDiv.setAttribute('hide', true) let DEBUG_LEVEL_NICK = null // Stupid branch here to make sure DEBUG_LEVEL isn't set in prod if (IS_DEVELOPMENT) { // DEBUG_LEVEL_NICK = 'Level Editor' // DEBUG_LEVEL_NICK = 'VOLCANO' // DEBUG_LEVEL_NICK = 'CONSTANT_LAKE' // DEBUG_LEVEL_NICK = 'Two Below' // DEBUG_LEVEL_NICK = 'Time Hard' } const world = World({ ui, screen, requestDraw, tickDelta, drawOrder: NINF, ...worldData[0], }) var numTicks = 0 // Core methods function tick() { tickInternal() // setTimeout and rendering impose some minimum overhead as the delay approaches 0, and thus // it becomes very likely that our timer loop will fall behind our desired tick rate. // we will check that here and tick repeatedly until we catch up const elapsedMs = Date.now() - startTime const expectedTicks = (elapsedMs / 1000.0) * ticksPerSecond while (numTicks < expectedTicks) { tickInternal() } } function tickInternal() { numTicks++ world.awake() world.start() world.sendEvent('tick') if (numTicks % drawModulo == 0) { requestDraw() } } let timeOfLastDraw = null let currentFps = 60 function draw() { if (!canvasIsDirty) return canvasIsDirty = false let entity for (let i = 0; i < world.drawArray.length; i++) { entity = world.drawArray[i] if (entity.activeInHierarchy && entity.draw) { screen.ctx.save() if (entity.predraw) entity.predraw() entity.draw() screen.ctx.restore() } } let now = performance.now() if (timeOfLastDraw) { let frameFps = 1000 / (now - timeOfLastDraw) // Avoid counting double-ticks if (frameFps != Infinity) { currentFps = 0.9 * currentFps + 0.1 * frameFps ui.levelInfoFpsStr.innerText = 'FPS: ' + currentFps.toFixed(2) } } timeOfLastDraw = now } function requestDraw() { if (!canvasIsDirty) { canvasIsDirty = true requestAnimationFrame(draw) } } tick() draw() if (!stepping) { setInterval(tick, 1000 / ticksPerSecond) } // T Parameter Slider ui.tSlider.addEventListener('input', refreshTSlider) function refreshTSlider() { if (world.globalScope && !world.running) { const newT = math.remap(0, 100, 0, 10, Number(ui.tSlider.value)) world.level?.sendEvent('tVariableChanged', [newT]) } } // MathQuill ui.mathFieldStatic = MQ.StaticMath(ui.mathFieldStatic) function createMathField(field, eventNameOnEdit) { field = MQ.MathField(field, { handlers: { edit: function () { const text = field.getPlainExpression() const latex = field.latex() world.level.sendEvent(eventNameOnEdit, [text, latex]) }, }, }) field.getPlainExpression = function () { var tex = field.latex() return mathquillToMathJS(tex) } return field } ui.mathField = createMathField(ui.mathField, 'setGraphExpression') ui.mathField.focused = () => ui._mathField.classList.contains('mq-focused') ui.dottedMathFieldStatic = MQ.StaticMath(ui.dottedMathFieldStatic) function onMathFieldFocus(event) { world.onMathFieldFocus() } ui.expressionEnvelope.addEventListener('focusin', onMathFieldFocus) function onMathFieldBlur(event) { world.onMathFieldBlur() } ui.expressionEnvelope.addEventListener('blurout', onMathFieldBlur) // HTML events function onKeyUp(event) { if (event.key === 'Enter') { if (!world.navigating && !world.level?.isCutscene) world.toggleRunning() } world.sendEvent('keyup', [event.key]) } window.addEventListener('mousewheel', (event) => { world.sendEvent('onMouseWheel', [event]) }) window.addEventListener('keydown', (event) => { if (ui.mathField.focused()) return // world.level?.sendEvent('keydown', [event.key]) world.sendEvent('keydown', [event.key]) }) window.addEventListener('keyup', onKeyUp) function onExpressionTextChanged(event) { world.level.sendEvent('setGraphExpression', [ui.expressionText.value]) } function setGlobalVolumeLevel(i) { Howler.volume(i) window.localStorage.setItem('volume', i) } function onSetVolume(event) { let volume = event.target.value / 100 setGlobalVolumeLevel(volume) } ui.volumeSlider.addEventListener('change', onSetVolume) ui.volumeSlider.addEventListener('mouseup', onSetVolume) ui.volumeSlider.addEventListener('input', onSetVolume) function onClickHint() { ui.dottedHintButton.style.display = 'none' ui.dottedSlider.hidden = false ui.dottedSlider.style.innerHeight = '200px' ui.dottedMathField.style.display = 'block' world.level.sendEvent('displayDottedGraph') } ui.dottedHintButton.addEventListener('click', onClickHint) // // prevent twitter link click from triggering level click // ui.twitterLinkRedirect.addEventListener('click', function (event) { // event.stopPropagation() // }) // // prevent reddit link click from triggering level click // ui.redditLinkRedirect.addEventListener('click', function (event) { // event.stopPropagation() // }) // prevent github link click from triggering level click ui.githubLinkRedirect.addEventListener('click', function (event) { event.stopPropagation() }) // Initial page state function initPageState() { let volume = window.localStorage.getItem('volume') if (volume) { setGlobalVolumeLevel(Number(volume)) ui.volumeSlider.value = volume * 100 } setGlobalVolumeLevel(ui.volumeSlider.value / 100) } function onClickMapButton(event) { world.onClickMapButton() requestDraw() } ui.levelButton.addEventListener('click', onClickMapButton) ui.navigatorButton.addEventListener('click', onClickMapButton) function onClickNextButton(event) { world.onClickNextButton() } ui.nextButton.addEventListener('click', onClickNextButton) function onClickRunButton(event) { if (!world.level?.isRunningAsCutscene && !world.navigating) world.toggleRunning() return true } // TODO: Encapsulate run/stop/victory button behavior (Entity?) ui.runButton.addEventListener('click', onClickRunButton) ui.stopButton.addEventListener('click', onClickRunButton) ui.tryAgainButton.addEventListener('click', onClickRunButton) function onClickShowAllButton(event) { let showall = localStorage.getItem('ShowAll') if (showall != 'True') { showDialog(ui.showAllConfirmationDialog) } else { onShowAllConfirm() } } ui.showAllButton.addEventListener('click', onClickShowAllButton) /* Dialogs */ function makeDialogCloseable(dialog) { dialog.addEventListener('click', (event) => { const x = event.clientX const y = event.clientY //access the visible element's bounding rectangle const rect = dialog.getBoundingClientRect() const outsideModal = !pointInRect(x, y, rect) if (outsideModal) { closeDialog(dialog) } }) } function pointInRect(x, y, rect) { return ( x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height ) } function makeButtonToggleDialog(button, dialog) { button.addEventListener('click', () => { if (dialog.open) { closeDialog(dialog) } else { showDialog(dialog) } }) } function showDialog(dialog) { dialog.classList.remove('hidden') dialog.showModal() } function closeDialog(dialog) { dialog.classList.add('hidden') dialog.close() } function makeButtonOpenDialog(button, dialog) { button.addEventListener('click', () => showDialog(dialog)) } function makeButtonCloseDialog(button, dialog) { button.addEventListener('click', () => closeDialog(dialog)) } makeDialogCloseable(ui.editorSharingLinkDialog) makeDialogCloseable(ui.graphicsSettingsDialog) makeDialogCloseable(ui.editorLevelConfigurationDialog) makeButtonCloseDialog(ui.graphicsSettingsCloseButton, ui.graphicsSettingsDialog) makeButtonOpenDialog(ui.settingsButton, ui.graphicsSettingsDialog) // makeButtonCloseDialog(ui.closeGraphicsButton, ui.graphicsSettingsDialog) makeButtonOpenDialog( ui.editorLevelConfigurationButton, ui.editorLevelConfigurationDialog, ) function onClickEditButton(event) { world.editing = !world.editing } ui.editButton.addEventListener('click', onClickEditButton) function onClickResetButton(event) { showDialog(ui.resetConfirmationDialog) } ui.resetButton.addEventListener('click', onClickResetButton) function onResetConfirm() { world.onResetConfirm() closeDialog(ui.resetConfirmationDialog) } ui.resetConfirmButton.addEventListener('click', onResetConfirm) function onResetCancel() { closeDialog(ui.resetConfirmationDialog) } ui.resetCancelButton.addEventListener('click', onResetCancel) function onShowAllConfirm() { world.navigator.showAll = !world.navigator.showAll closeDialog(ui.showAllConfirmationDialog) window.localStorage.setItem('ShowAll', 'True') } ui.showAllConfirmButton.addEventListener('click', onShowAllConfirm) function onShowAllCancel() { closeDialog(ui.showAllConfirmationDialog) } ui.showAllCancelButton.addEventListener('click', onShowAllCancel) function onResizeWindow(event) { world.sendEvent('resize', [window.innerWidth, window.innerHeight]) screen.resize() canvasIsDirty = true draw() } window.addEventListener('resize', onResizeWindow) /* Graphics setting buttons */ const resolutionSelector = { storage: 'resolutionSetting', default: 2, applySettings: (option) => { screen.resolutionScalingFactor = option[1] screen.resize() }, options: [ ['Low', 0.5], ['Medium', 0.75], ['High', 1.0], ], } const sampleDensitySelector = { storage: 'sampleDensitySetting', default: 1, applySettings: (option) => { world.sampleDensitySetting = option[1] }, options: [ ['Low', 60], ['Medium', 129], ['High', 300], ], } const terrainLayersSelector = { storage: 'terrainLayersSetting', default: 2, applySettings: (option) => { if (world.level?.graph) world.level.graph.terrainLayers = option[1].terrainLayers }, options: [ [ 'Low', { terrainLayers: 0, }, ], [ 'Medium', { terrainLayers: 3, }, ], [ 'High', { terrainLayers: 6, }, ], ], } function createToggleSelector(element, selector) { selector.selection = localStorage.getItem(selector.storage) ?? selector.default let choice = selector.options[selector.selection] element.innerText = choice[0] selector.applySettings(choice) return () => { selector.selection = (selector.selection + 1) % selector.options.length localStorage.setItem(selector.storage, selector.selection) choice = selector.options[selector.selection] element.innerText = choice[0] selector.applySettings(choice) } } ui.setResolutionButton.addEventListener( 'click', createToggleSelector(ui.setResolutionButton, resolutionSelector), ) ui.setSampleDensityButton.addEventListener( 'click', createToggleSelector(ui.setSampleDensityButton, sampleDensitySelector), ) ui.setTerrainLayersButton.addEventListener( 'click', createToggleSelector(ui.setTerrainLayersButton, terrainLayersSelector), ) function onClickCanvas() { if (stepping) { tick() } } canvas.addEventListener('click', onClickCanvas) ui.veil.addEventListener('click', onClickCanvas) function onMouseMoveCanvas(event) { world.clickableContext.processEvent(event, 'mouseMove') event.preventDefault() } function selectScreenCoordinatesFromWindow(windowX, windowY) { const screenX = screen.resolutionScalingFactor * windowX const screenY = screen.resolutionScalingFactor * windowY world.sendEvent('selectScreenCoordinates', [screenX, screenY]) } function onMouseMoveWindow(event) { selectScreenCoordinatesFromWindow(event.clientX, event.clientY) } canvas.addEventListener('mousemove', onMouseMoveCanvas) canvas.addEventListener('pointermove', onMouseMoveCanvas) window.addEventListener('mousemove', onMouseMoveWindow) window.addEventListener('pointermove', onMouseMoveWindow) function onMouseDownCanvas(event) { world.clickableContext.processEvent(event, 'mouseDown') event.preventDefault() selectScreenCoordinatesFromWindow(event.clientX, event.clientY) ui.mathField.blur() world.sendEvent('onMouseDown') } canvas.addEventListener('mousedown', onMouseDownCanvas) canvas.addEventListener('pointerdown', onMouseDownCanvas) function onMouseUpCanvas(event) { event.preventDefault() ui.tSlider.value = 0 // refreshTSlider() world.clickableContext.processEvent(event, 'mouseUp') world.sendEvent('onMouseUp') } canvas.addEventListener('mouseup', onMouseUpCanvas) window.addEventListener('pointerup', onMouseUpCanvas) ui.levelInfoDiv.addEventListener('mouseover', function () { ui.hideLevelInfoButton.setAttribute('hide', false) }) ui.levelInfoDiv.addEventListener('mouseleave', function () { ui.hideLevelInfoButton.setAttribute('hide', true) }) /* Editor UI */ function bindElementDOMEventToWorldEvent( element, domEventName, worldEventName, ) { element.addEventListener(domEventName, (event) => world.sendEvent(worldEventName, [event]), ) } /* Init configuration menus */ // TODO: Maybe use DOM prototypes? UI management in general // needs rewrite for (const biomeName of Object.keys(BIOMES)) { const option = document.createElement('option') option.innerText = biomeName.charAt(0).toUpperCase() + biomeName.substring(1).replace(/([A-Z])/g, ' $1') option.value = biomeName ui.editorLevelConfigurationBiomeSelect.add(option) } for (const [sledderName, sledderImage] of [ ['Ada', 'images.ada_sled'], ['Jack', 'images.jack_sled'], ['Ada & Jack', 'images.ada_jack_sled'], ]) { const option = document.createElement('option') option.innerText = sledderName option.value = sledderImage ui.editorLevelConfigurationSledderSelect.add(option) } ui.editorLevelConfigurationBiomeSelect.addEventListener('change', () => { const selectedIndex = ui.editorLevelConfigurationBiomeSelect.selectedIndex const options = ui.editorLevelConfigurationBiomeSelect.options const selectedBiomeKey = options[selectedIndex].value world.sendEvent('selectBiome', [selectedBiomeKey]) }) // ui.editorLevelConfigurationIsPolarCheckbox.addEventListener('input', () => { // const checked = ui.editorLevelConfigurationIsPolarCheckbox.checked // world.sendEvent('setPolar', [checked]) // }) ui.editorLevelConfigurationSledderSelect.addEventListener('input', () => { const selectedIndex = ui.editorLevelConfigurationSledderSelect.selectedIndex const options = ui.editorLevelConfigurationSledderSelect.options const selectedSledderImage = options[selectedIndex].value world.sendEvent('setSledderImage', [selectedSledderImage]) }) /* Events */ ui.shareButton.addEventListener('click', () => { world.sendEvent('onShareButtonClicked') }) ui.copyEditorLinkButton.addEventListener('click', () => { const link = window.location.href navigator.clipboard.writeText(link) }) ui.copyPuzzleLinkButton.addEventListener('click', () => { const link = ui.puzzleLink.value navigator.clipboard.writeText(link) }) // Spawner bindElementDOMEventToWorldEvent( ui.editorSpawner.addDynamic, 'click', 'onAddDynamicClicked', ) bindElementDOMEventToWorldEvent( ui.editorSpawner.addFixed, 'click', 'onAddFixedClicked', ) bindElementDOMEventToWorldEvent( ui.editorSpawner.addPath, 'click', 'onAddPathClicked', ) // Inspector for (const [name, input] of Object.entries(ui.editorInspector.inputs)) { input.addEventListener('input', (event) => world.sendEvent('onEditorInput', [name, event]), ) } ui.editorInspector.deleteSelectionButton.addEventListener('click', () => { world.sendEvent('deleteSelection') }) FILE: App_style.css * { -webkit-tap-highlight-color: rgba(0, 0, 0, 0); -webkit-tap-highlight-color: transparent; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } body { position: absolute; top: 0; left: 0; right: 0; bottom: 0; margin: 0; padding: 0; font-family: 'Roboto Mono'; color: #333; background: #fff; font-size: 18px; overflow: hidden; user-select: none; } html { overflow: hidden; } #canvas { user-select: none; width: 100vw; height: 100vh; } .map { /* transform: scale(1.42) rotateX(40deg) rotateY(-4deg); */ } #graphics-settings-dialog { display: flex; flex-direction: column; gap: 10px; } #graphics-settings-close-button{ position: absolute; color: rgb(0, 0, 0); width: 1px; height: 1px; text-align: center; justify-content: center; align-items: center; top: 5px } #graphics-settings-close-button:hover{ cursor: pointer; color: rgb(255, 0, 0); } .bar { margin: 0; padding: 4px; background: #eee; border-radius: 5px; overflow: hidden; display: flex; align-items: stretch; justify-content: center; transition: 0.2s; } body > .bar { position: absolute; left: 20px; right: 20px; } .bar > *:last-child { margin: 0; } .bar .envelope { height: 100%; display: flex; flex-direction: row; border-radius: 5px; } .floating { background: transparent; padding: 0; border-radius: 0; overflow: visible; } .floating .button, .envelope { box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.4); } .hackclub-logo { height: 5.642rem; width: 10rem; } .string { user-select: none; } .label { min-width: 30px; min-height: 30px; display: flex; align-items: center; justify-content: center; } .text { color: #333; min-height: 28px; border-radius: 5px; padding: 0 4px; font-size: 14px; transition: 0.2s; } .text:disabled { border: none; opacity: 1; } .button { min-width: 30px; min-height: 30px; padding: 0 8px; background: #fff; border-radius: 5px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: 0.2s; } .button:hover { background: #222; color: #fff; } .disabled { pointer-events: none; background: rgb(125, 125, 125) !important; } .button * { user-select: none; pointer-events: none; } #math-editor-container { display: grid; justify-items: center; } *[hide='true'] { display: none; } .menu { display: none; position: absolute; top: 0; border-top-left-radius: 0; border-top-right-radius: 0; } .menu[hide='true'] { top: -60px; } .victory { position: absolute; display: flex; background: transparent; bottom: 20px; transition: 0.5s; pointer-events: none; } .reddit-open { position: absolute; display: flex; background: transparent; bottom: 20px; transition: 0.5s; pointer-events: none; } .victory .envelope { flex-basis: 400px; box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.4); background: #eee; padding: 5px; pointer-events: all; } .reddit-open .envelope { flex-basis: 400px; box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.4); background: #eee; padding: 5px; pointer-events: all; } .victory[hide='true'] { bottom: -300px; } .puzzle-msg[hide='true'] { display: none; } .reddit-open[hide='true'] { bottom: -300px; } .message[hide='true'] { display: none; } .navigator { position: absolute; bottom: 60px; } .navigator[hide='true'] { bottom: -60px; display: none; } input { font-family: 'Roboto Mono'; } input:placeholder-shown { font-style: italic; } input[valid='false'] { border: 1px solid red; } #controls-bar { position: absolute; left: 20px; bottom: 20px; right: 20px; align-self: center; align-items: flex-end; justify-content: center; /*box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.4);*/ } #controls-bar[hide='true'] { bottom: -50vh; } .hbar { display: flex; flex-direction: row; } #expression-text { display: none; flex-grow: 1; } #math-field, #math-field-static { border: none; flex-grow: 0; padding: 4px 2px 2px 2px; /*margin: 4px 4px 4px 0px;*/ border-radius: 0 5px 5px 0; margin: 0; background: #fff; } #math-field[valid='false'] { border: 1px solid red; } #expression-envelope { width: fit-content; transition: transform 1s, opacity 0.2s; } #expression-envelope[disabled='true'] > #math-field { display: none; } #expression-envelope[disabled='false'] > #math-field-static { display: none; } #flag { left: 1rem; position: absolute; top: 0; } #math-field-static { background: #ddd; } #expression-envelope[disabled='true'] { background: #ddd; opacity: 0.4; } #expression-envelope[disabled='false'] { background: #fff; } #expression-envelope[hide='true'] { transform: translate(0, 200%); opacity: 0; } #editor-copy-puzzle-link { background: rgb(116, 146, 255); } #editor-copy-editor-link { background: rgb(255, 162, 116); } #share-button { background: rgb(116, 146, 255); } #run-button { position: absolute; right: 0; bottom: 0; } #try-again-button { animation: pulse 0.5s infinite; } #skip-cutscene-button { position: absolute; left: 0; bottom: 0; } /* TODO: Make copy link dialog for modal look decent */ #editor-share-dialog > .hbar { align-items: center; gap: 20px; padding: 0px 10px; max-width: 100%; margin-top: 10px; } #editor-sharing-link { width: 300px; overflow: scroll; } #editor-copy-sharing-link-button { background: rgb(99, 99, 255); } .reset-button { position: absolute; right: 0; bottom: 0; } #reset-confirmation-dialog, #show-all-confirmation-dialog { display: flex; flex-direction: column; justify-content: start; align-items: center; font-size: 3vmin; } #reset-confirmation-dialog > div, #reset-progress-confirmation-dialog > div, #show-all-confirmation-dialog > div { margin: 5px 0px; width: 100%; display: flex; justify-content: space-evenly; align-items: center; } #reset-confirmation-yes, #reset-progress-confirmation-yes, #show-all-yes { background: #f46; color: white; border-width: 2px; border-style: solid; border-color: #f46; } #reset-confirmation-yes:hover, #reset-progress-confirmation-yes:hover, #show-all-yes:hover { background: #222; border-color: #000; } #reset-confirmation-no, #reset-progress-confirmation-no, #show-all-no { border-width: 2px; border-style: solid; border-color: #000; } #run-button:not(:hover) { background: #4f6; } .reset-button:not(:hover), #skip-cutscene-button:not(:hover) { background: #f46; } /* TODO: Clean up settings button, use existing CSS */ #settings-button { margin-top:8px; margin-left:8px; padding: 0 8px; background: transparent; border-radius: 5px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: 0.2s; font-size: 2.4rem; } #next-button:not(:hover) { background: #4f6; } #close-reddit-open-button:not(:hover) { background: #f46; } #victory-stop-button:not(:hover) { background: #f46; } #reset-button:not(:hover) { background: #68f; } #reset-button { position: absolute; left: 0; bottom: 0; padding: 0; } #reset-button > .string { font-size: 24px; line-height: 14px; padding-bottom: 4px; } #level-text { text-align: center; } #level-button { flex-grow: 1; } #victory-label { flex-grow: 1; font-size: 14px; } #victory-text { margin: 0; text-align: center; font-size: 21px; } #victory-label .string { text-align: center; } .puzzle-msg { display: inline; } .hovertext { position: relative; display: inline-block; border-bottom: 1px dotted black; } .hovertext:before { content: attr(data-hover); visibility: hidden; opacity: 0; width: max-content; font-size: 13px; background-color: black; color: #fff; text-align: center; border-radius: 5px; padding: 5px 5px; transition: opacity 1s ease-in-out; z-index: 1; position: absolute; bottom: 100%; left: -150%; } .hovertext:hover:before { opacity: 1; visibility: visible; } #variables-bar { position: absolute; top: 20px; left: auto; right: 20px; font-size: 24px; justify-content: flex-end; transition: 0.5s; } #variables-bar[hide='true'] { top: -80px; } #time-string { color: #c44; } #top-bar { position: absolute; top: 0px; left: 0px; right: 0px; display: flex; justify-content: space-between; } #fixed-goal-selector, #dynamic-goal-selector { height: 70px; width: 70px; background: white; transition: 0.05s; } #volume-slider { opacity: 1; transition-duration: 1s; } #top-bar[hide='true'] { top: -80px; } #navigator-button { margin-right: 20px; border-radius: 0 0 10px 10px; font-size: 2.4rem; } #navigator-button > .string { padding-bottom: 5px; } #navigator-floating-bar { bottom: 0; justify-content: flex-start; } #show-all-button { border-radius: 10px 10px 0 0; cursor: pointer; } #show-all-button:hover { background-color: #f46; } #show-all-button > img { width: 20px; height: auto; margin: 0 5px; } #github-string > p { font-size: small; } /* #loading-remarks { max-width: 80vmin; } #loading-remarks > h3 { text-align: center; } */ #main-loading { display: flex; flex-direction: column; justify-content: center; align-items: center; width: 100vmin; } .loading-image { width: 100%; height: auto; display: block; margin: 0 auto; max-width: 50vmin; } #reset-string { text-align: center; padding: 0.375rem 0.75rem; border: 1px solid #000; cursor: pointer; font-size: 0.7em; } #loading-string { text-decoration: underline; cursor: pointer; padding: 0.375rem 0.75rem; } #loading-top { display: flex; flex-direction: column; align-items: center; } #loading-progress-bar-container { margin: 20px auto; width: 25%; height: 20px; background-color: #ccc; } #loading-progress-bar { height: 100%; background-color: #ec3750; width: 0%; } #loading-bottom { position: absolute; bottom: 20px; left: 0; right: 0; margin: 0 auto; width: 100%; max-width: 80vmin; padding: 10px 10%; text-align: center; } #puzzles { display: inline-flex; flex-wrap: wrap; justify-content: space-evenly; align-items: center; } #puzzles > h3 { text-align: center; font-size: 16px; } #loading-links { display: flex; justify-content: space-evenly; align-items: center; } .loading-link { display: inline-block; font-weight: 400; text-align: center; vertical-align: middle; user-select: none; } .puzzle-link-image { margin-left: 10px; width: 50%; height: 50%; object-fit: cover; } #twitter-link:hover, #reddit-link:hover { transform: scale(1.1); } #github-link { display: inline-block; color: #ec3750; position: relative; font-size: small; text-decoration: none; } #github-link:hover { text-decoration: underline; } .background { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: inherit; z-index: -1; } .bubblets { display: none; } .veil { position: absolute; top: 0; left: 0; right: 0; bottom: 0; opacity: 1; transition: 1s; background: white; pointer-events: all; display: flex; flex-direction: column; align-items: center; justify-content: center; } .veil-no-transition { transition: 0s !important; } @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } } .flash-shadow { animation-duration: 1s; animation-name: flash-shadow; animation-iteration-count: infinite; } @keyframes flash-shadow { from { box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.4); } 25% { box-shadow: 0px 2px 4px 3px rgba(0, 192, 255, 0.6); } to { box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.4); } } .veil[hide='true'] { opacity: 0; pointer-events: none; } .loading { transition: 3s; } /* Loading screen dot animations. From https://codepen.io/nzbin/pen/GGrXbp */ .dot-flashing { --dot-color: black; position: relative; width: 10px; height: 10px; border-radius: 5px; background-color: var(--dot-color); color: var(--dot-color); animation: dot-flashing 1s infinite linear alternate; animation-delay: 0.5s; } .dot-flashing::before, .dot-flashing::after { content: ""; display: inline-block; position: absolute; top: 0; } .dot-flashing::before { left: -15px; width: 10px; height: 10px; border-radius: 5px; background-color: var(--dot-color); color: var(--dot-color); animation: dot-flashing 1s infinite alternate; animation-delay: 0s; } .dot-flashing::after { left: 15px; width: 10px; height: 10px; border-radius: 5px; background-color: var(--dot-color); color: var(--dot-color); animation: dot-flashing 1s infinite alternate; animation-delay: 1s; } @keyframes dot-flashing { 0% { background-color: var(--dot-color); } 50%, 100% { background-color: rgba(0, 0, 0, 0.2); } } /* End of loading screen dot animations */ .envelope { position: relative; } .envelope:focus-within { -webkit-box-shadow: #8bd 0 0 1px 2px, inset #6ae 0 0 2px 0; -moz-box-shadow: #8bd 0 0 1px 2px, inset #6ae 0 0 2px 0; box-shadow: #8bd 0 0 1px 2px, inset #ffffff 0 0 2px 0; border-color: #ffffff; } .invalid-exp { -webkit-box-shadow: #f00 0 0 1px 2px, inset #6ae 0 0 2px 0 !important; -moz-box-shadow: #f00 0 0 1px 2px, inset #6ae 0 0 2px 0 !important; box-shadow: #f00 0 0 1px 3px, inset #ffffff 0 0 3px 0 !important; border-color: #ffffff !important; } .helper-bubble { width: 150px; text-align: center; border: 2px solid black; font-size: small; position: absolute; background-color: white; padding: 5px; color: black !important; animation-name: wiggle; animation-duration: 4500ms; animation-iteration-count: infinite; } .coordinate { width: 125px; text-align: center; border: 2px solid black; font-size: small; position: absolute; background-color: white; padding: 5px; color: black !important; } @keyframes wiggle { 0% { transform: rotate(0deg); } 80% { transform: rotate(0deg); } 85% { transform: rotate(-10deg); } 90% { transform: rotate(20deg); } 95% { transform: rotate(-5deg); } 100% { transform: rotate(0deg); } } /* TODO: Make decent looking */ #t-variable-container { display: flex; } #t-variable-container[hide='true'] { display: none; } #t-variable-label { box-sizing: border-box; padding-bottom: 3.5px; } .top-left { bottom: calc(80% + 20px); right: 90%; } .top-right { bottom: calc(80% + 20px); left: 90%; } .right{ left: 130%; top: 0%; } .left{ right: 130%; top: 0%; } .bottom-left { right: 23% !important; top: 103% !important; } .bottom-right { top: calc(80% + 20px); left: 90%; } .helper-bubble:before { content: ''; width: 0px; height: 0px; position: absolute; } .helper-bubble.top-left:before { border-left: 10px solid transparent; border-right: 10px solid black; border-top: 10px solid black; border-bottom: 10px solid transparent; right: -2px; bottom: -20px; } .helper-bubble.right:before{ border-left: 10px solid transparent; border-right: 10px solid black; border-top: 10px solid black; border-bottom: 10px solid transparent; left: -10px; bottom: 15%; transform: translate(0, 50%); transform: rotate(225deg); } .helper-bubble.left:before{ border-left: 10px solid transparent; border-right: 10px solid black; border-top: 10px solid black; border-bottom: 10px solid transparent; right: -10px; bottom: 15%; transform: translate(0, 50%); transform: rotate(45deg); } .helper-bubble.top-right:before { border-left: 10px solid black; border-right: 10px solid transparent; border-top: 10px solid black; border-bottom: 10px solid transparent; left: -2px; bottom: -20px; } .helper-bubble.bottom-left:before { border-left: 10px solid transparent; border-right: 10px solid black; border-top: 10px solid transparent; border-bottom: 10px solid black; right: -2px; top: -20px; } .helper-bubble.bottom-right:before { border-left: 10px solid black; border-right: 10px solid transparent; border-top: 10px solid transparent; border-bottom: 10px solid black; left: -2px; top: -20px; } #dotted-math-container { position: fixed; right: 40px; top: 15%; bottom: 20%; /* display: flex; */ display: none; /* Disabled by default */ border-width: 3px; } #dotted-math-field-static { border: 2px solid rgba(110, 70, 70, 0.2); backdrop-filter: blur(9px); text-shadow: 0px 0px 8px rgba(0, 0, 0, 0.75); margin-top: auto; margin-bottom: auto; font-weight: bold; font-family: monospace; color: rgb(0, 255, 0); display: none; } #dotted-math-button { margin-top: 40vh; margin-bottom: 40vh; margin-left: auto; background-color: rgb(255, 174, 0); } #dotted-slider { writing-mode: bt-lr; /* IE */ -webkit-appearance: slider-vertical; /* Chromium */ width: 8px; height: 70vh; /*line-height: 100vh;*/ accent-color: rgb(0, 255, 0); } .vstack { display: flex; flex-direction: column; } .hidden { display: none !important; } #expression-envelope-container { display: flex; } @media only screen and (max-width: 600px) { #expression-envelope-container { position: fixed; top: 20px; } #expression-envelope-container[hide='true'] { top: -50vh; } #navigator-button { position: fixed; bottom: 20px; font-size: 1.5rem; height: 30px; width: 40px; left: calc(50% - (30px / 2)); padding: 0px; border-radius: 5px; margin: 0; } #navigator-floating-bar { top: 0; bottom: auto; } #show-all-button { border-radius: 0 0 10px 10px; } #expression-envelope { font-size: 0.9rem; } } .tooltip::before, .tooltip::after { --scale: 0; --arrow-size: 10px; --reset-tooltip-color: #68f; --volume-tooltip-color: rgb(34, 136, 238); position: absolute; top: -0.25rem; left: 50%; transform: translateX(-50%) translateY(var(--translate-y, 0)) scale(var(--scale)) !important; transition: 150ms transform; transform-origin: bottom center; } .tooltip::before { --translate-y: calc(-100% - var(--arrow-size)); content: attr(data-tooltip); color: white; padding: 0.5rem; border-radius: 0.3rem; text-align: center; background: var(--reset-tooltip-color); } .tooltip:hover::before, .tooltip:hover::after { --scale: 1; } .tooltip::after { --translate-y: calc(-1 * var(--arrow-size)); content: ''; border: var(--arrow-size) solid transparent; border-top-color: var(--reset-tooltip-color); } .tooltip-e::before { top: 90%; left: calc(100% + 50px); background: var(--volume-tooltip-color); } .tooltip-e::after { top: 50%; left: 105%; transform: translateY(-50%); border-left-width: 0; border-top-color: transparent !important; border-right-color: var(--volume-tooltip-color); } #editor-inspector, #editor-spawner { background: #e8e8e8; display: flex; align-items: center; max-height: 2.5em; column-gap: 10px; padding: 10px 10px; margin: 10px 0 0 0; } #editor-spawner canvas { height: 30px; } #editor-inspector .button, #editor-spawner .button { padding: 5px 8px; box-shadow: none; } #editor-order-input { width: 4em; text-align: center; } #editor-x-input, #editor-y-input { width: 5em; } #editor-start-input, #editor-end-input { width: 5em; } #editor-timer-input { width: 6em; } #editor-position { display: inherit; } #editor-inspector-delete { background: #f46; } .editor-bar { transition: 0.5s; } .editor-bar[hide='true'] { display: none !important; transform: translateY(calc(-100% - 73px / 2)); } .editor-bar[hide='false'] { display: inherit; animation: show-editor-bar 0.5s; } .editor-sliding-up { transform: translateY(calc(-100% - 73px / 2)); } @keyframes show-editor-bar { from { transform: translateY(calc(-100% - 73px / 2)); } to { transform: translateY(0px); } } @keyframes slideInFromTop { from { transform: translateY(-100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } } @keyframes slideOutToTop { from { transform: translateY(0); opacity: 1; } to { transform: translateY(-100%); opacity: 0; } } #try-again { display: none; } @keyframes pulse { 0% { background-color: #f46; } 50% { background-color: #fff; } 100% { background-color: #f46; } } #goal-limit-visual{ background: white; border-radius: 5px; height: 33px; width: 40px; padding: 3px 5px 5px 5px; font-size: 13px; text-align: center; justify-content: center; font-weight: 500; } #lvl-debug-info { position: absolute; margin-left: 75px; top: 10px; font-size: 10px; width: 350px; height: 25px; user-select: all; } #button-hide-level-info { position: absolute; width: 25px; height: 25px; right: 50px; min-width: 25px; min-height: 25px; display: none; } .debugtitle { font-weight: bold; text-align: right; width: 150px; user-select: all; display: none; } .debuginfo { width: 150px; text-align: left; user-select: all; } .grid { position: absolute; left: 0px; top: 0px; width: 300px; display: grid; grid-template-columns: repeat(3, 1fr); grid-auto-rows: 12px; padding: 0px; } .modal { position: fixed; } FILE: DEPENDENCIES.txt mathquill-license.txt Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. math-license.txt Apache License Version 2.0, January 2004 https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS jquery-license.txt Copyright JS Foundation and other contributors, https://js.foundation/ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. lodash-license.txt Copyright OpenJS Foundation and other contributors Based on Underscore.js, copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors This software consists of voluntary contributions made by many individuals. For exact contribution history, see the revision history available at https://github.com/lodash/lodash The following license applies to all parts of this software except as documented below: ==== Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ==== Copyright and related rights for sample code are waived via CC0. Sample code is defined as all source code displayed within the prose of the documentation. CC0: http://creativecommons.org/publicdomain/zero/1.0/ ==== Files located in the node_modules and vendor directories are externally maintained libraries used by this software which have their own licenses; we recommend you read them, as their terms may differ from the terms above. FILE: LICENSE.txt GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . FILE: Libraries_GLUtils.js // TODO: Add JSDoc function GLUtils(gl) { const self = {} const FLOAT_SIZE = 4 function viewport(width, height) { gl.viewport(0, 0, width, height) } function clear() { gl.clearColor(0, 0, 0, 0) gl.clear(gl.COLOR_BUFFER_BIT) } function Program({ vert, frag }) { const self = { vert, frag } const program = gl.createProgram() const vertexShader = gl.createShader(gl.VERTEX_SHADER) gl.shaderSource(vertexShader, vert) gl.compileShader(vertexShader) if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { console.log('self', self) throw `Error in compiling vertex shader ${gl.getShaderInfoLog( vertexShader, )}.` } const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER) gl.shaderSource(fragmentShader, frag) gl.compileShader(fragmentShader) if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { console.log('self', self) throw `Error in compiling fragment shader ${gl.getShaderInfoLog( fragmentShader, )}. Source ${frag}` } gl.attachShader(program, vertexShader) gl.attachShader(program, fragmentShader) gl.linkProgram(program) self.use = () => { gl.useProgram(program) return self } let locations = { attributes: {}, uniforms: {}, } const getAttribute = (name) => { let location = locations.attributes[name] if (!location) { location = gl.getAttribLocation(program, name) if (location == -1) { console.trace() throw `Unable to find location of attribute '${name}' in shader program` } locations.attributes[name] = location } return location } self.instancedAttributes = (ext, array, layout) => { array.bind() for ({ type, name, perInstance, stride = null, offset = null, } of layout) { const length = lengths[type] const location = getAttribute(name) gl.enableVertexAttribArray(location) gl.vertexAttribPointer( location, length, gl.FLOAT, false, (stride || 0) * FLOAT_SIZE, (offset || 0) * FLOAT_SIZE, ) ext.vertexAttribDivisor(location, perInstance) } return self } self.vertices = (v) => { return self.attributes(v.array, v.layout) } self.resetVerticesInstancing = (ext, v) => { const layout = v.layout for ({ name } of layout) { const location = getAttribute(name) ext.vertexAttribDivisor(location, 0) } return self } self.attributes = (array, layout) => { array.bind() for ({ name, length, stride = null, offset = null } of layout) { try { const location = getAttribute(name) gl.enableVertexAttribArray(location) gl.vertexAttribPointer( location, length, gl.FLOAT, false, (stride || 0) * FLOAT_SIZE, (offset || 0) * FLOAT_SIZE, ) } catch (e) { console.log('Failing silently:', e) } } return self } self.attrib = (name, length, stride, offset) => { const location = getAttribute(name) gl.enableVertexAttribArray(location) gl.vertexAttribPointer( location, length, gl.FLOAT, false, stride * FLOAT_SIZE, offset * FLOAT_SIZE, ) return self } self.attribDivisor = (ext, name, count) => { const location = getAttribute(name) ext.vertexAttribDivisorANGLE(location, count) return self } const getUniform = (name) => { let location = locations.uniforms[name] if (!location) { location = gl.getUniformLocation(program, name) locations.uniforms[name] = location } return location } self.uniform = (name, value) => { const uniform = getUniform(name) if (typeof value === 'number') gl.uniform1f(uniform, value) else if (value.length == 2) gl.uniform2fv(uniform, value) else if (value.length == 3) gl.uniform3fv(uniform, value) else throw `Unexpected length ${value.length} when invoking GLUtils.Program.uniform() (${value})` return self } self.uniformi = (name, value) => { const uniform = getUniform(name) gl.uniform1i(uniform, value) return self } self.buffer = (buffer) => { buffer.bind() return self } self.viewport = (width, height) => { viewport(width, height) return self } self.draw = (type, count) => { clear() gl.drawArrays(type, 0, count) } self.drawInstanced = (ext, type, count, instancesCount) => { clear() ext.drawArraysInstanced(type, 0, count, instancesCount) } return self } function createReadBuffer(width, height, format) { const formatSize = format == gl.RGB ? 3 : format == gl.RGBA ? 4 : null return new Uint8Array(width * height * formatSize) } // TODO function pipeline(...programs) { const self = {} const fb = Framebuffer() self.instancedAttributes = (...args) => { programs.forEach((p) => p.instancedAttributes(args)) return self } self.vertices = (...args) => { programs.forEach((p) => p.vertices(args)) return self } self.resetVerticesInstancing = (...args) => { programs.forEach((p) => p.resetVerticesInstancing(args)) return self } self.attributes = (...args) => { programs.forEach((p) => p.attributes(args)) return self } self.attrib = (...args) => { programs.forEach((p) => p.attrib(args)) return self } self.attribDivisor = (...args) => { programs.forEach((p) => p.attribDivisor(args)) return self } self.uniform = (...args) => { programs.forEach((p) => p.uniform(args)) return self } self.uniformi = (...args) => { programs.forEach((p) => p.uniformi(args)) return self } self.buffer = (...args) => { programs.forEach((p) => p.buffer(args)) return self } self.viewport = (...args) => { programs.forEach((p) => p.viewport(args)) return self } self.drawInstanced = (...args) => { programs.forEach((p) => p.drawInstanced(args)) return self } self.draw = (type, count) => { for (const program of programs) { } } } function Framebuffer() { const framebuffer = gl.createFramebuffer() function readTo(buffer, width, height, format) { gl.readPixels(0, 0, width, height, format, gl.UNSIGNED_BYTE, buffer) return buffer } function bind() { gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer) } function setColorAttachment(texture) { bind() gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture._texture, 0, ) } return { bind, setColorAttachment, readTo, } } function bindDisplay() { gl.bindFramebuffer(gl.FRAMEBUFFER, null) } const fb = Framebuffer() function Texture(size = null, format = null) { const texture = gl.createTexture() const self = {} function bind(slot) { gl.activeTexture(gl.TEXTURE0 + slot) gl.bindTexture(gl.TEXTURE_2D, texture) } // Overwrites texture bound to unit 0 for initialization, // note that all draw calls which use Texture should immediately // be preceded by Texture binds bind(0) function image(img, format) { bind(0) gl.texImage2D(gl.TEXTURE_2D, 0, format, format, gl.UNSIGNED_BYTE, img) self.width = img.width self.height = img.height } function resize(_width, _height, _format) { const newTexture = Texture([_width, _height], _format) fb.bind() fb.setColorAttachment(newTexture) bind(0) self.quadProgram .use() .vertices(self.quad) .uniformi('texture', 0) .viewport(_width, _height) .draw(gl.TRIANGLE_STRIP, 4) texture.destroy() return newTexture } if (size) { if (!format) throw `Expected size and format when constructing GLUtils.Texture` gl.texImage2D( gl.TEXTURE_2D, 0, format, size[0], size[1], 0, format, gl.UNSIGNED_BYTE, null, ) } // Overwrites texture bound to unit 0 for setting parameter function parameter(name, value) { bind(0) gl.texParameteri(gl.TEXTURE_2D, name, value) } // Default texture parameters parameter(gl.TEXTURE_MIN_FILTER, gl.LINEAR) parameter(gl.TEXTURE_MAG_FILTER, gl.LINEAR) parameter(gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) parameter(gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) function destroy() { gl.deleteTexture(texture) } function resize(size) { destroy() return Texture(size, format) } return _.merge(self, { bind, parameter, image, destroy, resize, get _texture() { return texture }, width: size ? size.width : 0, height: size ? size.height : 0, }) } async function imageTexture(src, format) { const img = new Image() img.src = src await new Promise((resolve, _) => { img.onload = () => resolve() }) const imageTex = Texture() imageTex.image(img, format) return imageTex } function Array(values, drawMode) { const buffer = gl.createBuffer() function bind() { gl.bindBuffer(gl.ARRAY_BUFFER, buffer) } function unbind() { gl.bindBuffer(gl.ARRAY_BUFFER, null) } function data(data) { bind() gl.bufferSubData(gl.ARRAY_BUFFER, 0, data) unbind() } bind() // Array must be floats gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(values), drawMode) unbind() return { bind, unbind, data, } } const lengths = { float: 1, sampler2D: 1, vec2: 2, vec3: 3, } function Vertices(drawMode, attributes) { const values = Object.values(attributes) if (values.length == 0) throw `At least one attribute must be specified for vertex buffer` const count = values[0].data.length / lengths[values[0].type] if ( values.length > 1 && values.some((attr) => attr.data.length / lengths[attr.type] != count) ) throw `All attributes must be for the same number of vertices` const buffer = _.flattenDeep( _.zip(...values.map((attr) => _.chunk(attr.data, lengths[attr.type]))), ) const array = Array(buffer, drawMode) const stride = values.reduce((s, attr) => s + lengths[attr.type], 0) const layout = [] let offset = 0 for (const [name, { type }] of Object.entries(attributes)) { const length = lengths[type] layout.push({ name, length, stride, offset }) offset += length } return { layout, array, } } function InstancingExtension() { const ext = gl.getExtension('ANGLE_instanced_arrays') if (!ext) { alert( "WebGL feature 'ANGLE_INSTANCED_arrays' must be enabled in order to run this webpage. Try switching or updating your browser!", ) throw `'ANGLE_INSTANCED_arrays' not enabled for WebGL` // TODO: Handle case in which browser doesn't support feature -- delete document? } const drawArraysInstanced = (mode, first, count, primcount) => ext.drawArraysInstancedANGLE(mode, first, count, primcount) const vertexAttribDivisor = (location, divisor) => ext.vertexAttribDivisorANGLE(location, divisor) return { drawArraysInstanced, vertexAttribDivisor, } } const quadNoUV = Vertices(gl.STATIC_DRAW, { aCoords: { type: 'vec2', data: [-1, -1, -1, 1, 1, -1, 1, 1], }, }) const quad = Vertices(gl.STATIC_DRAW, { aCoords: { type: 'vec2', data: [-1, -1, -1, 1, 1, -1, 1, 1], }, aTexCoords: { type: 'vec2', data: [0, 0, 0, 1, 1, 0, 1, 1], }, }) const quadProgram = Program({ vert: ` precision mediump float; attribute vec2 aCoords; attribute vec2 aTexCoords; varying vec2 uv; void main() { gl_Position = vec4(aCoords, 0, 1); uv = aTexCoords; }`, frag: ` precision mediump float; uniform sampler2D texture; varying vec2 uv; void main(void) { vec3 col = texture2D(texture, uv).rgb; gl_FragColor = vec4(col, 1.0); }`, }) return _.mixIn(self, { Program, Framebuffer, Texture, Array, Vertices, InstancingExtension, bindDisplay, imageTexture, createReadBuffer, get quadNoUV() { return quadNoUV }, get quad() { return quad }, get quadProgram() { return quadProgram }, }) } FILE: Libraries_extensions.js // Extensions to external libraries that I need for my own sick purposes // lodash _.tryInvoke = function (object, path, ...args) { let f = _.get(object, path) if (_.isFunction(f)) { return f.apply(object, args) } return null } _.callEach = function (array, args) { for (let i = 0; i < array.length; i++) { if (_.isFunction(array[i])) { array[i].apply(null, args) } else if (_.isArray(array[i])) { _.callEach(array[i], args) } } } _.invokeEach = function (array, path, args) { for (let i = 0; i < array.length; i++) { let v = array[i] if (_.isArray(v)) { _.invokeEach(v, path, args) } else { let f = _.get(v, path) if (_.isFunction(f)) f.apply(v, args) } } } _.eachDeep = function (array, callback, args = []) { for (let i = 0; i < array.length; i++) { let v = array[i] if (_.isArray(v)) _.eachDeep(v, callback, args) else callback.apply(null, [v, ...args]) } } _.isInDeep = function (array, object) { for (let i = 0; i < array.length; i++) { let v = array[i] if (object === v) return true else if (_.isArray(v)) { if (_.isInDeep(v, object)) return true } } return false } _.removeDeep = function (array, object) { for (let i = 0; i < array.length; i++) { let v = array[i] if (v == object) { array.splice(i, 1) return } else if (_.isArray(v)) _.removeDeep(v, object) } } _.mix = function (...sources) { const result = {} for (const source of sources) { const props = Object.keys(source) for (const prop of props) { const descriptor = Object.getOwnPropertyDescriptor(source, prop) Object.defineProperty(result, prop, descriptor) } } return result } _.mixIn = function (...sources) { const result = sources[0] for (let i = 1; i < sources.length; i++) { let source = sources[i] const props = Object.keys(source) for (const prop of props) { const descriptor = Object.getOwnPropertyDescriptor(source, prop) Object.defineProperty(result, prop, descriptor) } } return result } // math.js math.isComplex = function (v) { return _.has(v, 're') && _.has(v, 'im') } math.clamp = function (a, b, t) { return Math.max(a, Math.min(t, b)) } math.clamp01 = function (t) { return Math.max(0, Math.min(t, 1)) } math.lerp = function (a, b, t, smooth = false) { if (smooth) t = math.smooth(t) return a + (b - a) * t } math.modLerp = function (a, b, t, mod = TAU, smooth = false) { while (a < 0) a += mod while (a > mod) a -= mod while (b < 0) b += mod while (b > mod) b -= mod if (Math.abs(a - b) > mod / 2) { if (a < b) a += mod else b += mod } let c = math.lerp(a, b, t, smooth) while (c < 0) c += mod while (c > mod) c -= mod return c } math.unlerp = function (a, b, t) { return (t - a) / (b - a) } math.remap = function (a, b, c, d, t, clamp = false) { if (clamp) t = math.clamp(a, b, t) return math.lerp(c, d, math.unlerp(a, b, t)) } math.truncate = function (number, digits = 1) { const c = Math.pow(10, digits) return Math.floor(number * c) / c } math.smooth = function (t) { t *= Math.PI return 1 - (Math.cos(t) + 1) / 2 } math.pinf = Number.POSITIVE_INFINITY math.ninf = Number.NEGATIVE_INFINITY // Constants PINF = Number.POSITIVE_INFINITY NINF = Number.NEGATIVE_INFINITY PI = Math.PI TAU = PI * 2 const alphabetUpper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' const alphabetLower = 'abcdefghijklmnopqrstuvwxyz' // "The poor man's jquery" —Nicky Case const $ = document.querySelector.bind(document) FILE: Libraries_lz-string.min.js var LZString = (function () { function o(o, r) { if (!t[o]) { t[o] = {} for (var n = 0; n < o.length; n++) t[o][o.charAt(n)] = n } return t[o][r] } var r = String.fromCharCode, n = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=', e = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$', t = {}, i = { compressToBase64: function (o) { if (null == o) return '' var r = i._compress(o, 6, function (o) { return n.charAt(o) }) switch (r.length % 4) { default: case 0: return r case 1: return r + '===' case 2: return r + '==' case 3: return r + '=' } }, decompressFromBase64: function (r) { return null == r ? '' : '' == r ? null : i._decompress(r.length, 32, function (e) { return o(n, r.charAt(e)) }) }, compressToUTF16: function (o) { return null == o ? '' : i._compress(o, 15, function (o) { return r(o + 32) }) + ' ' }, decompressFromUTF16: function (o) { return null == o ? '' : '' == o ? null : i._decompress(o.length, 16384, function (r) { return o.charCodeAt(r) - 32 }) }, compressToUint8Array: function (o) { for ( var r = i.compress(o), n = new Uint8Array(2 * r.length), e = 0, t = r.length; t > e; e++ ) { var s = r.charCodeAt(e) ;(n[2 * e] = s >>> 8), (n[2 * e + 1] = s % 256) } return n }, decompressFromUint8Array: function (o) { if (null === o || void 0 === o) return i.decompress(o) for (var n = new Array(o.length / 2), e = 0, t = n.length; t > e; e++) n[e] = 256 * o[2 * e] + o[2 * e + 1] var s = [] return ( n.forEach(function (o) { s.push(r(o)) }), i.decompress(s.join('')) ) }, compressToEncodedURIComponent: function (o) { return null == o ? '' : i._compress(o, 6, function (o) { return e.charAt(o) }) }, decompressFromEncodedURIComponent: function (r) { return null == r ? '' : '' == r ? null : ((r = r.replace(/ /g, '+')), i._decompress(r.length, 32, function (n) { return o(e, r.charAt(n)) })) }, compress: function (o) { return i._compress(o, 16, function (o) { return r(o) }) }, _compress: function (o, r, n) { if (null == o) return '' var e, t, i, s = {}, p = {}, u = '', c = '', a = '', l = 2, f = 3, h = 2, d = [], m = 0, v = 0 for (i = 0; i < o.length; i += 1) if ( ((u = o.charAt(i)), Object.prototype.hasOwnProperty.call(s, u) || ((s[u] = f++), (p[u] = !0)), (c = a + u), Object.prototype.hasOwnProperty.call(s, c)) ) a = c else { if (Object.prototype.hasOwnProperty.call(p, a)) { if (a.charCodeAt(0) < 256) { for (e = 0; h > e; e++) (m <<= 1), v == r - 1 ? ((v = 0), d.push(n(m)), (m = 0)) : v++ for (t = a.charCodeAt(0), e = 0; 8 > e; e++) (m = (m << 1) | (1 & t)), v == r - 1 ? ((v = 0), d.push(n(m)), (m = 0)) : v++, (t >>= 1) } else { for (t = 1, e = 0; h > e; e++) (m = (m << 1) | t), v == r - 1 ? ((v = 0), d.push(n(m)), (m = 0)) : v++, (t = 0) for (t = a.charCodeAt(0), e = 0; 16 > e; e++) (m = (m << 1) | (1 & t)), v == r - 1 ? ((v = 0), d.push(n(m)), (m = 0)) : v++, (t >>= 1) } l--, 0 == l && ((l = Math.pow(2, h)), h++), delete p[a] } else for (t = s[a], e = 0; h > e; e++) (m = (m << 1) | (1 & t)), v == r - 1 ? ((v = 0), d.push(n(m)), (m = 0)) : v++, (t >>= 1) l--, 0 == l && ((l = Math.pow(2, h)), h++), (s[c] = f++), (a = String(u)) } if ('' !== a) { if (Object.prototype.hasOwnProperty.call(p, a)) { if (a.charCodeAt(0) < 256) { for (e = 0; h > e; e++) (m <<= 1), v == r - 1 ? ((v = 0), d.push(n(m)), (m = 0)) : v++ for (t = a.charCodeAt(0), e = 0; 8 > e; e++) (m = (m << 1) | (1 & t)), v == r - 1 ? ((v = 0), d.push(n(m)), (m = 0)) : v++, (t >>= 1) } else { for (t = 1, e = 0; h > e; e++) (m = (m << 1) | t), v == r - 1 ? ((v = 0), d.push(n(m)), (m = 0)) : v++, (t = 0) for (t = a.charCodeAt(0), e = 0; 16 > e; e++) (m = (m << 1) | (1 & t)), v == r - 1 ? ((v = 0), d.push(n(m)), (m = 0)) : v++, (t >>= 1) } l--, 0 == l && ((l = Math.pow(2, h)), h++), delete p[a] } else for (t = s[a], e = 0; h > e; e++) (m = (m << 1) | (1 & t)), v == r - 1 ? ((v = 0), d.push(n(m)), (m = 0)) : v++, (t >>= 1) l--, 0 == l && ((l = Math.pow(2, h)), h++) } for (t = 2, e = 0; h > e; e++) (m = (m << 1) | (1 & t)), v == r - 1 ? ((v = 0), d.push(n(m)), (m = 0)) : v++, (t >>= 1) for (;;) { if (((m <<= 1), v == r - 1)) { d.push(n(m)) break } v++ } return d.join('') }, decompress: function (o) { return null == o ? '' : '' == o ? null : i._decompress(o.length, 32768, function (r) { return o.charCodeAt(r) }) }, _decompress: function (o, n, e) { var t, i, s, p, u, c, a, l, f = [], h = 4, d = 4, m = 3, v = '', w = [], A = { val: e(0), position: n, index: 1 } for (i = 0; 3 > i; i += 1) f[i] = i for (p = 0, c = Math.pow(2, 2), a = 1; a != c; ) (u = A.val & A.position), (A.position >>= 1), 0 == A.position && ((A.position = n), (A.val = e(A.index++))), (p |= (u > 0 ? 1 : 0) * a), (a <<= 1) switch ((t = p)) { case 0: for (p = 0, c = Math.pow(2, 8), a = 1; a != c; ) (u = A.val & A.position), (A.position >>= 1), 0 == A.position && ((A.position = n), (A.val = e(A.index++))), (p |= (u > 0 ? 1 : 0) * a), (a <<= 1) l = r(p) break case 1: for (p = 0, c = Math.pow(2, 16), a = 1; a != c; ) (u = A.val & A.position), (A.position >>= 1), 0 == A.position && ((A.position = n), (A.val = e(A.index++))), (p |= (u > 0 ? 1 : 0) * a), (a <<= 1) l = r(p) break case 2: return '' } for (f[3] = l, s = l, w.push(l); ; ) { if (A.index > o) return '' for (p = 0, c = Math.pow(2, m), a = 1; a != c; ) (u = A.val & A.position), (A.position >>= 1), 0 == A.position && ((A.position = n), (A.val = e(A.index++))), (p |= (u > 0 ? 1 : 0) * a), (a <<= 1) switch ((l = p)) { case 0: for (p = 0, c = Math.pow(2, 8), a = 1; a != c; ) (u = A.val & A.position), (A.position >>= 1), 0 == A.position && ((A.position = n), (A.val = e(A.index++))), (p |= (u > 0 ? 1 : 0) * a), (a <<= 1) ;(f[d++] = r(p)), (l = d - 1), h-- break case 1: for (p = 0, c = Math.pow(2, 16), a = 1; a != c; ) (u = A.val & A.position), (A.position >>= 1), 0 == A.position && ((A.position = n), (A.val = e(A.index++))), (p |= (u > 0 ? 1 : 0) * a), (a <<= 1) ;(f[d++] = r(p)), (l = d - 1), h-- break case 2: return w.join('') } if ((0 == h && ((h = Math.pow(2, m)), m++), f[l])) v = f[l] else { if (l !== d) return null v = s + s.charAt(0) } w.push(v), (f[d++] = s + v.charAt(0)), h--, (s = v), 0 == h && ((h = Math.pow(2, m)), m++) } }, } return i })() 'function' == typeof define && define.amd ? define(function () { return LZString }) : 'undefined' != typeof module && null != module && (module.exports = LZString) FILE: Libraries_preprocessMathQuill.js // Shamelessly stolen from Christopher Chudzicki, creator of Math3D.org // https://github.com/ChristopherChudzicki/math3d-react/blob/master/client/src/utils/mathParsing/preprocessors/preprocessMathQuill.js function escapeRegExp(str) { // from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Using_Special_Characters // $& means the whole matched string return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') } function replaceAll(str, find, replaceWith) { // from https://stackoverflow.com/a/1144788/2747370 return str.replace(new RegExp(escapeRegExp(find), 'g'), replaceWith) } function findClosingBrace(str, startIdx) { const braces = { '[': ']', '<': '>', '(': ')', '{': '}', } const openingBrace = str[startIdx] const closingBrace = braces[openingBrace] if (closingBrace === undefined) { throw Error( `${str} does not contain an opening brace at position ${startIdx}.`, ) } let stack = 1 // eslint-disable-next-line no-plusplus for (let j = startIdx + 1; j < str.length; j++) { if (str[j] === openingBrace) { stack += +1 } else if (str[j] === closingBrace) { stack += -1 } if (stack === 0) { return j } } // stack !== 0 throw Error( `${str} has a brace that opens at position ${startIdx} but does not close.`, ) } /** * Makes a series of replacements on MathQuill-generated LaTeX strings so that * they can be parsed by MathJS. * * Notes: * 1. This is pretty heuristic. Might discover updates needed. * 2. Much of this could be used to preprocess LaTeX generated by other means * (i.e., not MathQuill.) The main bit that is MathQuill-specific is probably * the operatorname replacements. * * @param {string} fromMQ a MathQuill-generated LaTeX expression * @return {string} the input expression with LaTeX commands converted to mathjs */ function mathquillToMathJS(fromMQ) { const replacements = [ { tex: '\\operatorname{diff}', mathjs: 'diff' }, { tex: '\\operatorname{pdiff}', mathjs: 'pdiff' }, { tex: '\\operatorname{curl}', mathjs: 'curl' }, { tex: '\\operatorname{div}', mathjs: 'div' }, { tex: '\\operatorname{norm}', mathjs: 'norm' }, { tex: '\\operatorname{mod}', mathjs: 'mod' }, { tex: '\\operatorname{abs}', mathjs: 'abs' }, { tex: '\\operatorname{unitT}', mathjs: 'unitT' }, { tex: '\\operatorname{unitN}', mathjs: 'unitN' }, { tex: '\\operatorname{unitB}', mathjs: 'unitB' }, { tex: '\\operatorname{arccosh}', mathjs: 'arccosh' }, { tex: '\\operatorname{arcsinh}', mathjs: 'arcsinh' }, { tex: '\\operatorname{arctanh}', mathjs: 'arctanh' }, { tex: '\\cdot', mathjs: ' * ' }, { tex: '\\left', mathjs: '' }, { tex: '\\right', mathjs: '' }, { tex: '{', mathjs: '(' }, { tex: '}', mathjs: ')' }, { tex: '~', mathjs: ' ' }, { tex: '\\', mathjs: ' ' }, ] // remove fractions, then apply replacements const noFrac = fracToDivision(fromMQ) const noBraceSub = convertSubscript(noFrac) return replacements.reduce( (acc, r) => replaceAll(acc, r['tex'], r['mathjs']), noBraceSub, ) } /** * Recursively removes braces from LaTeX subscripts * - example: x_{12foo_{bar123_{evenlower}}} --> x_12foo_bar123_evenlower */ function convertSubscript(expr) { const sub = '_{' const subStart = expr.indexOf(sub) if (subStart < 0) { return expr } const numStart = subStart + sub.length const closingBrace = expr.indexOf('}', numStart) const newExpr = expr.slice(0, subStart) + '_' + expr.slice(numStart, closingBrace) + expr.slice(closingBrace + 1) return convertSubscript(newExpr) } /** * Recursively replaces LaTeX fractions with normal divison * - example: \frac{a}{1 + \frac{b}{c}}x^2 + 1 --> ({a}/{1 + {b}/{c}})x^2 + 1 */ function fracToDivision(expr) { const frac = '\\frac' const fracStart = expr.indexOf(frac) const numStart = fracStart + frac.length if (fracStart < 0) { return expr } const divIdx = findClosingBrace(expr, numStart) const fracEndIdx = findClosingBrace(expr, divIdx + 1) // Remove '\frac', add '/', and wrap parens around fraction expr const newExpr = expr.slice(0, fracStart) + '(' + expr.slice(numStart, divIdx + 1) + '/' + expr.slice(divIdx + 1, fracEndIdx + 1) + ')' + expr.slice(fracEndIdx + 1) return fracToDivision(newExpr) } FILE: Types_Assets.js function Assets() { const self = {} let loadTotal = 0 let loadCount = 0 let loaded = false let failed = false const imageExtensions = ['svg', 'png', 'jpg', 'jpeg', 'webp'] const soundExtensions = ['m4a', 'mp3', 'ogg', 'wav'] const shaderExtensions = ['glsl', 'frag', 'vert'] const maxRetryAttempts = 5 let totalRetryAttemptsMade = 0 // Start with 100 kbps for (low) initial guess let predictedNetworkSpeedBps = 100 * 1024 const assetTimeouts = [] let checkAssetTimeoutsInterval = null const assetAbortControllers = {} const assetsLoaded = {} let bytesLoaded = 0 let onComplete function soundFromBlob(blob, assetSpec) { return new Promise((resolve, reject) => { const blobUrl = URL.createObjectURL(blob) assetSpec.src = blobUrl // For some reason loading howl before page // is clicked (loading veil) angers Chrome? TODO: Fix const howl = new Howl({ ...assetSpec, onload: () => resolve(howl), onerror: (error) => reject(error), }) }) } function imageFromBlob(blob) { const blobUrl = URL.createObjectURL(blob) const img = new Image() img.src = blobUrl return new Promise((resolve, reject) => { img.onload = () => resolve(img) img.onerror = (error) => reject(error) }) } function shaderFromBlob(blob) { return blob.text() } function correctNetworkSpeedPrediction(bytes, loadTimeMs) { loadTimeMs = Math.max(loadTimeMs, 1) const bps = bytes / (loadTimeMs / 1000) const total = bytesLoaded + bytes predictedNetworkSpeedBps = (bytesLoaded / total) * predictedNetworkSpeedBps + (bytes / total) * bps if (predictedNetworkSpeedBps == Infinity) debugger bytesLoaded = total } function loadAssetFromPath( object, key, path, assetFromBlob, assetSpec, onStartFetchCb, ) { const controller = new AbortController() const controllers = assetAbortControllers[key] || (assetAbortControllers[key] = []) controllers.push(controller) let start fetch(ASSET_MAP[path] || path, { signal: controller.signal, }) .then((response) => { start = performance.now() onStartFetchCb() return response.blob() }) .then((blob) => { // Incorporate blob load time + size to reestimate network speed const now = performance.now() correctNetworkSpeedPrediction(blob.size, now - start) return blob }) .then((blob) => assetFromBlob(blob, assetSpec)) .catch((error) => { // Throw if assetFromBlob fails (and not from abort) if (error.name === 'AbortError') return else handleFailure(error, path) }) .then((asset) => { // Abort other asset fetch attempts, if they exist for (const c of controllers) { if (c != controller) c.abort() } _.set(object, key, asset) assetLoaded() }) } function loadAsset(folders, key, file, assetSpec = {}) { const extensions = _.tail(file.split('.')) const extension = extensions[0] const name = file.split('.')[0] || key const path = 'Assets/' + folders .map((v) => v .split('_') .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) .join('_'), ) .join('/') + '/' + name + '.' + extension const objectPath = folders.join('.') + '.' + key if (_.has(self, objectPath)) { // If already loaded asset, return return } const isImage = _.includes(imageExtensions, extension) const isSound = _.includes(soundExtensions, extension) const isShader = _.includes(shaderExtensions, extension) const assetFromBlob = isImage ? imageFromBlob : isSound ? soundFromBlob : isShader ? shaderFromBlob : null if (!assetFromBlob) throw `Unexpected file extension: .${extension}` // Pass extension to Howler.js if it's a sound if (isSound) assetSpec.format = extension if(isSound){_.set(self,objectPath,silentSound());return} const attemptLoad = () => loadAssetFromPath(self, objectPath, path, assetFromBlob, assetSpec, () => assetTimeouts.push([assetTimeout, performance.now()]), ) const assetTimeout = (timeoutMs) => { if (!assetsLoaded[key]) { // TODO: Network speed prediction is terrible and calculated timeout // needs to be more conservative return if (totalRetryAttemptsMade < maxRetryAttempts) { console.log( `Failed to load ${path} after ${timeoutMs}ms, retrying...`, ) totalRetryAttemptsMade += 1 attemptLoad() } else { if (!failed) { // TODO: Use modal instead? alert( "We couldn't communicate with our servers — please try reloading", ) failed = true } } } } attemptLoad() loadCount++ loadTotal++ } function checkAssetTimeouts() { // Max time it could reasonably take // (estimate of how long it'd take to load 10 MB) // Constant for time being, could change w/ file let timeoutMs = ((10 * 1024 * 1024) / predictedNetworkSpeedBps) * 1000 timeoutMs = Math.max(timeoutMs, 1000) const now = performance.now() for (const [timeout, start] of assetTimeouts) { const elapsedMs = now - start if (elapsedMs > timeoutMs) { timeout(timeoutMs) } } } function loadAssets(object, folders = []) { if (!checkAssetTimeoutsInterval) { checkAssetTimeoutsInterval = setInterval(checkAssetTimeouts, 1000) } _.each(object, (v, i) => { if (_.isObject(v)) { if (_.has(v, 'src')) loadAsset(folders, i, v.src, v) else loadAssets(v, [...folders, i]) } else if (_.isString(v)) loadAsset(folders, i, v) }) } function loadingBarProgress(progress, total) { const percent = Math.round((100 * progress) / total) ui.loadingProgressBar.style.width = `${percent}%` } const levelLoadingTransitionLength = 1 function playLeveLLoadingScreenFadeOut() { ui.levelLoadingVeil.setAttribute( 'style', `animation: fadeOut ${levelLoadingTransitionLength}s ease-in-out forwards;`, ) ui.levelLoadingVeil.addEventListener('animationend', () => { ui.levelLoadingVeil.setAttribute('hide', true) ui.levelLoadingVeil.removeAttribute('style') }) } let levelLoadingScreenEnabled = true function hideLoadingScreen() { ui.loadingProgressBarContainer.setAttribute('hide', true) ui.loadingVeil.setAttribute('hide', true) if (levelLoadingScreenEnabled) playLeveLLoadingScreenFadeOut() levelLoadingScreenEnabled = false } function showLoadingScreen(loadingLevel = false) { if (!loadingLevel) { ui.loadingProgressBarContainer.setAttribute('hide', false) ui.loadingVeil.setAttribute('hide', false) } else { ui.levelLoadingVeil.setAttribute('style', `opacity: 1;`) ui.levelLoadingVeil.setAttribute('hide', false) levelLoadingScreenEnabled = true } } function load(paths, _onComplete, loadingLevel = false) { loaded = false onComplete = _onComplete loadAssets(paths) // If all assets happen to be already loaded, // invoke callback and clean up if (loadCount == 0) { onFinishLoading() } else { showLoadingScreen(loadingLevel) } } function onFinishLoading() { if (IS_DEVELOPMENT) { // For Puppeteer console.log('FINISHED_LOADING_ASSETS') } clearInterval(checkAssetTimeoutsInterval) checkAssetTimeoutsInterval = null //hideLoadingScreen() onComplete() } function assetLoaded(path) { assetsLoaded[path] = true loadCount-- if (loadCount == 0) { onFinishLoading() } const progress = loadTotal - loadCount loadingBarProgress(progress, loadTotal) } function handleFailure(error, path) { console.error('Asset request failed', error, path) if (!failed) { alert(`Something failed to load: ${path}. Try refreshing the page.`) failed = true } } return _.mixIn(self, { load, hideLoadingScreen, get loaded() { return loaded }, }) } FILE: Types_BufferSampler.js /* Manages a buffer which can be refreshed and then sampled from via linear interpolation */ function BufferSampler(sampler, sampleCount) { const self = {} const samples = sampler.generateSampleArray(sampleCount) let minX, maxX, sampleWidth let min = Infinity, max = -Infinity let scope = {} function refreshSamples(_minX, _maxX, ...params) { minX = _minX maxX = _maxX sampleWidth = (maxX - minX) / (sampleCount - 1) // Assign extra parameters, if given for (let i = 0; i < params.length; i += 2) { scope[params[i]] = params[i + 1] } for (let i = 0; i < sampleCount; i++) { const x = minX + sampleWidth * i let y try { scope['x'] = x y = sampler._evaluate(scope) } catch (err) { y = 0 } if (y < min) min = y if (y > max) max = y samples[i].set(x, y) } } function sample(x) { if (x < minX + 1e-6) { const a = samples[0][1] const b = samples[1][1] return a + ((b - a) / sampleWidth) * (x - minX) } if (x > maxX - 1e-6) { const a = samples[sampleCount - 2][1] const b = samples[sampleCount - 1][1] return b + ((b - a) / sampleWidth) * (x - maxX) } const i = (x - minX) / sampleWidth const leftI = Math.floor(i) const rightI = Math.ceil(i) const a = samples[leftI][1] const b = samples[rightI][1] const leftX = minX + leftI * sampleWidth return a + ((b - a) / sampleWidth) * (x - leftX) } return _.mix(self, { samples, refreshSamples, sample, get min() { return min }, get max() { return max }, }) } FILE: Types_Clickable.js // Anything on the canvas that responds to mouse events function Clickable(spec) { const self = {} const { entity, space = 'world', dragThreshold = 0.05 } = spec let enabled = spec.enabled ?? true const { screen = entity.screen, camera = entity.camera, shape = entity.shape, } = spec // If layer is unspecified, default is layer 1 // except for "shapeless" clickables, which are layer 0 let layer = _.get(spec, 'layer', shape ? 1 : 0) let hovering = false let holding = false let dragging = false let selected = false const overlapPoint = Vector2() const clickPoint = Vector2() const mouseDownPoint = Vector2() const mouseMovePoint = Vector2() const mouseUpPoint = Vector2() const hoverOnPoint = Vector2() const hoverMovePoint = Vector2() const hoverOffPoint = Vector2() const dragStartPoint = Vector2() const dragMovePoint = Vector2() const dragEndPoint = Vector2() const dragDelta = Vector2() let dragDistance = 0 function sendEvent(name, args) { entity.sendEvent(name, args) } function collectHit(point, hits) { const overlapping = testOverlap(point) if (overlapping) { hits.push(self) return true } return false } function testOverlap(point) { if (!shape) return true overlapPoint.set(point) if (camera) camera.screenToWorld(overlapPoint) return shape.intersectPoint(overlapPoint) } function hoverStart(point) {} function hoverMove(point) {} function hoverEnd(point) {} function dragStart(point) {} function dragMove(point) {} function dragEnd(point) {} function toFrameScalar(scalar) { if (space == 'world') return camera.worldToFrameScalar(scalar) if (space == 'screen') return screen.transform.transformScalar(scalar) // Already in frame space return scalar } function recordPoint(point, output) { if (space == 'world') return camera.screenToWorld(point, output) else if (space == 'frame') return screen.screenToFrame(point, output) // Event points are in screenspace by default return output.set(point) } function mouseEnter(point) { hovering = true recordPoint(point, hoverOnPoint) sendEvent('hoverOn', [hoverOnPoint]) } function mouseExit(point) { hovering = false recordPoint(point, hoverOffPoint) sendEvent('hoverOff', [hoverOffPoint]) } function mouseDown(point, eventData) { recordPoint(point, mouseDownPoint) if (hovering) { holding = true recordPoint(point, dragStartPoint) sendEvent('mouseDown', [mouseDownPoint, eventData]) } } function mouseMove(point) { if (hovering) { recordPoint(point, hoverMovePoint) sendEvent('hoverMove', [hoverMovePoint]) } if (holding) { recordPoint(point, dragMovePoint) dragMovePoint.subtract(dragStartPoint, dragDelta) dragDistance = dragDelta.magnitude let dragThresholdMet = toFrameScalar(dragDistance) > dragThreshold if (dragging) { sendEvent('dragMove', [dragMovePoint, dragDelta]) } else if (dragThresholdMet) { dragging = true sendEvent('dragStart', [dragStartPoint]) } sendEvent('mouseMove', [dragMovePoint]) } } function mouseUp(point) { recordPoint(point, clickPoint) if (holding) { holding = false sendEvent('click', [clickPoint]) sendEvent('mouseUp', [mouseUpPoint]) } if (dragging) { dragging = false recordPoint(point, dragEndPoint) sendEvent('dragEnd', [dragEndPoint]) } } let deselectMe = null function select(_deselectMe) { deselectMe = _deselectMe selected = true sendEvent('select', []) } function deselect() { deselectMe = null selected = false sendEvent('deselect', []) } return _.mixIn(self, { entity, testOverlap, collectHit, mouseMove, mouseDown, mouseUp, mouseEnter, mouseExit, clickPoint, hoverOnPoint, hoverMovePoint, hoverOffPoint, dragStartPoint, dragMovePoint, dragEndPoint, dragDelta, select, deselect, get layer() { return layer }, get hovering() { return hovering }, get dragging() { return dragging }, get holding() { return holding }, get selected() { return selected }, get enabled() { return enabled }, set enabled(v) { enabled = v }, get dragDistance() { return dragDistance }, }) } FILE: Types_ClickableContext.js function ClickableContext(spec) { const { entity, screen } = spec let target = null let selection = null const mousePoint = Vector2() const hits = [] const collectHitArgs = [mousePoint, hits] function processEvent(eventData, eventName) { mousePoint.set( eventData.offsetX * screen.resolutionScalingFactor, eventData.offsetY * screen.resolutionScalingFactor, ) // Collect all hits at mousePoint hits.length = 0 entity.sendEvent('clickable.collectHit', collectHitArgs) // Sort hits by layer hits.sort(compareHits) // New target is the top-level hit let newTarget = _.last(hits) if (newTarget != target) { // Exit old target, enter new target if (target) target.mouseExit(mousePoint) if (newTarget) newTarget.mouseEnter(mousePoint) // New target replaces the current target target = newTarget } // TODO: Figure out editor if (eventName == 'mouseDown') { // This event is only used for selection in editor let newSelection = hits .reverse() .find((h) => h.enabled && h.entity.selectable) if (newSelection != selection) { selection?.deselect() newSelection?.select(() => { selection = null }) selection = newSelection } } // Ping every clickable in this tree entity.sendEvent('clickable.' + eventName, [mousePoint, eventData]) } function deselect(entity) { selection = null entity.deselect() } function compareHits(a, b) { return a.drawOrder - b.drawOrder } function keydown(key) { // if (key == 'Backspace' || key == 'Delete') { // // TODO: Maybe tag entities with general type for polymorphism? // if (selection && selection.name.includes('Goal')) { // editor.deselect() // world.level.sendEvent('goalDeleted', [selection]) // selection.destroy() // } // } } return { deselect, processEvent, keydown, } } FILE: Types_Engine.js function Engine(spec) { const self = {} const { ui, canvas, ticksPerSecond = 60, stepping = false } = spec let canvasIsDirty = true const tickDelta = 1 / ticksPerSecond let time = 0 const screen = Screen({ canvas, }) const world = World({ ui, screen, tickDelta, getTime: () => time, }) tick() draw() if (!stepping) { setInterval(tick, 1000 / ticksPerSecond) } // Core methods function tick() { world.sendLifecycleEvent('awake') world.sendLifecycleEvent('start') world.sendEvent('tick') time += tickDelta requestDraw() } function draw() { if (!canvasIsDirty) return canvasIsDirty = false world.sendEvent('draw') } function requestDraw() { if (!canvasIsDirty) { canvasIsDirty = true requestAnimationFrame(draw) } } // HTML events function onKeyUp(event) {} window.addEventListener('keyup', onKeyUp) function onResizeWindow(event) { screen.resize() canvasIsDirty = true draw() } window.addEventListener('resize', onResizeWindow) function onClickCanvas() { if (stepping) { tick() } } canvas.addEventListener('click', onClickCanvas) function onMouseMoveCanvas(event) { world.clickableContext.processEvent(event, 'mouseMove') event.preventDefault() } canvas.addEventListener('mousemove', onMouseMoveCanvas) canvas.addEventListener('pointermove', onMouseMoveCanvas) function onMouseDownCanvas(event) { world.clickableContext.processEvent(event, 'mouseDown') event.preventDefault() } canvas.addEventListener('mousedown', onMouseDownCanvas) canvas.addEventListener('pointerdown', onMouseDownCanvas) function onMouseUpCanvas(event) { world.clickableContext.processEvent(event, 'mouseUp') event.preventDefault() } canvas.addEventListener('mouseup', onMouseUpCanvas) canvas.addEventListener('pointerup', onMouseUpCanvas) return _.mixIn(self, { world, }) } FILE: Types_Entities_Arrow.js let arrowsDisabled = false function Arrow(spec) { const { self, screen, camera, ctx } = Entity(spec, 'Arrow') const { point0 = Vector2(), point1 = Vector2(), color = '#111', style = 'solid', headSize = 1, lineWidth = 0.2, truncate = [0, 0], } = spec let { dashed = false, dashSettings = [0.5, 0.5], dashOffset = 0, fadeIn = false, fadeOut = false, } = spec const transform = Transform({ position: point0, ...spec, }) const endTransform = Transform({ position: point1, scale: headSize, }) const direction = Vector2() const pathPoint = Vector2() const undashedSettings = [] let fadeOutGradient let fadeInGradient let opacity = 1 recomputeValues() function drawLocalShaft() { ctx.beginPath() ctx.moveTo(0, truncate[0]) ctx.lineTo(0, direction.magnitude - truncate[1] - headSize) ctx.globalAlpha = opacity ctx.strokeStyle = fadeIn ? fadeInGradient : fadeOut ? fadeOutGradient : color ctx.lineWidth = lineWidth ctx.setLineDash(dashed ? dashSettings : undashedSettings) ctx.dashOffset = dashOffset ctx.stroke() ctx.setLineDash([]) } function drawLocalPoint() { const y = -truncate[1] / headSize ctx.beginPath() ctx.moveTo(0, y) ctx.lineTo(-0.5, y - 1) ctx.lineTo(0.5, y - 1) ctx.lineTo(0, y) ctx.globalAlpha = opacity ctx.fillStyle = color ctx.fill() } function linesIntersect(a, b, c, d, p, q, r, s) { const det = (a - c) * (q - s) - (b - d) * (p - r) if (det == 0) { return false } else { const lambda = (-q * r + b * (r - p) + a * (q - s) + p * s) / det return 0 <= lambda && lambda <= 1 } } function intersectsLine(x0, y0, x1, y1) { return linesIntersect( point0.x, point0.y, point1.x, point1.y, x0, y0, x1, y1, ) } function intersectsScreen() { let left = camera.lowerLeft.x let right = camera.upperRight.x let top = camera.upperRight.y let bottom = camera.lowerLeft.y if ( (point0.x >= left && point0.x <= right && point0.y >= bottom && point0.y <= top) || (point1.x >= left && point1.x <= right && point1.y >= bottom && point1.y <= top) ) { return true } return ( intersectsLine(left, top, right, top) || intersectsLine(right, top, right, bottom) || intersectsLine(right, bottom, left, bottom) || intersectsLine(left, bottom, left, top) ) } function draw() { if (arrowsDisabled) return if (opacity == 0) return if (!intersectsScreen()) return arrowsDrawn++ camera.drawThrough(ctx, drawLocalShaft, transform) if (!fadeOut) camera.drawThrough(ctx, drawLocalPoint, endTransform) ctx.globalAlpha = 1 } function recomputeValues() { point1.subtract(point0, direction) let angle = Math.atan2(direction.x, -direction.y) transform.rotation = angle endTransform.rotation = angle endTransform.position.set(point1) fadeOutGradient = ctx.createLinearGradient( 0, truncate[0], 0, direction.magnitude - truncate[1] - headSize, ) fadeInGradient = ctx.createLinearGradient( 0, truncate[0], 0, direction.magnitude - truncate[1] - headSize, ) // Makes a higher proportion of short arrows visible const shortyOffset = math.remap(10, 30, 0.6, 0, direction.magnitude, true) fadeOutGradient.addColorStop(0 + shortyOffset, color) fadeOutGradient.addColorStop(0.4 + shortyOffset, 'rgba(255, 255, 255, 0)') fadeInGradient.addColorStop(0.6 - shortyOffset, 'rgba(255, 255, 255, 0)') fadeInGradient.addColorStop(1 - shortyOffset, color) } return self.mix({ transform, endTransform, draw, get point0() { return point0 }, set point0(v) { transform.position.set(v) point0.set(v) recomputeValues() }, get point1() { return point0 }, set point1(v) { point1.set(v) recomputeValues() }, get opacity() { return opacity }, set opacity(v) { opacity = v }, get dashed() { return dashed }, set dashed(v) { dashed = v }, get fadeIn() { return fadeIn }, set fadeIn(v) { fadeIn = v }, get fadeOut() { return fadeOut }, set fadeOut(v) { fadeOut = v }, }) } FILE: Types_Entities_Axes.js function Axes(spec) { const { self, screen } = Entity(spec, 'Axes') const transform = Transform() let { camera } = spec const ctx = screen.ctx const origin = Vector2() const screenOrigin = Vector2() function tick() {} function drawLocal() { const xTicks = Math.ceil(camera.upperRight.x - camera.lowerLeft.x) + 1 const yTicks = Math.ceil(camera.upperRight.y - camera.lowerLeft.y) + 1 ctx.beginPath() // Draw right x ticks for (let i = 0; i < xTicks; i++) { ctx.moveTo(i, 0) ctx.lineTo(i, -0.2) } // Draw left x ticks for (let i = 0; i > -xTicks; i--) { ctx.moveTo(i, 0) ctx.lineTo(i, 0.2) } // Draw top y ticks for (let i = 0; i < yTicks; i++) { ctx.moveTo(0, -i) ctx.lineTo(0.2, -i) } // Draw bottom y ticks for (let i = 0; i > -yTicks; i--) { ctx.moveTo(0, -i) ctx.lineTo(-0.2, -i) } ctx.strokeStyle = '#aaa' ctx.lineWidth = camera.screenToWorldScalar(1) ctx.stroke() } function draw() { camera.worldToScreen(origin, screenOrigin) ctx.beginPath() ctx.moveTo(0, screenOrigin.y) ctx.lineTo(screen.width, screenOrigin.y) ctx.moveTo(screenOrigin.x, 0) ctx.lineTo(screenOrigin.x, screen.height) ctx.strokeStyle = '#aaa' ctx.lineWidth = 2 ctx.stroke() camera.drawThrough(ctx, drawLocal, transform) } return self.mix({ transform, tick, draw, }) } FILE: Types_Entities_Camera.js // The Camera transforms world-space coordinates into Frame-space coordinates (see Screen.js for an explanation of the "Frame") // // The camera smoothly pans and zooms to keep all Sledders, Goals, and the Axes origin in view at all times. function Camera(spec) { const { self, screen } = Entity(spec, 'Camera') const transform = Transform(spec) let { fov = 5, rotation = 0, globalScope = {}, directors = [] } = spec let shake = 0 // const defaultOffset = Vector2(spec.offset || [0, 0]) const defaultOffset = Vector2([0, 0]) const offset = defaultOffset.clone() const scaledOffset = Vector2() let activeDirector = null transform.scale = fov const lowerLeft = Vector2() const upperRight = Vector2() computeCorners() // Debug values const topScreen = Vector2() const topWorld = Vector2() const bottomScreen = Vector2() const bottomWorld = Vector2() const leftScreen = Vector2() const leftWorld = Vector2() const rightScreen = Vector2() const rightWorld = Vector2() const centerWorld = Vector2() const centerScreen = Vector2() function start() { sampleDirector() } function setFov(_fov) { fov = _fov transform.scale = fov } function worldToFrame(point, output) { return transform.invertPoint(point, output) } function frameToWorld(point, output) { return transform.transformPoint(point, output) } function worldToFrameScalar(scalar) { return transform.invertScalar(scalar) } function frameToWorldScalar(scalar) { return transform.transformScalar(scalar) } function worldToFrameDirection(direction, output) { return transform.invertDirection(direction, output) } function frameToWorldDirection(direction, output) { return transform.transformDirection(direction, output) } function worldToScreen(point, output, localTransform) { if (output) output.set(point) else output = point if (localTransform) localTransform.transformPoint(output) transform.invertPoint(output) screen.frameToScreen(output) return output } function screenToWorld(point, output, localTransform) { if (output) output.set(point) else output = point screen.screenToFrame(output) transform.transformPoint(output) if (localTransform) localTransform.invertPoint(output) return output } function worldToScreenDirection(input, output, localTransform) { if (output) output.set(input) else output = input if (localTransform) localTransform.transformDirection(output) transform.invertDirection(output) screen.frameToScreenDirection(output) return output } function screenToWorldDirection(input, output, localTransform) { if (output) output.set(input) else output = input screen.screenToFrameDirection(output) transform.transformDirection(output) if (localTransform) localTransform.invertDirection(output) return output } function worldToScreenScalar(scalar = 1) { const frameScalar = transform.invertScalar(scalar) return screen.transform.transformScalar(frameScalar) } function screenToWorldScalar(scalar = 1) { const frameScalar = screen.transform.invertScalar(scalar) return transform.transformScalar(frameScalar) } function worldToScreenCanvas(ctx, localTransform) { screen.transform.invertCanvas(ctx) ctx.scale(1, -1) transform.transformCanvas(ctx) if (localTransform) { localTransform.invertCanvas(ctx) } ctx.scale(1, -1) } function screenToWorldCanvas(ctx, localTransform) { ctx.scale(1, -1) if (localTransform) { localTransform.transformCanvas(ctx) } transform.invertCanvas(ctx) ctx.scale(1, -1) screen.transform.transformCanvas(ctx) } function computeCorners() { transform.transformPoint(screen.minFramePoint, lowerLeft) transform.transformPoint(screen.maxFramePoint, upperRight) } function sampleDirector() { let _activeDirector for (c of directors) { if (c.canControl()) { _activeDirector = c break } } if (activeDirector != _activeDirector) { if (activeDirector) activeDirector.stopControlling() if (_activeDirector) _activeDirector.startControlling() activeDirector = _activeDirector } alignToDirector() } function alignToDirector() { if (activeDirector) { offset.multiply(fov, scaledOffset) transform.position = activeDirector.cameraState.position transform.position.add(scaledOffset) setFov(activeDirector.cameraState.fov) // const offsetMargin = math.max(scaledOffset.x, scaledOffset.y) // setFov(activeDirector.cameraState.fov+offsetMargin) } } function addDirector(director) { directors.push(director) } let target = offset.clone() function tick() { computeCorners() sampleDirector() // Every n ticks, translate camera slightly then lerp to normal if (Math.random() > 1 - shake) { defaultOffset.add( Vector2( ((Math.random() - 0.5) * shake) / 2, ((Math.random() - 0.5) * shake) / 2, ), target, ) } const timescale = globalScope.timescale ?? 1 // globalScope is cleared on map navigation, not sure why? offset.lerp(shake > 0 ? target : defaultOffset, 0.1 * timescale) } function drawLocal(ctx) { ctx.fillStyle = 'green' ctx.textAlign = 'center' ctx.textBaseline = 'middle' // ctx.scale(size, size) ctx.font = '1px Roboto Mono' ctx.fillText('up', 0, 1) ctx.fillText('down', 0, -1) ctx.fillText('left', -1, 0) ctx.fillText('right', 1, 0) } function draw() { if (self.debug) { leftScreen.set(0, 0) rightScreen.set(0, screen.width) topScreen.set(0, screen.height) bottomScreen.set(0, 0) drawThrough(screen.ctx, drawLocal) } } function startRunning() {} function stopRunningLate() {} function drawThrough(ctx, drawCallback, localTransform) { ctx.save() worldToScreenCanvas(ctx, localTransform) drawCallback(ctx) ctx.restore() } function onResizeScreen() { computeCorners() } screen.resizeSubs.push(onResizeScreen) return self.mix({ transform, screen, lowerLeft, upperRight, worldToFrame, frameToWorld, worldToFrameScalar, frameToWorldScalar, worldToFrameDirection, frameToWorldDirection, worldToScreen, screenToWorld, worldToScreenScalar, screenToWorldScalar, worldToScreenCanvas, screenToWorldCanvas, tick, draw, start, startRunning, stopRunningLate, drawThrough, addDirector, computeCorners, get fov() { return fov }, set fov(v) { setFov(v) }, get activeDirector() { return activeDirector }, get position() { return transform.position }, set position(v) { transform.position.set(v) }, get rotation() { return rotation }, set rotation(v) { transform.rotation = v }, get shake() { return shake }, set shake(v) { shake = v }, get offset() { return defaultOffset }, set offset(v) { defaultOffset.set(v) }, }) } FILE: Types_Entities_Clouds.js function Cloud(spec) { const { ctx, camera, heightRange, speedRange, sizeRange, aspectRange, assets, } = spec let size let asset let speed let flipX let flipY let aspect let assetAspect let opacity let distance let progress = 0 let spawned = false let initialized = false const startPosition = Vector2() const endPosition = Vector2() const deltaPosition = Vector2() const position = Vector2() function spawn() { startPosition.x = math.lerp( camera.lowerLeft.x, camera.upperRight.x, Math.random(), ) startPosition.y = math.lerp( heightRange[0], heightRange[1], Math.pow(Math.random(), 1.5), ) endPosition.set(startPosition) distance = math.lerp(12, 24, Math.random()) endPosition.x += Math.random() < 0.5 ? -distance : distance endPosition.subtract(startPosition, deltaPosition) aspect = math.lerp(aspectRange[0], aspectRange[1], Math.random()) size = math.lerp(sizeRange[0], sizeRange[1], Math.random()) speed = math.lerp(speedRange[0], speedRange[1], Math.random()) asset = assets.images['cloud_' + (Math.floor(Math.random() * 9.9999) + 1)] assetAspect = asset.width / asset.height flipX = Math.random() < 0.5 ? 1 : 1 flipY = Math.random() < 0.5 ? 1 : 1 progress = 0 spawned = true initialized = true } function tick() { // if (!spawned) return // if (position.x > camera.upperRight.x + 1) spawned = false // else if (position.x < camera.lowerLeft.x - 1) spawned = false // else if (position.y < camera.lowerLeft.y - 1) spawned = false // else if (position.y > camera.upperRight.y + 1) spawn() if (!spawned) spawn() progress += (speed * tickDelta) / distance startPosition.lerp(endPosition, progress, position) if (progress >= 1) spawn() } function draw() { if (!spawned) return // ctx.beginPath() // ctx.moveTo(startPosition.x, -startPosition.y) // ctx.lineTo(endPosition.x, -endPosition.y) // ctx.lineWidth = camera.screenToWorldScalar() // ctx.stroke() ctx.globalAlpha = (0.5 * Math.pow(1 - Math.cos(TAU * progress), 1)) / 2 ctx.drawImage( asset, position.x - (aspect * flipX * size) / 2, -position.y - (flipY * size) / 2, assetAspect * aspect * size * flipX, size * flipY, ) } function log() { console.log( `Cloud size ${Math.round( size, )} moving from ${startPosition.toString()} to ${endPosition.toString()} at speed ${math.truncate( speed, )} — ${math.truncate(progress)}`, ) } function randomizeProgress() { progress = Math.random() } return { position, tick, draw, spawn, log, randomizeProgress, } } function Clouds(spec, name = 'Clouds') { const { self, screen, assets, density = 0.5, maxParticles = 5, sizeRange = [6, 12], heightRange = [8, 18], speedRange = [0.2, 0.8], aspectRange = [0.6, 1], camera, ctx, } = Entity(spec, name) const particles = [] let countdown = 3 let targetParticleCount = 0 for (let i = 0; i < maxParticles; i++) particles.push( Cloud({ ctx, camera, assets, sizeRange, speedRange, heightRange, aspectRange, }), ) function tick() { targetParticleCount = Math.min(camera.fov * density, maxParticles) countdown-- if (countdown > 0) return // particles[0].log() for (let i = 0; i < maxParticles; i++) { particles[i].tick() } if (countdown == 0) particles.forEach((v) => v.randomizeProgress()) } function drawLocal() { ctx.fillStyle = 'white' ctx.strokeStyle = 'green' // ctx.globalCompositeOperation = 'lighten' for (let i = 0; i < maxParticles; i++) { particles[i].draw() } } function draw() { camera.drawThrough(ctx, drawLocal) } return self.mix({ tick, draw }) } FILE: Types_Entities_ConstantLakeShader.js function ConstantLakeShader(spec) { const { self, screen } = Entity(spec, 'ConstantLakeShader') const { getWalkerPosition, defaultExpression } = spec const ctx = screen.ctx const transform = Transform(spec, self) let shouldUpdate = 0 let shouldRenderQuad = 0 let shouldTick = false const quad = ConstantLakeSunsetQuad({ defaultExpression, assets, }) self.mix(quad) function draw() { quad.render(getWalkerPosition()) ctx.drawImage(quad.localCanvas, 0, 0, screen.width, screen.height) shouldTick = true } function tick() { if (shouldTick) { quad.update() shouldTick = false } } function resize(width, height) { quad.resize(width, height) } return _.mixIn(self, { draw, resize, tick, transform, }) } FILE: Types_Entities_CoordinateBox.js function CoordinateBox(spec) { const { self } = Entity(spec, 'CoordinateBox') let { content = 'Hello', domSelector, style = {} } = spec let domElement = $(domSelector) style = { left: '50px', ...style } const helperBubble = document.createElement('div') helperBubble.innerHTML = content helperBubble.className = 'coordinate' Object.assign(helperBubble.style, style) domElement.appendChild(helperBubble) function destroy() { helperBubble.remove() } function selectCoordinates(screenX, screenY, worldX, worldY) { // TODO: Use either style object or manually set individual css // properties -- figure out best practice for JS-managed UI generally helperBubble.innerHTML = `(${worldX.toFixed(2)}, ${worldY.toFixed(2)})` helperBubble.style.left = `${screenX}px` helperBubble.style.bottom = `${screen.height - screenY}px` } return self.mix({ selectCoordinates, destroy, set visible(v) { helperBubble.style.visibility = v ? 'visible' : 'hidden' }, }) } FILE: Types_Entities_Directors_Director.js function Director(spec, defaultName = 'Director') { const { self, screen, camera } = Entity(spec, defaultName) let { globalScope } = spec const cameraState = CameraState() const bounds = spec.bounds ? Bounds(spec.bounds) : null camera.addDirector(self) const playerPosition = Vector2() function awake() { if (globalScope && globalScope.p) playerPosition.set(globalScope.p.re, globalScope.p.im) } function tick() { if (globalScope && globalScope.p) playerPosition.set(globalScope.p.re, globalScope.p.im) } function canControl() { // By default, controllers may activate if player is within bounds (if bounds are specified) or may always be active (if bounds are unspecified) return bounds ? bounds.contains(playerPosition) : true } function startControlling() { // virtual function stub } function stopControlling() { // virtual function stub } return self.mix({ awake, tick, bounds, cameraState, playerPosition, canControl, startControlling, stopControlling, get fov() { return cameraState.fov }, get position() { return cameraState.position }, get rotation() { return cameraState.rotation }, }) } FILE: Types_Entities_Directors_LerpDirector.js function LerpDirector(spec) { const { self, camera, screen, bounds, cameraState, playerPosition, tick: baseTick, awake: baseAwake, } = Director(spec, 'LerpDirector') let {} = spec const state0 = CameraState(spec.state0 || {}) const state1 = CameraState(spec.state1 || {}) const point0 = spec.point0 ? Vector2(spec.point0) : Vector2(bounds.min.x, bounds.center.y) const point1 = spec.point1 ? Vector2(spec.point1) : Vector2(bounds.max.x, bounds.center.y) let progress = 0 function awake() { baseAwake() computeState() } function tick() { baseTick() computeState() } function computeState() { // TODO: Interpolate from both x and y components, as god intended progress = playerPosition.x progress = math.unlerp(point0.x, point1.x, progress) progress = math.clamp01(progress) state0.lerp(state1, progress, cameraState) } function canControl() { const c = point1.x > playerPosition.x // console.log(`Can control lerp director: ${c}`, state0, state1, progress) return c } return self.mix({ awake, tick, canControl, }) } FILE: Types_Entities_Directors_PanDirector.js let n = 0.1 function PanDirector(spec) { const { self, screen, cameraState } = Director(spec, 'PanDirector') const { camera } = spec let {} = spec let velocity = Vector2() let newVelocity = Vector2() cameraState.position = Vector2() let justUpdatedVelocity = false let leftHeld = false let rightHeld = false let upHeld = false let downHeld = false let fromPoint = null let toPoint = null let callback = null let oldLerpPosition = Vector2() let transitDuration = 1 let transitProgress = 0 let transitComplete = false let panSpeed = 0.3 let clickable = Clickable({ entity: self, }) let lowerLeftScreen = Vector2() let heldDown = false function snapCameraToNearestPixel() { // camera.worldToScreen(camera.lowerLeft, lowerLeftScreen) // let { x, y } = lowerLeftScreen // let dx = Math.round(x) - x // let dy = Math.round(y) - y // cameraState.position.x += camera.screenToWorldScalar(dx) // cameraState.position.y += camera.screenToWorldScalar(dy) } function tick() { if (!transitComplete) { // If in transit, lerp between waypoints if (transitProgress == 1 && !transitComplete) { transitComplete = true if (callback) callback() } if (transitDuration == 0) transitProgress = 1 else if (toPoint) transitProgress += camera.tickDelta / transitDuration transitProgress = math.clamp01(transitProgress) if (fromPoint && toPoint && !transitComplete) { // oldPosition.set(cameraState.position) fromPoint.lerp(toPoint, transitProgress, cameraState) // cameraState.position.subtract(oldLerpPosition, positionDiff) // oldLerpPosition.set(cameraState.position) // oldPosition.add(positionDiff, cameraState.position) } return } // Else, navigate through user interaction if (leftHeld || rightHeld || upHeld || downHeld) { newVelocity.set(rightHeld - leftHeld, upHeld - downHeld) if (newVelocity.magnitude > 0) { newVelocity.normalize().multiply(panSpeed) } justUpdatedVelocity = true } if (!clickable.holding && !justUpdatedVelocity) { newVelocity.multiply(0.85) } velocity.lerp(newVelocity, 0.7, velocity) if (velocity.magnitude > 0.01) { cameraState.position.add(velocity) } justUpdatedVelocity = false // cameraState.position.x = Math.floor(cameraState.position.x / 0.1) * 0.1 // cameraState.position.y = Math.floor(cameraState.position.y / 0.1) * 0.1 snapCameraToNearestPixel() } function updateKeysHeld(key, held) { switch (key) { case 'ArrowLeft': case 'a': leftHeld = held break case 'ArrowRight': case 'd': rightHeld = held break case 'ArrowUp': case 'w': upHeld = held break case 'ArrowDown': case 's': downHeld = held break } transitComplete = true } function keyup(key) { updateKeysHeld(key, false) } function keydown(key) { updateKeysHeld(key, true) } function moveTo(waypointA, waypointB, duration = 0, cb = null) { fromPoint = CameraState(waypointA || camera) toPoint = CameraState(waypointB) oldLerpPosition.set(fromPoint.position) transitDuration = duration transitProgress = 0 transitComplete = false callback = cb } function setTargetFov(_fov) { cameraState.fov = _fov } function updateVelocity(mousePoint) { if (!heldDown) return transitComplete = true camera.worldToFrame(mousePoint, newVelocity) newVelocity.multiply(2) newVelocity.x = math.clamp(-1, 1, newVelocity.x) newVelocity.y = math.clamp(-1, 1, newVelocity.y) newVelocity.multiply(panSpeed) justUpdatedVelocity = true } function mouseDown(point, eventData) { const button = eventData.button if (button !== 2) { heldDown = true updateVelocity(point) } else { // HACK: Fix panning bug where // right-click menu breaks menu // by stopping on right-click heldDown = false velocity.set(0, 0) newVelocity.set(0, 0) } } function mouseUp() { heldDown = false } function hoverMove(point) { updateVelocity(point) } return self.mix({ tick, keydown, keyup, moveTo, setTargetFov, clickable, updateVelocity, mouseUp, mouseDown, hoverMove, }) } FILE: Types_Entities_Directors_TrackingDirector.js function TrackingDirector(spec) { const { self, screen, cameraState, parent } = Director( spec, 'TrackingDirector', ) let { trackedEntities = [] } = spec const { camera, globalScope, transitions = [] } = spec let { minFov = 5, minFovMargin = 3, smoothing = 0.02, verticalBias = 7, } = spec const targetState = CameraState({ fov: minFov, }) const minTrackPoint = Vector2() const maxTrackPoint = Vector2() const trackSpan = Vector2() let trackedEntityCount = 0 const difference = Vector2() let transitionActive = false let lastTransition = null cameraState.set(targetState) function awake() { snap() } let originalCameraOffset = camera.offset.clone() function tick() { trackEntities() transitionActive = false for (const transition of transitions) { if (_.inRange(targetState.position.x, ...transition.domain)) { lastTransition = transition transitionActive = true const transitionSmoothing = transition.properties.transitionSmoothing ?? 0.05 minFov = math.lerp( minFov, transition.properties.minFov ?? minFov, transitionSmoothing, ) minFovMargin = math.lerp( minFovMargin, transition.properties.minFovMargin ?? minFovMargin, transitionSmoothing, ) verticalBias = math.lerp( verticalBias, transition.properties.verticalBias ?? verticalBias, transitionSmoothing, ) smoothing = math.lerp( smoothing, transition.properties.smoothing ?? smoothing, transitionSmoothing, ) } } if (lastTransition && !transitionActive) { const transitionSmoothing = lastTransition.properties.transitionSmoothing ?? 0.05 minFov = math.lerp(minFov, spec.minFov ?? 5, transitionSmoothing) minFovMargin = math.lerp( minFovMargin, spec.minFovMargin ?? 3, transitionSmoothing, ) verticalBias = math.lerp( verticalBias, spec.verticalBias ?? 7, transitionSmoothing, ) smoothing = math.lerp( smoothing, spec.smoothing ?? 0.05, transitionSmoothing, ) } } function trackEntities() { targetState.fov = minFov targetState.position.set(0, 0) trackedEntityCount = 0 minTrackPoint.set(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY) maxTrackPoint.set(Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY) _.eachDeep(trackedEntities, trackEntity) if (trackedEntityCount == 0) { minTrackPoint.set() maxTrackPoint.set() } maxTrackPoint.subtract(minTrackPoint, trackSpan) // Bias for showing more background. Maybe should be configurable? maxTrackPoint.y += verticalBias // Correct for space occupied by equation bar const equationRatio = ui.expressionEnvelope.clientHeight / screen.height minTrackPoint.y -= 4 * trackSpan.y * equationRatio minTrackPoint.add(maxTrackPoint, targetState.position) targetState.position.divide(2) const ySpan = Math.abs(maxTrackPoint.y - minTrackPoint.y) const xSpan = Math.abs(maxTrackPoint.x - minTrackPoint.x) let span = Math.max(xSpan, ySpan) span = Math.max(span + minFovMargin, minFov) targetState.position.y += (0.5 * (span - ySpan)) / 2 cameraState.position.lerp(targetState.position, smoothing) targetState.fov = Math.max( Math.abs(maxTrackPoint.x - cameraState.position.x), Math.abs(maxTrackPoint.y - cameraState.position.y), ) targetState.fov = Math.max(targetState.fov + minFovMargin, minFov) cameraState.fov = math.lerp(cameraState.fov, targetState.fov, smoothing) } function trackEntity(entity) { if (!entity.active) return minTrackPoint.min(entity.transform.position) maxTrackPoint.max(entity.transform.position) if (entity.trackPoints) { for (point of entity.trackPoints) { minTrackPoint.min(point) maxTrackPoint.max(point) } } trackedEntityCount++ } function draw() {} function snap() { trackEntities() cameraState.set(targetState) trackEntities() cameraState.set(targetState) } function setGraphExpression() { snap() } function startRunning() {} function stopRunning() { snap() } return self.mix({ awake, tick, draw, startRunning, stopRunning, setGraphExpression, get trackedEntities() { return trackedEntities }, }) } FILE: Types_Entities_Directors_WaypointDirector.js function WaypointDirector(spec) { const { self, screen, cameraState } = Director(spec, 'WaypointDirector') const { camera } = spec let {} = spec let fromPoint = null let toPoint = null let callback = null let transitDuration = 1 let transitProgress = 0 let transitComplete = false function tick() { if (transitProgress == 1 && !transitComplete) { transitComplete = true if (callback) callback() } if (transitDuration == 0) transitProgress = 1 else if (toPoint) transitProgress += camera.tickDelta / transitDuration transitProgress = math.clamp01(transitProgress) if (fromPoint && toPoint && !transitComplete) { fromPoint.lerp(toPoint, transitProgress, cameraState) } } function canControl() { return !transitComplete } function lerpWaypoints(a, b, progress, output, smooth = true) { if (smooth) progress = math.smooth(progress) a.position.lerp(b.position, progress, output.position) output.rotation = math.modLerp(a.rotation, b.rotation, progress, TAU, true) output.fov = math.lerp(a.fov, b.fov, progress) } function moveTo(waypointA, waypointB, duration = 0, cb = null) { fromPoint = CameraState(waypointA || camera) toPoint = CameraState(waypointB) transitDuration = duration transitProgress = 0 transitComplete = false callback = cb lerpWaypoints(fromPoint, toPoint, 0, self) } function release() { fromPoint = null toPoint = null } function draw() {} return self.mix({ tick, draw, canControl, moveTo, release, get toPoint() { return toPoint }, }) } FILE: Types_Entities_Entity.js function Entity(spec, defaultName = 'Entity', dependencies = null) { const self = {} let { name = defaultName, active = true, enabled = true, parent = null, assets = null, camera = null, screen = null, tickDelta = null, getTime = null, ui = null, drawOrder = 0, debugSelf = false, debugTree = false, screenBuffer = null, activeRange = [NINF, PINF], motionBlur = true, blur = 0, world = null, } = spec // Because I constantly forget to use debugSelf instead of simply 'debug'. debugSelf = debugSelf || spec.debug // Inherit the fundamentals TODO: Make fundamentals a key/value abstraction if (parent) { parent.addChild(self) parent.root.addDescendant(self) if (!world) world = parent.world if (!camera) camera = parent.camera if (!assets) assets = parent.assets if (!screen) screen = parent.screen if (!tickDelta) tickDelta = parent.tickDelta if (!ui) ui = parent.ui if (!debugTree) debugTree = parent.debugTree if (_.isUndefined(spec.drawOrder)) drawOrder = parent.drawOrder } const ctx = screen ? screen.ctx : null // Mix in the fundamentals _.mixIn(self, { self, camera, screen, assets, ctx, ui, tickDelta, world, }) const children = [] const drawArray = parent ? [] : [self] let activeDrawArray = [] let destroyed = false const lifecycle = { awake: { entity: [], component: [], }, start: { entity: [], component: [], }, destroy: { entity: [], component: [], }, } if (spec.components) addComponents(spec.components) let entitiesToAwaken = null let activeInHierarchy = true let drawArrayIsUnsorted = true refreshActiveInHierarchy() // Called when the entity is fully constructed function awake() { sendLifecycleEvent('awake') } // Called just before the entity's first tick function start() { sendLifecycleEvent('start') } // Called when the object is to be fully removed from memory function destroy() { destroyed = true if (parent) { self.root.removeDescendant(self) parent.removeChild(self) } const e = lifecycle.destroy.entity while (e.length > 0) e.shift()() while (children.length > 0) children[0].destroy() } function sendLifecycleEvent(path) { const e = lifecycle[path].entity const c = lifecycle[path].component while (e.length > 0) e.shift()() while (c.length > 0) c.shift()() _.invokeEach(children, 'sendLifecycleEvent', arguments) } function sendEvent(path, args = [], latePath = null) { if (!active || !enabled) return // latePath is constructed on first call, and passed down for subsequent calls, to avoid memory pressure of creating the same new string for every object let argumentsArray = arguments if (latePath == null) { latePath = path + 'Late' argumentsArray = [...arguments] argumentsArray[2] = latePath } // Necessary to use _.get() so that 'parent.child' paths are supported. Do not change! let f = _.get(self, path) if (_.isFunction(f)) { const continuePropagating = f.apply(self, args) // Only if function explicitly returns false if (continuePropagating == false) { return } } _.invokeEach(children, 'sendEvent', argumentsArray) f = _.get(self, latePath) if (_.isFunction(f)) { const continuePropagating = f.apply(self, args) if (!continuePropagating) { return } } } function mix(other) { if (_.isFunction(other.awake)) lifecycle.awake.entity.push(other.awake) if (_.isFunction(other.start)) lifecycle.start.entity.push(other.start) if (_.isFunction(other.destroy)) lifecycle.destroy.entity.push(other.destroy) return _.mixIn(self, other, { start, awake, destroy }) } function hasChild(child) { return _.isInDeep(children, child) } function addChild(child) { if (hasChild(child)) return children.push(child) } function findChild(childName) { for (child of children) { if (child.name === childName) return child } return null } function findDescendant(descendantName) { for (child of children) { if (child.name === childName) return child let descendant = child.findDescendant(descendantName) if (descendant) return descendant } return null } function predraw() { screen.ctx.filter = `blur(${Math.floor(blur)}px)` } function sortChildren() { children.sort(compareChildren) } function compareChildren(a, b) { a = _.isNumber(a.drawOrder) ? a.drawOrder : 0 b = _.isNumber(b.drawOrder) ? b.drawOrder : 0 return a - b } function removeChild(child) { _.removeDeep(children, child) } function setActive(_active) { active = _active sendEvent('onSetActive', [_active]) drawArrayIsUnsorted = false refreshActiveInHierarchy() } function getLineage() { return parent ? parent.getLineage() + '.' + name : name } function getFromAncestor(path) { let v = _.get(self, path, undefined) if (_.isUndefined(v)) { if (parent) return parent.getFromAncestor(path) return null } return v } function toString() { return name } function addDescendant(descendant) { drawArray.push(descendant) drawArrayIsUnsorted = true } function removeDescendant(descendant) { if (drawArray.indexOf(descendant) >= 0) drawArray.splice(drawArray.indexOf(descendant), 1) } function isDescendantOf(other) { if (parent == null) return false if (parent == other) return true return parent.isDescendantOf(other) } function sortDrawArray() { drawArray.sort((a, b) => a.drawOrder - b.drawOrder) _.remove( drawArray, (v) => !v.draw || !v.isDescendantOf(self) || v.destroyed, ) drawArrayIsUnsorted = false } function refreshActiveInHierarchy() { activeInHierarchy = active && (!parent || parent.activeInHierarchy) for (const child of children) { child.refreshActiveInHierarchy() } } return _.mixIn(self, { awake, start, destroy, get destroyed() { return destroyed }, get name() { return name }, set name(v) { name = v }, lifecycle, mix, sendEvent, sendLifecycleEvent, hasChild, addChild, removeChild, addDescendant, removeDescendant, isDescendantOf, predraw, drawArray, get activeDrawArray() { return activeDrawArray }, sortDrawArray, findChild, findDescendant, getFromAncestor, getLineage, get lineage() { return getLineage() }, children, sortChildren, toString, get parent() { return parent }, get root() { return parent ? parent.root : self }, get active() { return active }, set active(v) { setActive(v) }, get enabled() { return enabled }, set enabled(v) { enabled = v }, get activeInHierarchy() { return activeInHierarchy }, refreshActiveInHierarchy, get drawOrder() { return drawOrder }, set drawOrder(v) { if (drawOrder != v) self.root.sortDrawArray() drawOrder = v }, get drawArrayIsUnsorted() { return drawArrayIsUnsorted }, get blur() { return blur }, set blur(v) { blur = v }, get activeRange() { return activeRange }, get debug() { return debugSelf || debugTree }, get motionBlur() { return motionBlur }, set motionBlur(v) { motionBlur = v }, }) } FILE: Types_Entities_Goals_DynamicGoal.js function DynamicGoal(spec) { const { self, screen, camera, transform, ctx, parent } = Goal( spec, 'Dynamic Goal', ) const base = _.mix(self) let { size = 1, globalScope, graph } = spec const bottom = Vector2(0, -size / 2) const bottomWorld = Vector2() const slopeTangent = Vector2() let startPosition = Vector2(spec) const shape = Circle({ transform, center: Vector2(0, 0), radius: size / 2, }) const clickable = Clickable({ entity: self, shape, transform, camera, }) const rigidbody = Rigidbody({ ...spec, transform, // fixedRotation: true, positionOffset: Vector2(0, -0.5), }) function drawLocal() { ctx.strokeStyle = self.strokeStyle ctx.fillStyle = self.fillStyle ctx.lineWidth = self.strokeWidth ctx.beginPath() ctx.arc(0, 0, size / 2, 0, TAU) ctx.fill() ctx.stroke() } function tick() { base.tick() rigidbody.tick() transform.transformPoint(bottom, bottomWorld) } function draw() { // Set alpha to fade with flash if completed self.setAlphaByFlashFade() camera.drawThrough(ctx, drawLocal, transform) base.draw() // Reset alpha ctx.globalAlpha = 1 if (self.debug) { rigidbody.draw(ctx) } } function reset() { base.reset() transform.position = startPosition transform.rotation = 0 transform.position.y = graph.sample('x', transform.position.x) + size / 2 transform.transformPoint(bottom, bottomWorld) rigidbody.resetVelocity() slopeTangent.x = 1 slopeTangent.y = graph.sampleSlope('x', bottomWorld.x) slopeTangent.normalize() // Set the Upright vector of rigidbody to the slope normal slopeTangent.orthogonalize(rigidbody.upright) let angle = math.atan2(slopeTangent.y, slopeTangent.x) transform.rotation = angle } function setX(x) { startPosition.x = x transform.position.x = x self.reset() } function setY(y) { startPosition.y = y transform.position.y = y self.reset() } /* Editor logic */ const editor = parent let moving = false function mouseDown() { if (!editor.editing) return transform.scale = 1.1 moving = true } function mouseMove(point) { if (!editor.editing) return if (!moving) return startPosition = point transform.position = point editor.update(false) } function mouseUp() { if (!editor.editing) return if (!moving) return transform.scale = 1 moving = false reset() editor.update() } function select() { if (!editor.editing) return editor.select(self, ['x', 'order']) } function deselect() { if (!editor.editing) return editor.deselect() } function setX(x) { startPosition.x = x transform.position.x = x self.reset() } function setY(y) { startPosition.y = y transform.position.y = y self.reset() } return self.mix({ transform, rigidbody, mouseDown, mouseUp, mouseMove, select, deselect, clickable, tick, draw, reset, setX, setY, shape, get type() { return 'dynamic' }, }) } FILE: Types_Entities_Goals_FixedGoal.js function FixedGoal(spec) { const { self, screen, camera, transform, ctx, parent } = Goal( spec, 'Fixed Goal', ) const base = _.mix(self) let { world, size = 1 } = spec const shape = Rect({ transform, width: size, height: size, }) const clickable = Clickable({ entity: self, shape, transform, camera, }) t = 0 function drawLocal() { t += 0.01 if (clickable.selectedInEditor) { transform.scale = 1.1 ctx.fillStyle = ctx.createConicGradient(Math.PI / 4, size / 2, size / 2) ctx.fillStyle.addColorStop(t % 1, '#FBA') ctx.fillStyle.addColorStop((t + 0.25) % 1, '#BC1') ctx.fillStyle.addColorStop((t + 0.5) % 1, '#BFC') ctx.fillStyle.addColorStop((t + 0.75) % 1, '#A9D') ctx.fillStyle.addColorStop((t + 1) % 1, '#A9B') ctx.lineWidth = 0.05 let outlinePadding = 0.3 ctx.fillRect( -size / 2 - outlinePadding / 2, -size / 2 - outlinePadding / 2, size + outlinePadding, size + outlinePadding, ) } else { transform.scale = 1 } ctx.strokeStyle = self.strokeStyle ctx.fillStyle = self.fillStyle ctx.lineWidth = self.strokeWidth ctx.fillRect(-size / 2, -size / 2, size, size) ctx.strokeRect(-size / 2, -size / 2, size, size) } function draw() { // Set alpha to fade with flash if completed self.setAlphaByFlashFade() camera.drawThrough(ctx, drawLocal, transform) base.draw() // Reset alpha ctx.globalAlpha = 1 } /* Editor logic */ const editor = parent let moving = false function select() { if (!editor.editing) return editor.select(self, ['x', 'y', 'order']) } function deselect() { if (!editor.editing) return editor.deselect() } function mouseDown() { if (!editor.editing) return moving = true } function mouseMove(point) { if (!moving) return transform.position = point editor.update() } function mouseUp() { if (!moving) return moving = false } return self.mix({ draw, shape, clickable, mouseDown, mouseMove, mouseUp, select, deselect, get type() { return 'fixed' }, }) } FILE: Types_Entities_Goals_Goal.js function Goal(spec) { const { self, screen, ctx, parent } = Entity(spec, 'Goal') const transform = Transform(spec) let { type = 'fixed', order = null, size = 1, camera, sledders, goalCompleted, goalFailed, globalScope, getLowestOrder, } = spec let triggered = false let available = false let completed = false let failed = false let triggeringSledder = null const triggeringSledderPosition = Vector2() const triggeringSledderDelta = Vector2() const completedFill = Color('#44FF66') const triggeredFill = Color('#88CCFF') const availableFill = Color('#FFFFFF') const unavailableFill = Color('#FFCC22') const failedFill = Color('#FF0044') const completedStroke = Color('#000000') const triggeredStroke = Color('#000000') const availableStroke = Color('#000000') const unavailableStroke = Color('#000000') const failedStroke = Color('#FFFFFF') let fillColor = Color() let strokeColor = Color() let strokeColorB = Color() const flashWhite = Color('#FFFFFF') let strokeStyle let fillStyle let strokeWidth = 0.08 const completedStrokeB = Color(completedStroke).setV(0.4) const triggeredStrokeB = Color(triggeredStroke).setV(0.4) const availableStrokeB = Color(availableStroke).setV(0.4) const unavailableStrokeB = Color(unavailableStroke).setV(0.4) const failedStrokeB = Color(failedStroke).setV(0.8) const worldPosition = Vector2() const cameraDirection = Vector2() let cameraDistance = 0 const sledderPosition = Vector2() let flashProgress = 0 const isSafari = navigator.userAgent.toLowerCase().indexOf('safari/') > -1 && navigator.userAgent.toLowerCase().indexOf('chrome/') == -1 function awake() { self.reset() } function tVariableChanged() { self.reset() } function tick() { if (globalScope.running) { self.refreshTriggered() self.checkComplete() } flashProgress *= 0.95 cameraDirection.set(camera.transform.position) cameraDistance = cameraDirection.magnitude // if (spec.toolbarGoal) debugger if (cameraDistance == 0) cameraDirection.set(0, 1) else cameraDirection.normalize() } function refreshTriggered() { let alreadyTriggered = triggered triggered = false triggeringSledder = null for (sledder of sledders) { if (intersectSledder(sledder)) { if (!alreadyTriggered) triggeringSledderPosition.set(sledder.transform.position) sledder.transform.position.subtract( triggeringSledderPosition, triggeringSledderDelta, ) triggeringSledderPosition.set(sledder.transform.position) triggered = true triggeringSledder = sledder break } } } function checkComplete() { if (triggered && !completed && !failed) { if (available) complete() else fail() } } function complete() { if (completed) return flashProgress = 1 completed = true goalCompleted(self) } function fail() { if (failed) return flashProgress = 1 failed = true goalFailed(self) } function intersectSledder(sledder) { for (sledderPoint of sledder.pointCloud) { sledderPosition.set(sledderPoint) sledder.transform.transformPoint(sledderPosition) if (self.shape.intersectPoint(sledderPosition)) return true } return false } function drawLocal() { if (order) { ctx.save() // ctx.fillStyle = strokeStyle ctx.textAlign = 'center' ctx.textBaseline = 'middle' ctx.font = '1px Roboto Mono' const rescale = 0.7 ctx.scale(rescale, rescale) ctx.translate(0, isSafari ? 0.325 : 0) let center = self.shape.center ctx.fillText(order, center.x / rescale, -center.y / rescale) ctx.restore() } } function setAlphaByFlashFade() { ctx.globalAlpha = completed ? flashProgress : 1 } function draw() { refreshColors() camera.drawThrough(ctx, drawLocal, transform) if (self.debug) { shape.draw(ctx, camera) } } function reset() { triggered = false completed = false failed = false triggeringSledder = null self.refresh() } function startRunning() {} function stopRunning() { self.reset() } function refreshColors() { strokeColor.set( completed ? completedStroke : failed ? failedStroke : triggered ? triggeredStroke : available ? availableStroke : unavailableStroke, ) strokeColorB.set( completed ? completedStrokeB : failed ? failedStrokeB : triggered ? triggeredStrokeB : available ? availableStrokeB : unavailableStrokeB, ) availableFill.lerp(completedFill, self.completedProgress, triggeredFill) fillColor.set( completed ? completedFill : failed ? failedFill : triggered ? triggeredFill : available ? availableFill : unavailableFill, ) strokeColor.lerp(flashWhite, flashProgress) strokeColorB.lerp(flashWhite, flashProgress) fillColor.lerp(flashWhite, flashProgress) strokeStyle = ctx.createLinearGradient( (cameraDirection.x * size) / 2, (-cameraDirection.y * size) / 2, (-cameraDirection.x * size) / 2, (cameraDirection.y * size) / 2, ) strokeStyle.addColorStop(0, strokeColorB.hex) strokeStyle.addColorStop(1 - 1 / (1 + cameraDistance), strokeColor.hex) strokeStyle.addColorStop(1, strokeColorB.hex) fillStyle = fillColor.hex } function refresh() { available = true if (order) { available = getLowestOrder().localeCompare(order) >= 0 } self.refreshColors() } const baseDestroy = self.destroy function destroy() { baseDestroy() // Level bubbles (TODO: Clean up separation of concerns // between normal level and bubble) if (parent.goalDeleted) parent.goalDeleted() } function setOrder(v) { order = v } function setX(x) { transform.position.x = x } function setY(y) { transform.position.y = y } return self.mix({ tVariableChanged, transform, destroy, awake, setOrder, setX, setY, tick, draw, reset, refresh, refreshColors, startRunning, stopRunning, refreshTriggered, checkComplete, complete, fail, setAlphaByFlashFade, get x() { return transform.position.x }, get y() { return transform.position.y }, get completed() { return completed }, get available() { return available }, get triggered() { return triggered }, get failed() { return failed }, get order() { return order }, get triggeringSledder() { return triggeringSledder }, get triggeringSledderPosition() { return triggeringSledderPosition }, get triggeringSledderDelta() { return triggeringSledderDelta }, get fillStyle() { return fillStyle }, get strokeStyle() { return strokeStyle }, get strokeWidth() { return strokeWidth }, get flashProgress() { return flashProgress }, get flashWhite() { return flashWhite }, get completedProgress() { return completed ? 1 : 0 }, // TODO: Separate level editor state from playing level/normal levels get selectable() { // HACK: Necessary until above TODO is addressed^ // return false return !globalScope.running }, }) } FILE: Types_Entities_Goals_PathGoal.js function PathGoal(spec) { const { self, camera, transform, ctx, parent } = Goal(spec, 'Path Goal') const base = _.mix(self) let { assets, size = 1, globalScope, expression: pathExpression = 'sin(x)', pathX = 4, pathY = 0, graph, debug = false, // Optional, used for editor expressionLatex: pathExpressionLatex = null, } = spec if (!pathExpressionLatex) { // Ideally this should never happen, // but to prevent PathGoal from breaking pathExpressionLatex = pathExpression } const minBoundsHeight = 1 let trackPoints = [] const bottom = Vector2(0, -size / 2) const bottomWorld = Vector2() const slopeTangent = Vector2() const pathPosition = Vector2() const pathPositionWorld = Vector2() const pathStart = Vector2() const pathEnd = Vector2(pathX, 0) const pathMin = Vector2() const pathMax = Vector2() const pathStartWorld = Vector2() const pathEndWorld = Vector2() const pathStartScreen = Vector2() const pathEndScreen = Vector2() const pathMinWorld = Vector2() const pathMaxWorld = Vector2() let pathSign = Math.sign(pathX) let pathSpan = Math.abs(pathX) let maxPathResetSpeed = 3 let pathResetSpeed = 0 let pathProgress = 0 const outerColor = Color() const outerColors = [Color('#111111'), Color('#444444'), Color('#333333')] const shape = Circle({ transform, center: Vector2(0, 0), radius: size / 2, }) // Establish path origin in world space transform.transformPoint(pathStart, pathStartWorld) transform.transformPoint(pathEnd, pathEndWorld) const pathGraph = Graph({ name: 'Path Graph', parent: self, globalScope, expression: pathExpression, freeze: true, fill: false, scaleStroke: true, bounds: [pathStartWorld.x, pathEndWorld.x], sampleCount: Math.round(pathSpan * 4), strokeWidth: 1, strokeColor: '#888', dashed: true, dashSettings: [0.5, 0.5], useInterpolation: false, }) const hintGraph = Graph({ name: 'Path Hint Graph', parent: self, globalScope, expression: pathExpression, fill: false, scaleStroke: true, strokeWidth: 0.1, strokeColor: '#FFA500', dashed: true, dashSettings: [0.5, 0.5], // fixedPoints: true, }) // HACK: Hijack the graph's draw method to draw it behind the goal object const drawPathGraph = pathGraph.draw.bind(pathGraph) pathGraph.draw = () => {} const drawHintGraph = hintGraph.draw.bind(hintGraph) hintGraph.draw = () => {} assets.sounds.path_goal_start.volume(0.5) assets.sounds.path_goal_continue.volume(0.5) // Sample start/end points pathStartWorld.y = pathGraph.sample('x', pathStartWorld.x) pathEndWorld.y = pathGraph.sample('x', pathEndWorld.x) // Move transform to start of path transform.position = pathStartWorld // Compute world-space points transform.invertPoint(pathStartWorld, pathStart) transform.invertPoint(pathEndWorld, pathEnd) pathPosition.set(pathStart) transform.transformPoint(pathPosition, pathPositionWorld) // Compute min/max points pathStart.min(pathEnd, pathMin) pathStart.max(pathEnd, pathMax) pathStartWorld.min(pathEndWorld, pathMinWorld) pathStartWorld.max(pathEndWorld, pathMaxWorld) // trackPoints.push(pathStartWorld) // trackPoints.push(pathEndWorld) trackPoints.push(pathGraph.minSample) trackPoints.push(pathGraph.maxSample) // Handles for editor const leftHandle = PathGoalHandle({ parent: self, transform, drawOrder: Infinity, type: 'start', editor: parent, // parent is implicitly editor }) const rightHandle = PathGoalHandle({ parent: self, transform, drawOrder: Infinity, type: 'end', editor: parent, }) const boundsTransform = Transform({ x: spec.x, }) const bounds = Rect({ transform: boundsTransform, }) updateBounds() const union = Union({ shapes: [shape, bounds, leftHandle, rightHandle], }) const clickable = Clickable({ entity: self, shape: union, transform, camera, }) function onRequestAssetsPass(requestAssets) { requestAssets( ['sounds.path_goal_continue', 'sounds.path_goal_start'], (assets) => (localAssets = assets), ) } function setEnds(pathStartX, pathEndX) { pathX = pathEndX - pathStartX pathStart.set(pathStartX, 0) pathEnd.set(pathEndX, 0) pathSign = Math.sign(pathX) pathSpan = Math.abs(pathX) // If graph grew/shrunk enough, update sample count const newSampleCount = Math.round(pathSpan * 4) if (Math.abs(pathGraph.sampleCount - newSampleCount) > 5) pathGraph.sampleCount = newSampleCount dragMove(transform.position) } function updateBounds() { const max = pathGraph.samples.reduce( (max, el) => (el[1] > max ? el[1] : max), NINF, ) const min = pathGraph.samples.reduce( (min, el) => (el[1] < min ? el[1] : min), PINF, ) const height = Math.max(max - min, minBoundsHeight) const verticalCenter = (max + min) / 2 const startY = pathGraph.sample('x', transform.x + pathStart.x) const endY = pathGraph.sample('x', transform.x + pathEnd.x) bounds.width = Math.abs(pathX) bounds.height = height bounds.center = Vector2((pathStart.x + pathEnd.x) / 2, verticalCenter) leftHandle.transform.position.set(transform.x + pathStart.x, startY) rightHandle.transform.position.set(transform.x + pathEnd.x, endY) } function tick() { base.tick() tickPath() } function tickPath() { if (!self.completed && !self.failed) { const pathProgressZero = pathProgress == 0 if (self.triggered) { pathPositionWorld.x += self.triggeringSledderDelta.x pathResetSpeed = 0 } else { pathPositionWorld.x -= pathSign * self.tickDelta * pathResetSpeed pathResetSpeed = Math.min( pathResetSpeed + self.tickDelta * 6, maxPathResetSpeed, ) } pathPositionWorld.x = math.clamp( pathMinWorld.x, pathMaxWorld.x, pathPositionWorld.x, ) pathProgress = math.unlerp( pathStartWorld.x, pathEndWorld.x, pathPositionWorld.x, ) pathPositionWorld.y = pathGraph.sample('x', pathPositionWorld.x) transform.invertPoint(pathPositionWorld, pathPosition) shape.center.set(pathPosition) if (self.triggered && pathProgress != 0 && pathProgressZero) { assets.sounds.path_goal_start.play() assets.sounds.path_goal_continue.loop(true) assets.sounds.path_goal_continue.play() } if (!self.triggered && pathProgress == 0 && !pathProgressZero) { assets.sounds.path_goal_continue.stop() } } } function checkComplete() { if (self.triggered && !self.completed && !self.failed) { if (!self.available) self.fail() else if (pathProgress == 1) { self.complete() assets.sounds.path_goal_continue.stop() } } } function drawLocal() { ctx.strokeStyle = self.strokeStyle ctx.fillStyle = self.fillStyle ctx.lineWidth = self.strokeWidth ctx.beginPath() ctx.arc(pathPosition.x, -pathPosition.y, size / 2, 0, TAU) ctx.fill() ctx.stroke() // ctx.beginPath() // ctx.arc(pathEnd.x, -pathEnd.y, size / 2, 0, TAU) // ctx.strokeStyle = '#888' // ctx.stroke() if (self.debug) { ctx.font = '1px Roboto Mono' ctx.fillStyle = 'green' ctx.textAlign = 'center' ctx.textBaseline = 'middle' ctx.fillText( 'position: ' + pathPosition.toString(), pathPosition.x, -pathPosition.y, ) ctx.fillText('start: ' + pathStart.toString(), pathStart.x, -pathStart.y) ctx.fillText('end: ' + pathEnd.toString(), pathEnd.x, -pathEnd.y) } } function draw() { // Set alpha to fade with flash if completed self.setAlphaByFlashFade() camera.worldToScreen(pathStartWorld, pathStartScreen) camera.worldToScreen(pathEndWorld, pathEndScreen) let outerStyle = ctx.createLinearGradient( pathStartScreen.x, 0, pathEndScreen.x, 0, ) for (let i = 0; i < outerColors.length; i++) { let p = i / (outerColors.length - 1) outerColor.set(outerColors[i]).lerp(self.flashWhite, self.flashProgress) outerStyle.addColorStop(p, outerColor.hex) } let innerStyle = ctx.createLinearGradient( pathStartScreen.x, 0, pathEndScreen.x, 0, ) innerStyle.addColorStop(0, '#6F0') innerStyle.addColorStop(pathProgress / 2, '#4F6') innerStyle.addColorStop(pathProgress, '#4F6') innerStyle.addColorStop(math.clamp01(pathProgress + 0.02), '#FFF') if (editor.editing && clickable.selected) { hintGraph.resize() drawHintGraph() } pathGraph.strokeWidth = 0.4 pathGraph.strokeColor = outerStyle pathGraph.dashed = false drawPathGraph() pathGraph.strokeWidth = 0.2 pathGraph.strokeColor = innerStyle pathGraph.dashed = true drawPathGraph() camera.drawThrough(ctx, drawLocal, transform) base.draw() // Reset alpha ctx.globalAlpha = 1 if (self.debug) { shape.draw(ctx, camera) if (dynamic) rigidbody.draw(ctx) } // TODO: Polish bounding box if (debug || clickable.selectedInEditor) union.draw(ctx, camera) } function reset() { base.reset() pathPosition.set(pathStart) pathPositionWorld.set(pathStartWorld) // TODO: Fix circle jitter w/o calling tick() tick() } function resize() { hintGraph.resize() } /* Editor logic */ const editor = parent // Parent is implicitly editor let oldExpressionLatex function select() { if (!editor.editing) return parent.sendEvent('selectedPathGoalForEditing') editor.select(self, ['start', 'end']) oldExpressionLatex = parent.currentLatex ui.mathFieldLabel.innerText = 'P=' ui.mathField.latex(pathExpressionLatex) ui.mathFieldStatic.latex(pathExpressionLatex) } function deselect() { if (!editor.editing) return parent.sendEvent('unselectedPathGoalForEditing') editor.deselect() ui.mathFieldLabel.innerText = 'Y=' ui.mathField.latex(oldExpressionLatex) ui.mathFieldStatic.latex(oldExpressionLatex) } function setGraphExpression(text, latex) { if (!clickable.selected) return if (!text) return pathExpression = text pathExpressionLatex = latex pathGraph.expression = text pathGraph.resample() hintGraph.expression = text hintGraph.resample() ui.mathFieldStatic.latex(latex) updateBounds() } function dragMove(point) { if (!editor.editing) return transform.position.x = point.x // Reset pathStart/pathEnd // pathStart.x += delta.x // pathEnd.x += delta.x // Re-transform to world space transform.transformPoint(pathStart, pathStartWorld) transform.transformPoint(pathEnd, pathEndWorld) // Reset pathPosition pathPosition.set(pathStart) transform.transformPoint(pathPosition, pathPositionWorld) pathStart.min(pathEnd, pathMin) pathStart.max(pathEnd, pathMax) pathStartWorld.min(pathEndWorld, pathMinWorld) pathStartWorld.max(pathEndWorld, pathMaxWorld) // Re-calculate path progress tickPath() // Update graph pathGraph.bounds[0] = pathStartWorld.x pathGraph.bounds[1] = pathEndWorld.x pathGraph.resample() boundsTransform.x = point.x updateBounds() trackPoints = [pathStartWorld, pathEndWorld] editor.update(false) // Don't propagate drag events to handles return false } function mouseDown() { // Don't propagate mouse down event to handles return false } function dragEnd() { if (!editor.editing) return editor.update() reset() } let _p = Vector2() function setX(x) { // HACK: Not sure why, but // when I move the code in dragMove // to setX() the bounding box doesn't // update properly? TODO: Debug _p.x = x dragMove(_p) } const minPathLength = 1 function setStart(newStart) { const newStartX = newStart - transform.x if (Math.abs(newStartX - pathEnd.x) >= minPathLength) { setEnds(newStartX, pathEnd.x) } editor.update(false) } function setEnd(newEnd) { const newEndX = newEnd - transform.x if (Math.abs(newEndX - pathStart.x) >= minPathLength) { setEnds(pathStart.x, newEndX) } editor.update(false) } return self.mix({ transform, tick, draw, setStart, setEnd, setX, select, deselect, mouseDown, dragMove, dragEnd, reset, resize, checkComplete, trackPoints, shape, clickable, setGraphExpression, bounds, boundsTransform, setEnds, get selected() { return clickable.selected }, get starting() { return pathStart.x + transform.x }, get ending() { return pathEnd.x + transform.x }, get pathStart() { return pathStart }, get pathEnd() { return pathEnd }, get pathExpression() { return pathExpression }, get pathExpressionLatex() { return pathExpressionLatex }, get completedProgress() { return pathProgress }, get type() { return 'path' }, set debug(v) { debug = v }, }) } FILE: Types_Entities_Graph.js function Graph(spec) { const { self, screen, camera, ctx } = Entity(spec, 'Graph') let { sampleCount = 129, sampleDensity = 4, globalScope, colors = Colors.biomes.alps, bounds, freeze = false, fill = true, scaleStroke = false, stroke = true, dashed = false, dashOffset = 0, dashSettings = [0.5, 0.5], sledders = [], useInterpolation = true, refreshPeriodT = 0.3, fixedPoints = false, } = spec // Polar let { polar = false, minTheta = 0, maxTheta = TAU, invertGravity = false, } = spec let { strokeColor = colors.groundStroke, strokeWidth = colors.groundStrokeWidth, fillColor = colors.groundFill, } = spec if (bounds && sampleDensity) { let span = Math.abs(bounds[0] - bounds[1]) sampleCount = Math.ceil(sampleDensity * span) } let minX, maxX const undashedSettings = [] const dashSettingsScreen = [0, 0] // Scope is global scope const scope = globalScope const sampler = new Sampler({ ...spec, scope, }) let samples = sampler.generateSampleArray(sampleCount) let interpolationSampler = null if (useInterpolation) { interpolationSampler = new InterFrameSampler({ sampler, sampleCount, refreshPeriodT, }) } const screenSpaceSample = Vector2() const minWorldPoint = Vector2() const maxWorldPoint = Vector2() const minSample = Vector2() const maxSample = Vector2() let terrainLayers = 6 let terrainParameters generateTerrainParameters() function generateTerrainParameters() { terrainParameters = [] for (let i = 0; i < terrainLayers; i++) { scalar = math.lerp(1, 3, Math.random()) terrainParameters.push([ math.lerp(0, TAU, Math.random()), math.lerp(0.3, 3, Math.random()), math.lerp(3, 5, Math.random()) * scalar, math.lerp(1, 3, Math.random()) * scalar, math.lerp(0.05, 0.25, Math.random()), ]) } } function resample(refresh = false) { if (polar) { /* Resample polar graph */ sampler.resetExtrema() sampler.sampleRange( scope, samples, sampleCount, 'theta', minTheta, maxTheta, ) minSample.y = math.pinf maxSample.y = math.ninf for (const sample of samples) { const theta = sample[0], r = sample[1] sample.set(r * Math.cos(theta), r * Math.sin(theta)) if (sample.y < minSample.y) minSample.set(sample) if (sample.y > maxSample.y) maxSample.set(sample) } } else { /* Resample cartesian graph */ updateXBounds() if (useInterpolation) { if (refresh) interpolationSampler.refresh(minX, maxX, scope.t, scope.dt) interpolationSampler.resetExtrema() interpolationSampler.sampleRange( scope, samples, sampleCount, minX, maxX, ) minSample.set(minX, interpolationSampler.min) maxSample.set(maxX, interpolationSampler.max) } else { sampler.resetExtrema() sampler.sampleRange(scope, samples, sampleCount, 'x', minX, maxX) minSample.set(minX, sampler.min) maxSample.set(maxX, sampler.max) } } } function tVariableChanged() { resample(true) } function tick() { if (!freeze) resample() } function drawCartesianGraph() { const worldToScreenScalar = camera.worldToScreenScalar() const strokeScalar = scaleStroke ? worldToScreenScalar : 1 if (fill) { ctx.beginPath() camera.worldToScreen(samples[0], screenSpaceSample) ctx.moveTo(screenSpaceSample.x, screenSpaceSample.y) for (let i = 1; i < sampleCount; i++) { camera.worldToScreen(samples[i], screenSpaceSample) ctx.lineTo(screenSpaceSample.x, screenSpaceSample.y) } ctx.lineTo(screen.width, screen.height) ctx.lineTo(0, screen.height) ctx.fillStyle = fillColor ctx.fill() ctx.clip() for (let i = 0; i < terrainLayers; i++) drawSine.apply(null, terrainParameters[i]) } if (stroke) { ctx.beginPath() let roundedSample // TODO: Implement fixed points to make // stroked hint graphs not "march" if (fixedPoints) { const sample = samples[1] const roundedSampleX = Math.round(sample.x) const sampleA = sample const sampleB = samples[i + 1] const roundedSampleSlope = (sampleB.y - sampleA.y) / (sampleB.x - sampleA.x) const roundedSampleY = sample.y + (roundedSampleX - sample.x) * roundedSampleSlope roundedSample.set(roundedSampleX, roundedSampleY) camera.worldToScreen(roundedSample, screenSpaceSample) } else { camera.worldToScreen(samples[0], screenSpaceSample) } ctx.moveTo(screenSpaceSample.x, screenSpaceSample.y) for (let i = 1; i < sampleCount; i++) { const sample = samples[i] if (fixedPoints) { const roundedSampleX = Math.round(sample.x) let sampleA, sampleB if (i == sampleCount - 1) { sampleA = samples[i - 1] sampleB = sample } else { sampleA = sample sampleB = samples[i + 1] } const roundedSampleSlope = (sampleB.y - sampleA.y) / (sampleB.x - sampleA.x) const roundedSampleY = sample.y + (roundedSampleX - sample.x) * roundedSampleSlope roundedSample.set(roundedSampleX, roundedSampleY) camera.worldToScreen(roundedSample, screenSpaceSample) } else { camera.worldToScreen(sample, screenSpaceSample) } ctx.lineTo(screenSpaceSample.x, screenSpaceSample.y) } dashSettingsScreen[0] = dashSettings[0] * strokeScalar dashSettingsScreen[1] = dashSettings[1] * strokeScalar ctx.setLineDash(dashed ? dashSettingsScreen : undashedSettings) ctx.dashOffset = 30 ctx.lineCap = 'round' ctx.strokeStyle = strokeColor ctx.lineWidth = strokeWidth * strokeScalar ctx.stroke() } } const debugVectorOrigin = Vector2() const debugVectorTerminus = Vector2() function drawDebugVector(ctx, vector, color, origin = position) { camera.worldToScreen(origin, debugVectorOrigin) debugVectorTerminus.set(vector) debugVectorTerminus.add(origin) camera.worldToScreen(debugVectorTerminus) ctx.beginPath() ctx.moveTo(debugVectorOrigin.x, debugVectorOrigin.y) ctx.lineTo(debugVectorTerminus.x, debugVectorTerminus.y) ctx.strokeStyle = color ctx.lineWidth = 4 ctx.stroke() } let point = Vector2(), tangent = Vector2(), normal = Vector2() function drawPolarGraph() { const worldToScreenScalar = camera.worldToScreenScalar() const strokeScalar = scaleStroke ? worldToScreenScalar : 1 // Fill if curve is closed. TODO: I'm pretty sure // this doesn't work in every case? const start = samples[0] const end = samples[sampleCount - 1] fill = start.distance(end) < 0.001 if (fill) { ctx.beginPath() camera.worldToScreen(samples[0], screenSpaceSample) ctx.moveTo(screenSpaceSample.x, screenSpaceSample.y) for (let i = 1; i < sampleCount; i++) { camera.worldToScreen(samples[i], screenSpaceSample) ctx.lineTo(screenSpaceSample.x, screenSpaceSample.y) } // ctx.lineTo(screen.width, screen.height) // ctx.lineTo(0, screen.height) ctx.fillStyle = fillColor ctx.fill() ctx.clip() // for (let i = 0; i < terrainLayers; i++) // drawSine.apply(null, terrainParameters[i]) } if (stroke) { ctx.beginPath() camera.worldToScreen(samples[0], screenSpaceSample) ctx.moveTo(screenSpaceSample.x, screenSpaceSample.y) for (let i = 1; i < sampleCount; i++) { const sample = samples[i] camera.worldToScreen(sample, screenSpaceSample) ctx.lineTo(screenSpaceSample.x, screenSpaceSample.y) } dashSettingsScreen[0] = dashSettings[0] * strokeScalar dashSettingsScreen[1] = dashSettings[1] * strokeScalar ctx.setLineDash(dashed ? dashSettingsScreen : undashedSettings) ctx.dashOffset = 30 ctx.lineCap = 'round' ctx.strokeStyle = strokeColor ctx.lineWidth = strokeWidth * strokeScalar ctx.stroke() } for (let theta = 0; theta <= TAU + 0.001; theta += TAU / 12) { pointAtTheta(theta, point) normalVectorAt(theta, normal) normal.normalize() normal.multiply(0.2) drawDebugVector(ctx, normal, 'black', point) tangentVectorAt(theta, tangent) tangent.normalize() tangent.negate() drawDebugVector(ctx, tangent, 'blue', point) } } function draw() { ctx.save() if (polar) drawPolarGraph() else drawCartesianGraph() ctx.restore() } function drawSine( xOffset = 0, yOffset = 0, xScale = 1, yScale = 1, opacity = 0.5, ) { ctx.beginPath() ctx.globalAlpha = opacity camera.worldToScreen(samples[0], screenSpaceSample) ctx.moveTo(screen.width, screen.height) ctx.lineTo(0, screen.height) for (let i = 0; i < sampleCount; i++) { const x = samples[i].x const increasedX = x + 50 // if (!window.logged) // console.log(samples[i]); window.logged = true camera.worldToScreen(samples[i], screenSpaceSample) const y = screenSpaceSample.y + (Math.sin(x / xScale - xOffset) + 1) * camera.worldToScreenScalar(1) * yScale + yOffset * camera.worldToScreenScalar(1) ctx.lineTo(screenSpaceSample.x, y) } ctx.fillStyle = _.isString(colors.groundPattern) ? colors.groundPattern : _.sample(colors.groundPattern) ctx.fill() ctx.globalAlpha = 1 } function updateXBounds() { camera.frameToWorld(screen.minFramePoint, minWorldPoint) camera.frameToWorld(screen.maxFramePoint, maxWorldPoint) if (bounds) { minX = bounds[0] maxX = bounds[1] } else { minX = minWorldPoint[0] maxX = maxWorldPoint[0] } // TODO: Update min/max theta? } function resize() { resample(true) } function startRunning() { resample(true) } function stopRunning() { resample(true) _.invokeEach(sledders, 'reset') } function assignColors(_colors) { colors = _colors strokeColor = colors.groundStroke strokeWidth = colors.groundStrokeWidth fillColor = colors.groundFill } function pointAtTheta(theta, p) { const r = self.sample('theta', theta) p.set(r * Math.cos(theta), r * Math.sin(theta)) } const pAtTheta = Vector2(), pAtMinusTheta = Vector2() function thetaOfClosestSurfacePoint(position) { const theta = Math.atan2(position.y, position.x) pointAtTheta(theta, pAtTheta) pointAtTheta(-theta, pAtMinusTheta) if (pAtTheta.distance(position) < pAtMinusTheta.distance(position)) { return theta } else { return -theta } } function sampleSlopeFromTheta(theta) { const r = self.sample('theta', theta) const rPrime = self.sampleSlope('theta', theta) const sinTheta = Math.sin(theta), cosTheta = Math.cos(theta) const graphSlope = (rPrime * sinTheta + r * cosTheta) / (rPrime * cosTheta - r * sinTheta) return graphSlope } function tangentVectorAt(theta, output = Vector2()) { const r = self.sample('theta', theta), rPrime = self.sampleSlope('theta', theta), sinTheta = Math.sin(theta), cosTheta = Math.cos(theta) output.set( -r * sinTheta + cosTheta * rPrime, cosTheta * r + sinTheta * rPrime, ) output.negate() } function normalVectorAt(theta, output = Vector2()) { tangentVectorAt(theta, output) output.orthogonalize() } // Velocity vector w.r.t. time function velocityVectorAt(theta, output = Vector2()) { const rPrimeWrtTime = self.sampleSlope('t', globalScope.t, 'theta', theta), sinTheta = Math.sin(theta), cosTheta = Math.cos(theta) output.set(rPrimeWrtTime * cosTheta, rPrimeWrtTime * sinTheta) } resample(true) self.mix(sampler) return self.mix({ tick, draw, resize, assignColors, tVariableChanged, resample, bounds, startRunning, stopRunning, pointAtTheta, thetaOfClosestSurfacePoint, velocityVectorAt, normalVectorAt, tangentVectorAt, sampleSlopeFromTheta, set polar(v) { polar = v }, get isPolar() { return polar }, get label() { return polar ? 'R' : 'Y' }, get minSample() { return minSample }, get maxSample() { return maxSample }, get samples() { return samples }, get expression() { return sampler.expression }, set expression(v) { sampler.expression = v resample(true) }, get strokeWidth() { return strokeWidth }, set strokeWidth(v) { strokeWidth = v }, get strokeColor() { return strokeColor }, set strokeColor(v) { strokeColor = v }, get fillColor() { return fillColor }, set fillColor(v) { fillColor = v }, get dashSettings() { return dashSettings }, set dashSettings(v) { dashSettings = v }, get dashOffset() { return dashOffset }, set dashOffset(v) { dashOffset = v }, get dashed() { return dashed }, set dashed(v) { dashed = v }, set terrainLayers(v) { terrainLayers = v generateTerrainParameters() }, get sampleCount() { return sampleCount }, set sampleCount(v) { sampleCount = v samples = sampler.generateSampleArray(sampleCount) interpolationSampler = new InterFrameSampler({ sampler, sampleCount, refreshPeriodT, }) }, }) } FILE: Types_Entities_Gridlines.js // TODO: Refactor Gridlines, clean up interface function Gridlines(spec) { const { self, screen, parent } = Entity(spec, 'Gridlines') const transform = Transform() let { camera } = spec const ctx = screen.ctx let x = 0 let y = 0 const origin = Vector2() const screenOrigin = Vector2() function updatePosition(newX, newY) { x = newX y = newY } function tick() {} function drawLocalAxesTicks() { const xTicks = Math.ceil(camera.upperRight.x - camera.lowerLeft.x) + 1 const yTicks = Math.ceil(camera.upperRight.y - camera.lowerLeft.y) + 1 ctx.beginPath() // Draw right x ticks for (let i = 0; i < xTicks; i++) { ctx.moveTo(i, 0) ctx.lineTo(i, -0.2) } // Draw left x ticks for (let i = 0; i > -xTicks; i--) { ctx.moveTo(i, 0) ctx.lineTo(i, 0.2) } // Draw top y ticks for (let i = 0; i < yTicks; i++) { ctx.moveTo(0, -i) ctx.lineTo(0.2, -i) } // Draw bottom y ticks for (let i = 0; i > -yTicks; i--) { ctx.moveTo(0, -i) ctx.lineTo(-0.2, -i) } ctx.strokeStyle = '#DDD' ctx.lineWidth = camera.screenToWorldScalar(1.4) ctx.stroke() } function drawLocal() { const xTicks = Math.ceil(camera.upperRight.x - camera.lowerLeft.x) + 1 const yTicks = Math.ceil(camera.upperRight.y - camera.lowerLeft.y) + 1 ctx.beginPath() // Draw vertical lines for (let i = 0; i < xTicks; i++) { ctx.moveTo(i, -camera.lowerLeft.y) ctx.lineTo(i, -camera.upperRight.y) ctx.moveTo(-i, -camera.lowerLeft.y) ctx.lineTo(-i, -camera.upperRight.y) } // Draw horizontal lines for (let i = 0; i < yTicks; i++) { ctx.moveTo(camera.lowerLeft.x, -i) ctx.lineTo(camera.upperRight.x, -i) ctx.moveTo(camera.lowerLeft.x, i) ctx.lineTo(camera.upperRight.x, i) } ctx.strokeStyle = 'rgba(170, 170, 170, 0.5)' ctx.lineWidth = camera.screenToWorldScalar(1) ctx.stroke() if (!parent.isEditor) { ctx.beginPath() ctx.moveTo(camera.lowerLeft.x, -y) ctx.lineTo(camera.upperRight.x, -y) ctx.moveTo(x, -camera.lowerLeft.y) ctx.lineTo(x, -camera.upperRight.y) ctx.strokeStyle = 'rgba(136, 187, 221, 1)' ctx.lineWidth = camera.screenToWorldScalar(1) ctx.stroke() } } function draw() { camera.drawThrough(ctx, drawLocal, transform) // Draw Axis Lines When Active camera.worldToScreen(origin, screenOrigin) ctx.beginPath() ctx.moveTo(0, screenOrigin.y) ctx.lineTo(screen.width, screenOrigin.y) ctx.moveTo(screenOrigin.x, 0) ctx.lineTo(screenOrigin.x, screen.height) ctx.strokeStyle = '#DDD' ctx.lineWidth = 2.8 ctx.stroke() camera.drawThrough(ctx, drawLocalAxesTicks, transform) } return self.mix({ transform, tick, draw, updatePosition, }) } FILE: Types_Entities_HintGraph.js function HintGraph(spec) { const { self, camera, ui } = Entity(spec, 'Hint Graph') const { slider, globalScope, drawOrder } = spec const { bounds, expression: expressionForm } = slider function createExpression(n) { return expressionForm .replaceAll('$', n.toFixed(2)) .replaceAll('+ -', '-') .replaceAll('- +', '-') .replaceAll('+-', '-') .replaceAll('-+', '-') } const dottedGraph = Graph({ camera, globalScope, expression: createExpression(0), parent: self, strokeWidth: 0.1, strokeColor: 'rgb(0,255,0,0)', dashed: true, scaleStroke: true, dashSettings: [0.5, 0.5], fill: false, drawOrder, }) let expression = createExpression(bounds[2]) function setSliderExpression(text) { dottedGraph.expression = mathquillToMathJS(text) ui.dottedMathFieldStatic.latex(`Y=${text}`) } ui.dottedSlider.oninput = (e) => { let val = ui.dottedSlider.value / 100 expression = createExpression(val * (bounds[1] - bounds[0]) + bounds[0]) setSliderExpression(expression) } ui.dottedSlider.value = 100 * ((bounds[2] - bounds[0]) / (bounds[1] - bounds[0])) setSliderExpression(expression) function setVisible(visible) { ui.dottedMathContainer.style.display = visible ? 'flex' : 'none' // ui.dottedMathFieldStatic.latex(visible ? `\\text{Y}=${expression}` : '') // ui.dottedSlider.hidden = !visible } setVisible(true) function displayDottedGraph() { dottedGraph.strokeColor = 'rgba(0,255,0)' } function destroy() { setVisible(false) dottedGraph.strokeColor = 'rgba(0,255,0,0)' ui.dottedMathField.style.display = 'none' ui.dottedSlider.hidden = true ui.dottedHintButton.style.display = 'block' } function onToggleMap(mapEnabled) { setVisible(!mapEnabled) } return self.mix({ displayDottedGraph, destroy, setVisible, onToggleMap, }) } FILE: Types_Entities_LavaMonster.js function LavaMonster(spec) { const { self, camera, screen, assets } = Entity(spec, 'LavaMonster') const { getSledderPosition } = spec const transform = Transform(spec, self) const roar = _.get(assets, 'sounds.lava_monster_roar') transform.position.x = 150 // Lava x transform.position.y = 0 const sprite = Sprite({ parent: self, drawOrder: self.drawOrder, asset: 'images.lavamonster', camera, assets, screen, size: 15, }) const jaw = Sprite({ parent: self, drawOrder: self.drawOrder, asset: 'images.lavamonster_jaw', camera, assets, screen, x: 2.5, y: 0, size: 7, }) let roarPlayed = false function tick() { transform.position.x = 0 transform.position.y = 110 const x = getSledderPosition() const tangentAngle = Math.atan(-(x - 250) / 150) transform.rotation = tangentAngle transform.position.y = 48 - ((x - 200) / 6) ** 2 transform.position.x = (x - 8) * 1.01 const t = 1 / (1 + Math.exp(-0.3 * (x - 195))) jaw.transform.y = -10 + 4 * t jaw.transform.rotation = -10 + t / 2 if (x > 170 && !roarPlayed) { roar.play() roarPlayed = true } } return self.mix({ transform, tick, roar, }) } FILE: Types_Entities_LevelBubble.js let levelBubblesDrawn = 0 function LevelBubble(spec) { const { ui, self, parent, screen, camera, assets } = Entity( spec, 'LevelBubble ' + spec.levelDatum.nick, ) const { setLevel, levelDatum, getEditing, tickDelta, getBubbleByNick, getShowAll, quad, panCamera, } = spec const { nick, requirements, radius = 3 } = levelDatum const dependencies = [] const transform = Transform(levelDatum) const shape = Circle({ transform, radius, }) const clickable = Clickable({ entity: self, shape, transform, camera, }) const arrows = [] let rendered = false let completed = false let hilighted = false let playable = false let unlocked = false let visible = false let frameCounter = 0 let bubbletRunning = false let bubbletRunTime = 0 const bubbletGlobalScope = { get t() { return bubbletRunTime }, dt: tickDelta, get running() { return bubbletRunning }, } const bubbletCanvas = document.createElement('canvas') const previewCanvas = document.createElement('canvas') let bubbletPixels = 512 bubbletCanvas.width = bubbletPixels bubbletCanvas.height = bubbletPixels previewCanvas.width = bubbletPixels * 2 previewCanvas.height = bubbletPixels * 2 ui.bubblets.appendChild(bubbletCanvas) const bubbletScreen = Screen({ canvas: bubbletCanvas, element: bubbletCanvas, }) const bubbletCamera = Camera({ screen: bubbletScreen, }) // levelDatum.axesEnabled = false let bitmap = null function resizeBitmap() { let size = Math.round(camera.worldToScreenScalar(radius * 2)) if (bitmap) { bitmap.close() bitmap = null } createImageBitmap(bubbletCanvas, { resizeWidth: size, resizeHeight: size, }).then((_bitmap) => { bitmap = _bitmap }) } function resize() { resizeBitmap() } const ctx = screen.ctx function awake() { linkRequirements() // Offset position by first requirement's position const p = Vector2() if (requirements.length > 0) { p.set(requirements[0].transform.position) } transform.position.add(p) } function start() { // Create an arrow to each dependency for (bubble of requirements) { let arrow = Arrow({ truncate: [radius + 0.9, radius + 0.9], point0: bubble.transform.position, point1: transform.position, drawOrder: LAYERS.arrows, parent: self, }) arrow.fromBubble = bubble arrow.toBubble = self arrows.push(arrow) } refreshPlayable() } let tmp = Vector2() function localToScreen(localX, localY) { tmp.set(localX, localY) camera.worldToScreen(tmp, tmp, transform) return [tmp.x, tmp.y] } function render() { const bubbletLevel = Level({ datum: levelDatum, axesEnabled: false, screen: bubbletScreen, camera: bubbletCamera, globalScope: bubbletGlobalScope, parent: self, useDragCamera: false, isBubbleLevel: true, drawOrder: LAYERS.levelBubbles, tryToLoadAssets: false, }) bubbletLevel.awake() bubbletLevel.start() bubbletLevel.sendEvent('tick') bubbletLevel.sendEvent('draw') bubbletLevel.destroy() rendered = true resizeBitmap() const previewCtx = previewCanvas.getContext('2d') let previewWidth = previewCanvas.width / 2, previewHeight = previewCanvas.height / 2 const previewTransform = Transform({ x: previewWidth, y: previewHeight, scale: Math.min(bubbletCanvas.width / 2, bubbletCanvas.height / 2), }) // camera.drawThrough(previewCtx, drawLocalWithTransform, previewTransform) previewTransform.invertCanvas(previewCtx) drawLocalWithTransform(previewCtx, 2) } // Old drawLocal which uses passed transform // TODO: Clean up function drawLocalWithTransform(ctx, radius) { // Opacity for local draw is always 1, which is then // changed when drawing the offscreen canvas to the screen ctx.beginPath() let strokeWidth = 0.12 let outlineRadius = radius - strokeWidth // if (playable) { // if (hilighted) strokeWidth = 0.4 // else if (playable) strokeWidth = 0.2 // if (clickable.hovering) strokeWidth *= 2 // } const cutsceneFrameSides = 8 if (levelDatum.runAsCutscene) { ctx.rotate(((180 / cutsceneFrameSides) * Math.PI) / 180) ctx.beginPath() ctx.moveTo(outlineRadius, 0) for (var i = 1; i <= cutsceneFrameSides; i += 1) { ctx.lineTo( outlineRadius * Math.cos((i * 2 * Math.PI) / cutsceneFrameSides), outlineRadius * Math.sin((i * 2 * Math.PI) / cutsceneFrameSides), ) } } else { ctx.arc(0, 0, outlineRadius, 0, Math.PI * 2) } ctx.lineWidth = strokeWidth ctx.fillStyle = '#fff' ctx.strokeStyle = '#444' // Draw as unhighlighted for buffer ctx.save() ctx.fill() ctx.clip() if (levelDatum.runAsCutscene) { ctx.rotate((-(180 / cutsceneFrameSides) * Math.PI) / 180) ctx.drawImage(bubbletCanvas, -radius, -radius, radius * 2, radius * 2) } else { ctx.drawImage(bubbletCanvas, -radius, -radius, radius * 2, radius * 2) } ctx.fillStyle = '#333' ctx.textAlign = 'center' ctx.textBaseline = 'hanging' ctx.font = '1px Roboto Mono' ctx.scale(0.8, 0.8) // ctx.fillText(levelDatum.name, 0, radius) ctx.restore() // ctx.globalAlpha = 1 ctx.lineCap = 'butt' ctx.miterLimit = 20 ctx.lineJoin = 'miter' ctx.beginPath() if (levelDatum.runAsCutscene) { ctx.beginPath() ctx.moveTo(outlineRadius + strokeWidth / 2 - 0.02, 0) for (var i = 1; i < cutsceneFrameSides; i += 1) { ctx.lineTo( (outlineRadius + strokeWidth / 2 - 0.02) * Math.cos((i * 2 * Math.PI) / cutsceneFrameSides), (outlineRadius + strokeWidth / 2 - 0.02) * Math.sin((i * 2 * Math.PI) / cutsceneFrameSides), ) } ctx.closePath() } else { ctx.arc(0, 0, outlineRadius + strokeWidth / 2 - 0.02, 0, Math.PI * 2) } ctx.stroke() } function drawLocal() { const opacity = visible ? (playable ? 1 : 0.5) : 0 ctx.globalAlpha = opacity ctx.drawImage(previewCanvas, -radius, -radius, radius * 2, radius * 2) // if (playable) { // if (hilighted) strokeWidth = 0.4 // else if (playable) strokeWidth = 0.2 // if (clickable.hovering) strokeWidth *= 2 // } if (playable && (hilighted || clickable.hovering)) { let strokeWidth = 0.24 if (clickable.hovering) strokeWidth *= 1.5 let outlineRadius = radius - 0.2 ctx.lineWidth = strokeWidth ctx.fillStyle = '#fff' ctx.strokeStyle = hilighted ? '#f88' : '#444' const cutsceneFrameSides = 8 ctx.beginPath() if (levelDatum.runAsCutscene) { ctx.beginPath() ctx.rotate(((180 / cutsceneFrameSides) * Math.PI) / 180) ctx.moveTo(outlineRadius + strokeWidth / 2 - 0.02, 0) for (var i = 1; i < cutsceneFrameSides; i += 1) { ctx.lineTo( (outlineRadius + strokeWidth / 2 - 0.02) * Math.cos((i * 2 * Math.PI) / cutsceneFrameSides), (outlineRadius + strokeWidth / 2 - 0.02) * Math.sin((i * 2 * Math.PI) / cutsceneFrameSides), ) } ctx.closePath() } else { ctx.arc(0, 0, outlineRadius + strokeWidth / 2 - 0.02, 0, Math.PI * 2) } ctx.stroke() } } function refreshPlayable() { unlocked = _.every(requirements, 'completed') if (unlocked) { playable = true hilighted = !completed } else { playable = getShowAll() hilighted = false } visible = playable || _.some(requirements, (v) => v.playable) } function refreshArrows() { _.each(arrows, (v) => { v.opacity = visible ? playable ? 1 : 0.5 : v.fromBubble.visible ? 0.5 : 0 v.dashed = !v.toBubble.unlocked v.fadeIn = visible && !v.fromBubble.visible v.fadeOut = !visible && v.fromBubble.visible }) } function intersectsScreen() { let center = transform.position let left = camera.lowerLeft.x - radius let right = camera.upperRight.x + radius let top = camera.upperRight.y + radius let bottom = camera.lowerLeft.y - radius return ( center.x > left && center.x < right && center.y > bottom && center.y < top ) } function draw() { if (!visible) return if (!intersectsScreen()) return levelBubblesDrawn++ ctx.save() // Use drawLocal directly instead of passing // to camera to use rounded image coordinates camera.drawThrough(ctx, drawLocal, transform) ctx.restore() } function complete() { if (completed) return completed = true refreshPlayable() _.invokeEach(dependencies, 'refreshPlayable') } function linkRequirements() { for (let i = 0; i < requirements.length; i++) { let requiredBubble = getBubbleByNick(requirements[i]) requiredBubble.dependencies.push(self) requirements[i] = requiredBubble } } function mouseDown(point) { // If not playable, then delegate to parent if (!playable) { parent.updatePanVelocity(point, clickable.holding) } } // TODO: Doesn't work if mouse is held on LevelBubble function hoverMove(point) { // If not playable, then delegate to parent if (!playable && clickable.holding) { parent.updatePanVelocity(point, clickable.holding) } } function click(point) { if (!playable) return ui.veil.setAttribute('hide', false) assets.sounds.enter_level.play() assets.sounds.map_zoom_in.play() completeAllRequirements() panCamera(transform.position, () => { setLevel(levelDatum.nick) ui.veil.setAttribute('hide', true) }) } function completeAllRequirements() { for (requirement of requirements) { requirement.complete() requirement.completeAllRequirements() } refreshPlayable() } return self.mix({ transform, clickable, start, awake, resize, draw, render, mouseDown, hoverMove, click, dependencies, linkRequirements, refreshPlayable, refreshArrows, get nick() { return nick }, get rendered() { return rendered }, get completed() { return completed }, get hilighted() { return hilighted }, get playable() { return playable }, get visible() { return visible }, get unlocked() { return unlocked }, complete, completeAllRequirements, }) } FILE: Types_Entities_Level_Cutscene.js // TODO: Finish moving in all the cutscene abstractions function Cutscene(spec) { const { self, ui, levelCompleted, datum, sledders, walkers } = Level(spec) const base = _.mix(self) const showSkipCutsceneButtonAnimation = { keyframes: [ { transform: 'translateX(calc(-100% - 20px))' }, { transform: 'translateX(0px)' }, // { opacity: '0' }, // { opacity: '1' }, ], options: { duration: 1700, easing: 'ease-out', fill: 'forwards', }, } const hideSkipCutsceneButtonAnimation = { keyframes: [ { transform: 'translateX(0px)' }, { transform: 'translateX(calc(-100% - 20px))' }, // { opacity: '0' }, // { opacity: '1' }, ], options: { duration: 1700, easing: 'ease-out', }, } let showingSkipCutsceneButton = false function initMathEditor() { /* Hide math editor by default */ ui.expressionEnvelope.classList.add('hidden') ui.expressionEnvelope.setAttribute('disabled', true) } function destroy() { base.destroy() // TODO: Refactor running world._stopRunning() // Hide "Skip Cutscene" button ui.skipCutsceneButton.setAttribute('hide', true) } function awake() { base.awake() ui.skipCutsceneButton.addEventListener('click', levelCompleted) // All cutscenes are "running" by default, and can't be stopped. // TODO: Refactor how running works world._startRunning(false, false) // Disable stop button for cutscenes // TODO: Refactor how starting/stopping work ui.stopButton.classList.add('disabled') } function shouldShowSkipCutsceneButton() { return globalScope.t < 10 } function getCutsceneX() { return ( ( sledders.find((sledder) => sledder.active) ?? walkers.find((walker) => walker.active) )?.transform.x ?? -Infinity ) } function getTime() { return self.getCutsceneX() } function tick() { base.tick() // if (victoryX != null && self.getCutsceneX() > victoryX) { // completed = true // levelCompleted(true) // } if (self.shouldShowSkipCutsceneButton() && !showingSkipCutsceneButton) { showingSkipCutsceneButton = true ui.skipCutsceneButton.setAttribute('hide', false) ui.skipCutsceneButton.animate( showSkipCutsceneButtonAnimation.keyframes, showSkipCutsceneButtonAnimation.options, ) } else if ( !self.shouldShowSkipCutsceneButton() && showingSkipCutsceneButton ) { showingSkipCutsceneButton = false const hideAnimation = ui.skipCutsceneButton.animate( hideSkipCutsceneButtonAnimation.keyframes, hideSkipCutsceneButtonAnimation.options, ) hideAnimation.onfinish = () => ui.skipCutsceneButton.setAttribute('hide', true) } } function skipCutscene() { levelCompleted() } function onLevelFadeOut() { ui.skipCutsceneButton.setAttribute('hide', true) } function onLevelFadeIn() { ui.skipCutsceneButton.setAttribute('hide', !showingSkipCutsceneButton) } return self.mix({ awake, destroy, tick, getCutsceneX, getTime, get isCutscene() { // Overriding falsey (null) by default in Level.js return true }, onLevelFadeIn, onLevelFadeOut, initMathEditor, skipCutscene, shouldShowSkipCutsceneButton, }) } FILE: Types_Entities_Level_Cutscenes_ConstantLake.js function ConstantLake(spec) { const vectorFieldStartX = 13.5 const vectorFieldEndX = 17.5 const defaultVectorField = '\\frac{(\\sin (x)-(y-2)\\cdot i)\\cdot i}{2}+\\frac{x}{4}+\\frac{y\\cdot i}{5}' const { datum, quads, savedLatex } = spec const { screen, self, assets, ui, world, walkers } = Cutscene({ ...spec, useDarkenBuffer: true, startingExpression: datum.defaultExpression, }) const base = _.mix(self) const startingVectorFieldExpression = savedLatex ?? defaultVectorField let vectorExpression = startingVectorFieldExpression let vectorEditorIsActive = false const showUIAnimation = { keyframes: [ { transform: 'translateY(calc(100% + 20px))', opacity: '0' }, { transform: 'translateY(0px)', opacity: '1' }, // { opacity: '0' }, // { opacity: '1' }, ], options: { duration: 1700, easing: 'ease-out', fill: 'forwards', }, } const hideUIAnimation = { keyframes: [ { transform: 'translateY(0px)', opacity: '1' }, { transform: 'translateY(calc(100% + 20px))', opacity: '0' }, ], options: { duration: 1700, easing: 'ease-out', }, } const shader = ConstantLakeShader({ parent: self, screen, assets, quad: quads.sunset, drawOrder: LAYERS.sky, getWalkerPosition, defaultExpression: '', }) // Hide math editor (temporarily) in constructor to fix jitter ui.expressionEnvelope.setAttribute('hide', true) function getWalkerPosition() { return walkers[0]?.transform.x ?? 0 } function setWalkerPosition(x) { if (walkers[0]) walkers[0].transform.x = x } function awakeWithAssetsAndDatum() { base.awakeWithAssetsAndDatum() if (savedLatex != defaultVectorField) { // Bit before end of level but when stars are fully visible // For some reason we can't seem to identify when the latex has changed or not. I have not deeply investigated this but I am disabling for now. // setWalkerPosition(23) } // TODO: Rework running, starting/stopping (should be managed by Level?) world._startRunning(false, false, false) // Hide math field by default ui.expressionEnvelope.classList.add('hidden') ui.stopButton.classList.add('disabled') } function shouldShowSkipCutsceneButton() { return !savedLatex && getWalkerPosition() < 6 } function getDefaultExpression() { // Restart w/ default vector field expression rather than graph return defaultVectorField } function setGraphExpression(text, latex) { vectorExpression = latex // Update math editor ui.mathFieldStatic.latex(latex) // Save new vector field to URL + LocalStorage self.save() // Update background shader shader.setVectorFieldExpression(text) } function getTime() { return getWalkerPosition() } function updateMathEditor(x) { if (x > vectorFieldEndX && !vectorEditorIsActive) { // If walker is past a certain point and vector editor // hasn't been shown yet, show editor vectorEditorIsActive = true ui.resetButton.setAttribute('hide', false) ui.expressionEnvelope.classList.remove('hidden') ui.expressionEnvelope.animate( showUIAnimation.keyframes, showUIAnimation.options, ) ui.resetButton.animate(showUIAnimation.keyframes, showUIAnimation.options) } else if (x < vectorFieldStartX && vectorEditorIsActive) { // Otherwise, if walker is behind a different point and vector // editor is currently shown, hide editor vectorEditorIsActive = false const resetButtonAnimation = ui.resetButton.animate( hideUIAnimation.keyframes, hideUIAnimation.options, ) const expressionEnvelopeAnimation = ui.expressionEnvelope.animate( hideUIAnimation.keyframes, hideUIAnimation.options, ) resetButtonAnimation.onfinish = () => ui.resetButton.setAttribute('hide', true) expressionEnvelopeAnimation.onfinish = () => ui.expressionEnvelope.classList.add('hidden') } } function tick() { base.tick() const x = getWalkerPosition() updateMathEditor(x) /* Darken scene according to player distance */ const darkenOpacity = Math.min(0.9, Math.pow(Math.max(0, x) / 20, 2)) self.darkenBuffer.parameters.darkenOpacity = darkenOpacity // const walkerDarkenOpacity = Math.pow(darkenOpacity, 5) // for (const walker of walkers) { // walker.darkModeOpacity = walkerDarkenOpacity // for (const w of walker.walkers) { // if (w.hasDarkMode) w.darkModeOpacity = walkerDarkenOpacity // } // } } function initMathEditor() { // Enable math editor ui.expressionEnvelope.setAttribute('disabled', false) // But change it to be for a vector field (V) // and use different default expression ui.mathFieldLabel.innerText = 'V=' ui.mathField.latex(startingVectorFieldExpression) ui.mathFieldStatic.latex(startingVectorFieldExpression) } function serialize() { return { // Version, nick, completed, etc. ...base.serialize(), savedLatex: vectorExpression, } } return self.mix({ tick, awakeWithAssetsAndDatum, setGraphExpression, getDefaultExpression, getTime, initMathEditor, serialize, shouldShowSkipCutsceneButton, // Used by Walkers get darkenable() { // Use getter so constant return true }, }) } FILE: Types_Entities_Level_Cutscenes_Credits.js function Credits(spec) { const { self } = Cutscene(spec) return self.mix({}) } FILE: Types_Entities_Level_Cutscenes_Desert.js function Desert(spec) { const { self } = Cutscene(spec) // Currently, all "special logic" just sprites/dialogue return self } FILE: Types_Entities_Level_Cutscenes_Volcano.js function Volcano(spec) { const { self, graph, sledders, camera, datum, isBubbleLevel, darkenBufferOrScreen, screen, } = Cutscene(spec) const base = _.mix(self) // Same as Constant Lake const defaultVectorField = '\\frac{(\\sin (x)-(y-2)\\cdot i)\\cdot i}{2}+\\frac{x}{4}+\\frac{y\\cdot i}{5}' let lava if (!isBubbleLevel) { // Create lava lava = Water({ parent: self, camera, lava: true, screen: darkenBufferOrScreen, globalScope, drawOrder: LAYERS.backSprites, ...datum.lava, }) } const volcanoSunset = VolcanoSunsetShader({ parent: self, screen, drawOrder: LAYERS.sky, getSledderPosition, defaultExpression: mathquillToMathJS(defaultVectorField), }) function awakeWithAssetsAndDatum() { base.awakeWithAssetsAndDatum() LavaMonster({ parent: self, screen, drawOrder: LAYERS.backSprites - 1, camera, globalScope, getSledderPosition, }) } function getSledderPosition() { return sledders[0]?.transform.x ?? 0 } function getSledderVelocity() { return sledders[0]?.velocity ?? 20 } function tick() { base.tick() const x = getSledderPosition() const sunsetTime = Math.exp(-(((x - 205) / 100) ** 2)) globalScope.timescale = 1 - sunsetTime * 0.7 camera.shake = sunsetTime > 0.1 ? sunsetTime * 0.3 : 0 const vel = getSledderVelocity() const motionBlur = Math.min((vel / 40) * 4, 10) // console.log(`Volcano Sunset Name:`, volcanoSunset.name) // console.log(`Volcano Sky Name:`, sky.name) graph.blur = motionBlur lava.blur = motionBlur volcanoSunset.blur = motionBlur } return self.mix({ tick, awakeWithAssetsAndDatum, }) } FILE: Types_Entities_Level_Editor_LevelEditor.js // TODO: Figure out how Editor should work? // (Maybe doesn't need its own class?) function LevelEditor(spec) { const { self, ui, goals, sledders, gridlines, coordinateBox, graph, sky, datum, } = Level(spec) const base = _.mix(self) let biomeSetting = datum.biome ?? 'westernSlopes' /* Editor flow: Open level editor (map, URL (edit: true), sinerider.com/?edit) - Level -> LevelEditor() -> sendEvent('editorEnabled') - gridlines always enabled -> override selectScreenCoordinates() -> if selected, no coordinate box - Share button Share -> open dialog w/ serialized JSON with edit: false, name: "Custom" - Open as normal, uneditable level */ let editingPath = false let editing let selection = null let selectionIsDeletable = false const goalLimit = 20 //determines the maximum number of goals allowed in the editor const { inputs, labels } = ui.editorInspector function goalDeleted(goal) { goals.splice(goals.indexOf(goal), 1) updateGoalLimitVisual() } function serialize(nick = null) { const json = { // Version, etc. ...base.serialize(), defaultExpression: self.currentLatex, sledders: sledders.map((v) => ({ asset: v.asset, x: v.x, y: v.y })), goals: goals.map((g) => { const goalJson = { type: g.type, x: g.transform.x, y: g.transform.y, } if (g.order) goalJson.order = g.order if (g.pathExpression) { goalJson.expression = g.pathExpression goalJson.expressionLatex = g.pathExpressionLatex goalJson.pathX = g.pathEnd.x - g.pathStart.x } return goalJson }), biome: biomeSetting, } if (nick) json.nick = nick return json } function awake() { // ui.nextButton.setAttribute('hide', true) enableEditing() ui.editorLevelConfigurationBiomeSelect.value = biomeSetting ui.editorLevelConfigurationSledderSelect.value = datum.sledders[0].asset self.save() } function destroy() { // ui.nextButton.setAttribute('hide', false) hide() } function hide() { ui.editorSpawner.panel.setAttribute('hide', true) ui.editorInspector.panel.setAttribute('hide', true) } function select( newSelection, attributes = ['start', 'end', 'order', 'timer', 'x', 'y'], newSelectionIsDeletable = true, ) { selection = newSelection selectionIsDeletable = newSelectionIsDeletable showInspectorPanel() const hideOrder = !attributes.includes('order') inputs.order.setAttribute('hide', hideOrder) labels.order.setAttribute('hide', hideOrder) const hideTimer = !attributes.includes('timer') inputs.timer.setAttribute('hide', hideTimer) labels.timer.setAttribute('hide', hideTimer) const hideX = !attributes.includes('x') const hideY = !attributes.includes('y') inputs.x.setAttribute('hide', hideX) inputs.y.setAttribute('hide', hideY) labels.position.setAttribute('hide', hideX && hideY) const hideStart = !attributes.includes('start') labels.start.setAttribute('hide', hideStart) inputs.start.setAttribute('hide', hideStart) const hideEnd = !attributes.includes('end') labels.end.setAttribute('hide', hideEnd) inputs.end.setAttribute('hide', hideEnd) ui.editorInspector.deleteSelectionButton.setAttribute( 'hide', !selectionIsDeletable, ) update() } function deselect() { self.save() selection = null selectionIsDeletable = false if (!world.navigating) showSpawnerPanel() } /* UI Events */ function showSpawnerPanel() { ui.editorSpawner.panel.setAttribute('hide', false) ui.editorInspector.panel.setAttribute('hide', true) ui.editorInspector.panel.classList.remove('editor-sliding-up') ui.editorSpawner.panel.classList.remove('editor-sliding-up') } function showInspectorPanel() { ui.editorSpawner.panel.setAttribute('hide', true) ui.editorInspector.panel.setAttribute('hide', false) ui.editorInspector.panel.classList.remove('editor-sliding-up') ui.editorSpawner.panel.classList.remove('editor-sliding-up') } function stringifyInputNumber(number) { const string = number.toString() const stringFixed = number.toFixed(2) return string.length < stringFixed.length ? string : stringFixed } function updateGoalLimitVisual() { let goalLimitText = document.getElementById('goal-limit-visual') goalLimitText.innerText = `${self.goals.length}/${goalLimit} Goals` goalLimitText.style.color = self.goals.length >= goalLimit ? 'red' : 'black' } function update(save = true) { // Update UI inputs.order.value = selection.order ?? '' inputs.x.value = selection.x ? selection.x.toFixed(2) : '' inputs.y.value = selection.y ? selection.y.toFixed(2) : '' inputs.timer.value = selection.timer ?? '' if (selection.starting) inputs.start.value = stringifyInputNumber(selection.starting) if (selection.ending) inputs.end.value = stringifyInputNumber(selection.ending) // Save to URL if (save) self.save() } //Allows a goal to be added if the goal limit has not been reached //More conditions can be added function tryAddGoal() { return self.goals.length < goalLimit } function onAddFixedClicked() { if (!tryAddGoal()) return self.addGoal({ type: 'fixed' }) updateGoalLimitVisual() } function onAddDynamicClicked() { if (!tryAddGoal()) return self.addGoal({ type: 'dynamic' }) updateGoalLimitVisual() } function onAddPathClicked() { if (!tryAddGoal()) return self.addGoal({ type: 'path' }) updateGoalLimitVisual() } function deleteSelection() { if (selection && selectionIsDeletable) { selection.destroy() deselect() } } // Handle all editor inspector inputs here // for ease-of-use and error handling function onEditorInput(inputName, event) { switch (inputName) { case 'order': { let data = event.data ? event.data.toUpperCase() : null if (!/[a-z]/i.test(data)) { event.preventDefault() inputs.order.value = '' return } inputs.order.value = data if (selection?.setOrder) selection.setOrder(data) return } case 'timer': { let value try { value = Number(inputs.timer.value) } catch (err) { return } if (selection?.setTimer) selection.setTimer(value) return } case 'x': { let value try { value = Number(inputs.x.value) } catch (err) { return } if (selection?.setX) selection.setX(value) return } case 'y': { let value try { value = Number(inputs.y.value) } catch (err) { return } if (selection?.setY) selection?.setY(value) return } case 'start': { let value try { value = Number(inputs.start.value) } catch (err) { return } selection.setStart(value) return } case 'end': { let value try { value = Number(inputs.end.value) } catch (err) { return } selection.setEnd(value) return } } throw `Unhandled editor input event: '${inputName}'` } function keydown(key) { if ( document.activeElement == document.body && // Ignore if in text field (key === 'Backspace' || key === 'Delete') ) { deleteSelection() } } function enableEditing() { editing = true showSpawnerPanel() gridlines.active = true } function disableEditing() { gridlines.active = false editing = false hide() } function startRunning() { disableEditing() } function stopRunning() { enableEditing() } // TODO: Change these so // bar fading in/out isn't visible function onLevelFadeOut() { console.log('fading out') ui.editorInspector.panel.classList.add('editor-sliding-up') ui.editorSpawner.panel.classList.add('editor-sliding-up') } // Overload mouse gridlines behavior function onMouseUp() {} function onMouseDown() {} function onShareButtonClicked() { const serialized = serialize('CUSTOM_LEVEL') const url = location.origin + '?' + LZString.compressToBase64(JSON.stringify(serialized)) ui.puzzleLink.value = url showDialog(ui.editorSharingLinkDialog) } // TODO: Finish polar coordinates // function setPolar(polar) { // graph.polar = polar // ui.mathFieldLabel.innerText = `${graph.label}=` // self.sendEvent('reset') // } function selectBiome(biomeKey) { biomeSetting = biomeKey const biome = BIOMES[biomeKey] base.setBiome(biome) self.save() } function setSledderImage(sledderImagePath) { const sledder = sledders[0] const x = sledder.x, y = sledder.y sledders.splice(sledders.indexOf(sledder), 1) sledder.destroy() base.addSledder({ x, y, asset: sledderImagePath, }) self.save() } function setStart() {} function setEnd() {} return self.mix({ awake, goalDeleted, serialize, // setPolar, selectBiome, setSledderImage, keydown, onShareButtonClicked, onLevelFadeOut, startRunning, stopRunning, onMouseUp, onMouseDown, // TODO: Remove need for this // (effectively global variable) get editing() { return editing }, get isEditor() { return true }, awake, destroy, get editingPath() { return editingPath }, set editingPath(v) { editingPath = v }, deselect, select, onAddDynamicClicked, onAddFixedClicked, onAddPathClicked, onEditorInput, deleteSelection, update, }) } FILE: Types_Entities_Level_Level.js /** * Base class representing a level scene with * a graph, camera/director(s) and walkers/sledders/sprites/text. * Can be started, stopped, reset, serialized and takes in a levelCompleted * callback which is invoked whenever the level's completion condition is met. */ function Level(spec) { const { self, assets, screen, ui, world } = Entity(spec, spec.datum.nick) const { globalScope, isBubbleLevel, datum, storage, urlData, savedLatex, playBackgroundMusic, quads, darkenable, tryToLoadAssets = true, } = spec preprocessDatum(datum) let { colors = Colors.biomes.alps, defaultExpression, hint = '', openMusic, runMusic, flashMathField = false, flashRunButton = false, runAsCutscene = false, camera: cameraSpec = {}, graphType = 'cartesian', minTheta = 0, maxTheta = TAU, invertGravity = false, } = datum let completed = false let hasBeenCompleted = spec.hasBeenCompleted ?? false const levelCompleted = (soft = false) => { hasBeenCompleted = true spec.levelCompleted(soft) } const sledders = [] const walkers = [] const goals = [] const texts = [] const sprites = [] const speech = [] const directors = [] const tips = [] const sounds = [] let lowestOrder = 'A' let highestOrder = 'A' let sky, clouds, snow let usingTInExpression = false if (!isBubbleLevel) { if (flashMathField) ui.expressionEnvelope.classList.add('flash-shadow') else ui.expressionEnvelope.classList.remove('flash-shadow') if (flashRunButton) ui.runButton.classList.add('flash-shadow') else ui.runButton.classList.remove('flash-shadow') } let currentLatex = defaultExpression const trackedEntities = [speech, sledders, walkers, goals] // TODO: Fix hint text. Mathquill broke it // ui.mathField.setAttribute('placeholder', hint) assets.sounds.level_success.volume(0.5) assets.sounds.goal_success.volume(0.5) assets.sounds.goal_fail.volume(0.5) openMusic = _.get(assets, openMusic, null) runMusic = _.get(assets, runMusic, null) let hasBeenRun = false let camera = Camera({ globalScope, parent: self, ...cameraSpec, }) // axesEnabled can be specified in datum or overridden in spec (LevelBubbles) let axes = null let axesEnabled = spec.hasOwnProperty('axesEnabled') ? spec.axesEnabled : datum.hasOwnProperty('axesEnabled') ? datum.axesEnabled : true if (axesEnabled) axes = Axes({ drawOrder: LAYERS.axes, camera, globalScope, parent: self, }) if (axes) trackedEntities.unshift(axes) // Gridlines and corresponding coordinate bubble const gridlines = Gridlines({ drawOrder: LAYERS.gridlines, camera, globalScope, parent: self, domSelector: '#body', }) const coordinateBox = CoordinateBox({ drawOrder: LAYERS.gridlines, camera, globalScope, parent: self, content: '0, 0', domSelector: '#reset-button', style: { ...self.style, visibility: 'hidden' }, }) disableGridlines() let darkenBufferOrScreen, darkenBuffer // HACK: Collect entities created (only) in Level // in buffer which can then be darkened (ConstantLake) if (spec.useDarkenBuffer) { darkenBuffer = ScreenBuffer({ parent: self, drawOrder: LAYERS.lighting, screen, postProcess: (ctx, width, height, params) => { ctx.globalCompositeOperation = 'source-atop' ctx.fillStyle = `rgba(1.0, 0.5, 0, ${params.darkenOpacity})` ctx.fillRect(0, 0, width, height) ctx.globalCompositeOperation = 'source-over' }, parameters: { darkenOpacity: 0.5, }, }) darkenBufferOrScreen = Screen({ canvas: darkenBuffer.canvas, parentScreen: screen, }) } else { darkenBufferOrScreen = screen } const startingExpression = spec.startingExpression ?? getStartingGraphExpression() let polar if (graphType === 'polar') polar = true else if (graphType === 'cartesian') polar = false else throw `Unexpected graphType '${graphType}', expected polar/cartesian` const graph = Graph({ camera, screen: darkenBufferOrScreen, globalScope, expression: mathquillToMathJS(startingExpression), parent: self, drawOrder: LAYERS.graph, colors, sledders, useInterpolation: false, polar, minTheta, maxTheta, invertGravity, sampleCount: world.sampleDensitySetting, terrainLayers: world.terrainLayersSetting, }) let skyColors = colors.sky if (_.isString(skyColors)) skyColors = [[0, skyColors]] let skyGradient = screen.ctx.createLinearGradient(0, 0, 0, 1) for (const color of skyColors) skyGradient.addColorStop(color[0], color[1]) function preprocessDatum(datum) { // Reuse datum across levels/bubbles if (datum._preprocessed) return // Add biome defaults + required assets const biomeName = datum.biome ?? 'westernSlopes' const biome = BIOMES[biomeName] _.defaults(datum, biome) if (biome.assets) _.merge(datum.assets, biome.assets) // Expand `dialogue` array to individual speech objects const dialogue = datum.dialogue const walkers = datum.walkers ?? [] const allWalkers = [ ...walkers, ..._.flatten( walkers.map((v) => _.isArray(v.walkers) ? v.walkers : _.isObject(v.walkers) ? [v.walkers] : [], ), ), ] if (dialogue) { for (const line of dialogue) { if (!line.speaker) { throw new Error( `Line of dialogue '${line.content}' has no speaker specified in level '${datum.name}'`, ) } const speaker = allWalkers.find((walker) => walker.name == line.speaker) if (!speaker) { throw new Error( `Tried to add line of dialogue for unknown speaker '${line.speaker}' in level '${datum.name}'`, ) } // Add default length and gap values if (_.isUndefined(line.domain)) { if (_.isUndefined(line.length)) line.length = 3 if (_.isUndefined(line.gap)) line.gap = 2 } // Add default positions if (line.speaker == 'Ada') { if (_.isUndefined(line.x)) line.x = 0 if (_.isUndefined(line.y)) line.y = 0.7 } else if (line.speaker == 'Jack') { if (_.isUndefined(line.x)) line.x = 0 if (_.isUndefined(line.y)) line.y = 0.8 } if (!speaker.speech) speaker.speech = [] speaker.speech.push(line) } let previous = null for (const line of dialogue) { if (line.length) { if (line.domain) { throw new Error( `Tried to set length of line of dialogue in level '${datum.name}' with existing domain`, ) } // Hard-coded assumption that the speaker we care about is the first one. // We only support a single top-level walker anyway… const speaker = walkers[0] // By default start at speaker x const endOfLastLine = (previous && previous.domain && previous.domain[1]) || speaker.x const start = endOfLastLine + line.gap const domain = [start, start + line.length] line.domain = domain } previous = line } } // Also split up processed speech bubbles with \n for (const walker of walkers) { if (!walker.speech) continue const newLines = [] const removeIndices = [] for (let i = 0; i < walker.speech.length; i++) { const speech = walker.speech[i] const content = speech.content if (!content) continue let lines = content.split('\n') if (lines.length > 0) { removeIndices.push(i) let previous = null for (let line of lines) { const newSpeech = _.cloneDeep(speech) newSpeech.content = line newSpeech.distance = ((previous && previous.distance) || lines.length * 0.4 + 1.1) - 0.4 if (!newSpeech.content) console.log( 'adding new lines', newLines.filter((line) => line.content == null), newSpeech, ) newLines.push(newSpeech) previous = newSpeech } } } for (let i = removeIndices.length; i--; i >= 0) { walker.speech.splice(removeIndices[i], 1) } walker.speech = walker.speech.concat(newLines) } datum._preprocessed = true } function initMathEditor() { /* By default, make math editor display the graph */ ui.expressionEnvelope.setAttribute('disabled', false) ui.expressionEnvelope.classList.remove('hidden') ui.mathFieldLabel.innerText = `${graph.label}=` ui.mathField.latex(startingExpression) ui.mathFieldStatic.latex(startingExpression) } function awake() { /* Initialization tasks that can be done without assets */ // Can be overridden, use self. self.initMathEditor() // For puzzles, enable stop button ui.stopButton.classList.remove('disabled') /* Load assets */ if (tryToLoadAssets) { self.active = false // Load assets assets.load( datum.assets, () => { self.active = true awakeWithAssets() }, true, ) } else { awakeWithAssets() } } function awakeWithAssets() { loadDatum(spec.datum) // Allow for overload from subclass self.awakeWithAssetsAndDatum() } function awakeWithAssetsAndDatum() { refreshLowestOrder() // Add a variable to globalScope for player position globalScope.p = math.complex() assignPlayerPosition() if (playBackgroundMusic) playBackgroundMusic(datum.backgroundMusic, self) } function start() {} function checkTransition(entity) { if (!entity.activeInHierarchy) return const transition = entity.transition if (transition) { // If X values met then make transition if (transition.xRequirements.length == 0) { const target = self.children.find((s) => s.name === transition.name) if (!target) throw Error( `Unable to find transition target from '${entity.name}' to '${transition.name}' (check the manifest!)`, ) target.active = true entity.active = false if (entity.walkers) entity.walkers.forEach((w) => (w.active = false)) if (target.walkers) target.walkers.forEach((w) => (w.active = true)) const x = entity.transform.x target.transform.x = x } // Otherwise check if current target met, if so then pop else { if (Math.abs(transition.xRequirements[0] - entity.transform.x) < 1) { transition.xRequirements.splice(0, 1) } } } } function getStartingGraphExpression() { const isPuzzle = urlData?.isPuzzle ?? false if (isPuzzle) { return urlData?.expressionOverride ? urlData?.expressionOverride : defaultExpression } return savedLatex ?? defaultExpression } function getTime() { return globalScope.t } function getCutsceneX() { return 0 } function tick() { _.each(walkers, checkTransition) _.each(sledders, checkTransition) if ( datum.victoryX != null && !completed && self.getCutsceneX && self.getCutsceneX() > datum.victoryX ) { completed = true levelCompleted(true) } // Can be overridden (self.) let time = self.getTime().toFixed(1) if (time === '0.0') time = '0' // ui.timeString.innerHTML = 't='+time ui.runButtonString.innerHTML = 't=' + time ui.stopButtonString.innerHTML = 't=' + time assignPlayerPosition() } function assignPlayerPosition() { const playerEntity = walkers.length > 0 ? walkers[0] : sledders.length > 0 ? sledders[0] : axes // TODO: assignPlayerPosition() shouldn't exist, // as it abuses globalScope if (playerEntity) { globalScope.p.re = playerEntity.transform.position.x globalScope.p.im = playerEntity.transform.position.y } } function addGoal(goalDatum) { const generator = { path: PathGoal, fixed: FixedGoal, dynamic: DynamicGoal, }[goalDatum.type || 'fixed'] const goal = generator({ name: 'Goal ' + goals.length, parent: self, camera, graph, assets, sledders, globalScope, drawOrder: LAYERS.goals, goalCompleted, goalFailed, getLowestOrder: () => lowestOrder, world, invertGravity, ...goalDatum, }) goals.push(goal) } function addDirector(directorDatum) { const generator = { tracking: TrackingDirector, waypoint: WaypointDirector, lerp: LerpDirector, // 'drag': DragDirector, }[directorDatum.type || 'tracking'] const director = generator({ parent: self, camera, graph, globalScope, trackedEntities, ...directorDatum, }) directors.push(director) } function tipCompleted() { // Movves index of all tips down by one for (i = 0; i < tips.length; i++) { datum.tips[i].index -= 1 tips[i].refreshDOM() } } function addTip(tipDatum) { tips.push( Tip({ parent: self, camera, graph, tipCompleted, globalScope, visible: false, place: 'top-right', ...tipDatum, }), ) } function addWalker(walkerDatum) { const walker = Walker({ name: 'Walker ' + walkers.length, parent: self, camera, graph, globalScope, levelCompleted, screen: darkenBufferOrScreen, speechScreen: screen, drawOrder: LAYERS.walkers, hasDarkMode: darkenable, ...walkerDatum, }) walkers.push(walker) // trackDescendants(walker) } function addSledder(sledderDatum) { const sledder = Sledder({ name: 'Sledder ' + sledders.length, parent: self, camera, graph, globalScope, screen: darkenBufferOrScreen, drawOrder: LAYERS.sledders, speechScreen: screen, motionBlur: false, invertGravity, ...sledderDatum, }) sledders.push(sledder) // trackDescendants(sledder, speech) } function addSound(soundDatum) { const sound = Sound({ name: 'Sound ' + soundDatum.asset, parent: self, level: self, walkers, sledders, ...soundDatum, }) sounds.push(sound) } function addSprite(spriteDatum) { const sprite = Sprite({ name: 'Sprite ' + sprites.length, parent: self, camera, graph, sky, globalScope, drawOrder: LAYERS.backSprites, anchored: true, screen: darkenBufferOrScreen, speechScreen: screen, ...spriteDatum, }) sprites.push(sprite) } function addText(textDatum) { const text = Text({ name: 'Text ' + texts.length, parent: self, camera, globalScope, drawOrder: LAYERS.text, ...textDatum, }) texts.push(text) } function goalCompleted(goal) { if (!completed) { refreshLowestOrder() let levelComplete = true for (goal of goals) { if (!goal.completed) { levelComplete = false break } } assets.sounds.goal_success.play() if (levelComplete) { completed = true levelCompleted() assets.sounds.level_success.play() save() } } } // Serialize to // 1. Store completed levels // 2. Share solutions // 3. Share custom levels function serialize() { if (urlData?.isPuzzle) { return serializePuzzle() } const json = { v: 0.1, // TODO: change version handling to World? nick: datum.nick, completed: hasBeenCompleted, } return json } // Puzzles are completely serializable using the url data function serializePuzzle() { urlData.expressionOverride = currentLatex return urlData } function goalFailed(goal) { if (goal.order) { for (g of goals) { if (g.order && !g.completed) g.fail() } } assets.sounds.goal_fail.play() // Show try again button in place of reset button ui.tryAgainButton.setAttribute('hide', false) ui.stopButton.setAttribute('hide', true) } function playOpenMusic() { if (openMusic) openMusic.play() } function reset() { stopRunning() } function getDefaultExpression() { return defaultExpression } function restart() { // Can be overridden, use self. const expression = self.getDefaultExpression() // setGraphExpression() only updates static ui.mathField.latex(expression) self.sendEvent('setGraphExpression', [ mathquillToMathJS(expression), expression, ]) refreshLowestOrder() } function refreshLowestOrder() { lowestOrder = 'Z' for (goal of goals) { if (!goal.completed && goal.order < lowestOrder) { lowestOrder = goal.order } } _.invokeEach(goals, 'refresh') } function startRunning() { ui.runButton.classList.remove('flash-shadow') ui.mathFieldStatic.latex(currentLatex) ui.tSliderContainer.setAttribute('hide', true) if (usingTInExpression) { if (graph.resample) graph.resample() _.invokeEach(goals, 'reset') _.invokeEach(sledders, 'reset') } if (!hasBeenRun) { if (runMusic) runMusic.play() hasBeenRun = true } } function stopRunning() { _.invokeEach(goals, 'reset') _.invokeEach(tips, 'toggleVisible') if (usingTInExpression) ui.tSliderContainer.setAttribute('hide', false) completed = false refreshLowestOrder() } function setSky(skyDatum) { if (sky) sky.destroy() if (skyDatum) sky = Sky({ parent: self, camera, globalScope, asset: skyDatum.asset, margin: skyDatum.margin, screen: darkenBufferOrScreen, drawOrder: LAYERS.background, motionBlur: false, ...skyDatum, }) } function setSnow(snowDatum) { if (snow) snow.destroy() if (snowDatum) snow = Snow({ parent: self, camera, globalScope, screen, density: snowDatum.density, velocityX: snowDatum.velocity.x, velocityY: snowDatum.velocity.y, maxHeight: snowDatum.maxHeight, drawOrder: LAYERS.snow, screen: darkenBufferOrScreen, ...snowDatum, }) } function setClouds(cloudDatum) { if (clouds) clouds.destroy() if (cloudDatum) clouds = Clouds({ parent: self, camera, globalScope, assets, velocity: cloudDatum.velocity, heights: cloudDatum.heights, drawOrder: LAYERS.clouds, screen: darkenBufferOrScreen, ...cloudDatum, }) } function loadDatum(datum) { if (datum.sky) setSky(datum.sky) if (!isBubbleLevel) _.each(datum.sounds, addSound) _.each(datum.sprites, addSprite) _.each(datum.sledders, addSledder) _.each(datum.walkers, addWalker) _.each(datum.goals, addGoal) _.each(datum.texts, addText) _.each(datum.directors || [{}], addDirector) isBubbleLevel || _.each(datum.tips || [], addTip) if (urlData && urlData.t) { globalScope.runTime = urlData.t } if (isBubbleLevel && datum.bubble) { // console.log(datum) datum = { ...datum, ...datum.bubble, } } if (datum.clouds) setClouds(datum.clouds) if (datum.water && !isBubbleLevel) { // Water({ // parent: self, // camera, // waterQuad: quads.water, // screen: darkenBufferOrScreen, // globalScope, // drawOrder: LAYERS.backSprites, // ...datum.water, // }) } if (datum.snow) setSnow(datum.snow) if (datum.slider && !isBubbleLevel) { HintGraph({ ui, parent: self, camera, screen, globalScope, drawOrder: LAYERS.hintGraph, slider: datum.slider, }) } self.sortChildren() //TODO: Hide the loading screen here instead of just when the assets are loaded assets.hideLoadingScreen() } function save() { // Do not write to URL if this is a bubble level if (isBubbleLevel) return // Save to player storage and to URI const serialized = self.serialize() // Overridden storage.setLevel(datum.nick, serialized) history.pushState( null, null, '?' + LZString.compressToBase64(JSON.stringify(serialized)), ) // console.log('Writing to URL:', serialized) } function tVariableChanged(newT) { globalScope.customT = newT if (graph.resample) graph.resample() _.invokeEach(goals, 'reset') _.invokeEach(sledders, 'reset') } // Tightly-couple Level superclass to PathGoal // to ignore graph updates while editing goal // TODO: Fix this let editingPath function selectedPathGoalForEditing() { console.log('Selected path goal for editing') editingPath = true } function unselectedPathGoalForEditing() { console.log('Unselected path goal for editing') editingPath = false } function setGraphExpression(text, latex) { if (editingPath) return ui.mathFieldStatic.latex(latex) currentLatex = latex save() usingTInExpression = false try { math.parse(text).traverse((node) => { if (node.name == 't' || node.args?.some((arg) => arg.name == 't')) { usingTInExpression = true throw '' // HACK: Throw exception for early return } }) } catch {} ui.tSliderContainer.setAttribute('hide', !usingTInExpression) graph.expression = text ui.expressionEnvelope.setAttribute('valid', graph.valid) if (!graph.valid) { ui.expressionEnvelope.classList.add('invalid-exp') } else { ui.expressionEnvelope.classList.remove('invalid-exp') } _.invokeEach(sledders, 'reset') _.invokeEach(goals, 'reset') } function mathFieldFocused() { ui.expressionEnvelope.classList.remove('flash-shadow') } // TODO: Figure out how gridlines enable/disable logic // should be organized, along with UI generally function disableGridlines() { gridlines.active = false coordinateBox.visible = false } function enableGridlines() { // TODO: Implement special gridline behavior for editor gridlines.active = true coordinateBox.visible = true } function onMouseDown() { if (!world.running) enableGridlines() } function onMouseUp() { disableGridlines() } function selectScreenCoordinates(screenX, screenY) { // TODO: Creating a vector every call? Anti-pattern? const worldPoint = Vector2(screenX, screenY) camera.screenToWorld(worldPoint) coordinateBox.selectCoordinates( screenX, screenY, worldPoint.x, worldPoint.y, ) gridlines.updatePosition(worldPoint.x, worldPoint.y) } function destroy() { _.invokeEach(tips, 'destroy') } function resize() { darkenBufferOrScreen.resize() graph.resize() } function setBiome(biome) { // Sky setSky(biome.sky) // Snow, clouds setSnow(biome.snow) setClouds(biome.clouds) // Music if (playBackgroundMusic) playBackgroundMusic(biome.backgroundMusic, self) // Graph colors graph.assignColors(biome.colors) } return self.mix({ awake, awakeWithAssetsAndDatum, start, destroy, darkenBufferOrScreen, setBiome, addGoal, tick, resize, serialize, startRunning, stopRunning, setGraphExpression, selectedPathGoalForEditing, unselectedPathGoalForEditing, camera, graph, restart, reset, save, playOpenMusic, mathFieldFocused, gridlines, coordinateBox, enableGridlines, disableGridlines, selectScreenCoordinates, get datum() { return datum }, get isRunningAsCutscene() { return runAsCutscene }, save, goals, get currentLatex() { return currentLatex }, get datum() { return spec.datum }, get completed() { return completed }, get nick() { return datum.nick }, sledders, walkers, get graph() { return graph }, // TODO: temp trackedEntities, tVariableChanged, sky, levelCompleted, getDefaultExpression, initMathEditor, getTime, getCutsceneX, onMouseDown, onMouseUp, loadDatum, addSledder, get darkenBuffer() { if (!spec.useDarkenBuffer) { throw `Tried to access non-existent darken buffer in Level` } return darkenBuffer }, }) } FILE: Types_Entities_Navigator.js function Navigator(spec) { const self = Entity(spec, 'Navigator') const { screen, levelData, tickDelta, getEditing, setLevel, assets, playerStorage, } = spec const initialMapFov = 15 const minMapFov = initialMapFov const maxMapFov = 80 let mapFov = initialMapFov const camera = Camera({ screen, fov: mapFov, parent: self, offset: [0, 0], }) const panDirector = PanDirector({ parent: self, camera, }) const map = Sprite({ parent: self, camera, drawOrder: LAYERS.map, anchored: false, size: 190, x: 85, y: -5.5, asset: 'images.world_map', }) const clickable = Clickable({ entity: self, camera, }) let showAll = false let showAllUsed = false let initialBubble = null const bubbles = _.map(levelData, createBubble) const bubbleRenderQueue = [] const bubbleRenderCap = 10 function start() { if (initialBubble) initialBubble.completeAllRequirements() for (const datum of playerStorage.getCompletedLevels()) { let bubble = getBubbleByNick(datum.nick) if (bubble) { bubble.complete() bubble.completeAllRequirements() } } } function tick() { if (bubbleRenderQueue.length > 0) { let b = 0 while (b++ <= bubbleRenderCap && bubbleRenderQueue.length > 0) { const bubble = bubbleRenderQueue.pop() console.log('Rendering bubble', bubble.nick) bubble.render() } } } function draw() { screen.ctx.fillStyle = '#fff' screen.ctx.fillRect(0, 0, screen.width, screen.height) // let left = camera.lowerLeft.x + 5 // let right = camera.upperRight.x - 5 // let top = camera.upperRight.y // let bottom = camera.lowerLeft.y // rect.center.set((left + right) / 2, (top + bottom) / 2) // rect.width = right - left // rect.height = top - bottom // rect.draw(screen.ctx, camera) } function createBubble(levelDatum) { const bubble = LevelBubble({ parent: self, levelDatum, setLevel, assets, camera, getEditing, tickDelta, getBubbleByNick, panCamera, getShowAll: () => showAll, drawOrder: LAYERS.levelBubbles, }) return bubble } function getBubbleByNick(nick) { for (bubble of bubbles) { if (bubble.nick == nick) return bubble } return null } function revealHighlightedLevels(nick) { const highlightedLevels = _.filter(bubbles, (v) => v.hilighted || showAll) const nicks = _.map(highlightedLevels, (v) => v.nick) // nicks.push(nick) moveToLevel( nick, 0, () => { assets.sounds.map_zoom_out.play() }, 8, ) } function moveToLevel(nicks, duration = 0, cb, padding = 10) { if (!_.isArray(nicks)) nicks = [nicks] const array = nicks.length == 0 ? bubbles : _.map(nicks, getBubbleByNick) const position = Vector2() const minPosition = Vector2Pinf() const maxPosition = Vector2Ninf() for (bubble of array) { position.add(bubble.transform.position) minPosition.min(bubble.transform.position) maxPosition.max(bubble.transform.position) } if (array.length == 0) { maxPosition.set() minPosition.set() } minPosition.add(maxPosition, position) position.divide(2) const delta = Vector2(maxPosition).subtract(minPosition) // const fov = Math.max(delta.x, delta.y) / 2 + padding panDirector.moveTo( null, { fov: initialMapFov, position, }, duration, () => { // When finished moving, reset FOV from zoom mapFov = initialMapFov panDirector.setTargetFov(mapFov) cb() }, ) } function setShowAll(_showAll) { if (showAll != _showAll) { showAll = _showAll if (showAll) { ui.showAllButton.setAttribute('hide', true) assets.sounds.map_zoom_show_all.play() showAllUsed = true } else { } refreshBubbles() } } function refreshBubbles() { _.invokeEach(bubbles, 'refreshPlayable') _.invokeEach(bubbles, 'refreshArrows') let b = 0 for (bubble of bubbles) { if (bubble.visible && !bubble.rendered) { bubbleRenderQueue.push(bubble) } } } function panCamera(point, cb = null) { panDirector.moveTo( null, { fov: initialMapFov, position: point, }, 1, cb, ) } function click() { return false // Don't let propagate to child LevelBubbles } function onMouseWheel(event) { const deltaY = event.deltaY mapFov = math.clamp(minMapFov, maxMapFov, mapFov + deltaY * 0.2) panDirector.setTargetFov(mapFov) } return self.mix({ start, tick, draw, onMouseWheel, moveToLevel, clickable, updatePanVelocity: panDirector.updateVelocity, click, refreshBubbles, revealHighlightedLevels, getBubbleByNick, get showAll() { return showAll }, set showAll(v) { setShowAll(v) }, get showAllUsed() { return showAllUsed }, set showAllUsed(v) { showAllUsed = v }, get initialBubble() { return initialBubble }, set initialBubble(v) { initialBubble = v }, }) } FILE: Types_Entities_PathGoalHandle.js function PathGoalHandle(spec) { const { self, parent, camera, ctx } = Entity(spec, 'Fixed Goal Handle') const { type, editor } = spec const transform = Transform(spec) const radius = 0.5 const color = '#f00' const center = Vector2() const handle = Circle({ center, radius, color, }) self.mix(handle) const clickable = Clickable({ entity: self, shape: handle, }) function draw() { if (parent.selected && editor.editing) { handle.center = transform handle.draw(ctx, camera) } } function dragMove(point) { if (parent.selected && editor.editing) { if (type === 'start') { parent.setStart(point.x) } else if (type === 'end') { parent.setEnd(point.x) } } } function dragEnd() { if (parent.selected && editor.editing) { editor.update() } } return self.mix({ transform, clickable, dragMove, dragEnd, draw, center, }) } FILE: Types_Entities_PolarGraph.js // TODO: Figure out how polar vs. cartesian should work // and how this class should work (or if needs to exist?) function PolarGraph(spec) { const { self, screen, camera, ctx } = Entity(spec, 'Polar Graph') let { sampleCount = 129, sampleDensity = 4, globalScope, colors = Colors.biomes.alps, bounds, freeze = false, fill = true, scaleStroke = false, stroke = true, dashed = false, dashOffset = 0, dashSettings = [0.5, 0.5], sledders = [], useInterpolation = true, refreshPeriodT = 0.3, fixedPoints = false, minTheta = 0, maxTheta = TAU, invertGravity, } = spec let { strokeColor = colors.groundStroke, strokeWidth = colors.groundStrokeWidth, fillColor = colors.groundFill, } = spec if (bounds && sampleDensity) { let span = Math.abs(bounds[0] - bounds[1]) sampleCount = Math.ceil(sampleDensity * span) } const undashedSettings = [] const dashSettingsScreen = [0, 0] // Scope is global scope const scope = globalScope const sampler = new Sampler({ ...spec, scope, }) const samples = sampler.generateSampleArray(sampleCount) let interpolationSampler = null if (useInterpolation) { interpolationSampler = new InterFrameSampler({ sampler, sampleCount, refreshPeriodT, }) } const screenSpaceSample = Vector2() const minSample = Vector2() const maxSample = Vector2() let terrainLayers = 6 let terrainParameters // generateTerrainParameters() // function generateTerrainParameters() { // terrainParameters = [] // for (let i = 0; i < terrainLayers; i++) { // scalar = math.lerp(1, 3, Math.random()) // terrainParameters.push([ // math.lerp(0, TAU, Math.random()), // math.lerp(0.3, 3, Math.random()), // math.lerp(3, 5, Math.random()) * scalar, // math.lerp(1, 3, Math.random()) * scalar, // math.lerp(0.05, 0.25, Math.random()), // ]) // } // } function resample(refresh = false) { if (useInterpolation) { if (refresh) interpolationSampler.refresh(minTheta, maxTheta, scope.t, scope.dt) interpolationSampler.resetExtrema() interpolationSampler.sampleRange(scope, samples, sampleCount, minX, maxX) minSample.set(minX, interpolationSampler.min) maxSample.set(maxX, interpolationSampler.max) } else { sampler.resetExtrema() sampler.sampleRange( scope, samples, sampleCount, 'theta', minTheta, maxTheta, ) // TODO: Refactor Sampler (ParametericSampler?) minSample.y = math.pinf maxSample.y = math.ninf for (const sample of samples) { const theta = sample[0], r = sample[1] sample.set(r * Math.cos(theta), r * Math.sin(theta)) if (sample.y < minSample.y) minSample.set(sample) if (sample.y > maxSample.y) maxSample.set(sample) } } } function tVariableChanged() { resample(true) } function tick() { if (!freeze) resample() } const debugVectorOrigin = Vector2() const debugVectorTerminus = Vector2() function drawDebugVector(ctx, vector, color, origin = position) { camera.worldToScreen(origin, debugVectorOrigin) debugVectorTerminus.set(vector) debugVectorTerminus.add(origin) camera.worldToScreen(debugVectorTerminus) ctx.beginPath() ctx.moveTo(debugVectorOrigin.x, debugVectorOrigin.y) ctx.lineTo(debugVectorTerminus.x, debugVectorTerminus.y) ctx.strokeStyle = color ctx.lineWidth = 4 ctx.stroke() } let point = Vector2(), tangent = Vector2(), normal = Vector2() function draw() { ctx.save() const worldToScreenScalar = camera.worldToScreenScalar() const strokeScalar = scaleStroke ? worldToScreenScalar : 1 // Fill if curve is closed. TODO: I'm pretty sure // this doesn't work in every case? const start = samples[0] const end = samples[sampleCount - 1] fill = start.distance(end) < 0.001 if (fill) { ctx.beginPath() camera.worldToScreen(samples[0], screenSpaceSample) ctx.moveTo(screenSpaceSample.x, screenSpaceSample.y) for (let i = 1; i < sampleCount; i++) { camera.worldToScreen(samples[i], screenSpaceSample) ctx.lineTo(screenSpaceSample.x, screenSpaceSample.y) } // ctx.lineTo(screen.width, screen.height) // ctx.lineTo(0, screen.height) ctx.fillStyle = fillColor ctx.fill() ctx.clip() // for (let i = 0; i < terrainLayers; i++) // drawSine.apply(null, terrainParameters[i]) } if (stroke) { ctx.beginPath() camera.worldToScreen(samples[0], screenSpaceSample) ctx.moveTo(screenSpaceSample.x, screenSpaceSample.y) for (let i = 1; i < sampleCount; i++) { const sample = samples[i] camera.worldToScreen(sample, screenSpaceSample) ctx.lineTo(screenSpaceSample.x, screenSpaceSample.y) } dashSettingsScreen[0] = dashSettings[0] * strokeScalar dashSettingsScreen[1] = dashSettings[1] * strokeScalar ctx.setLineDash(dashed ? dashSettingsScreen : undashedSettings) ctx.dashOffset = 30 ctx.lineCap = 'round' ctx.strokeStyle = strokeColor ctx.lineWidth = strokeWidth * strokeScalar ctx.stroke() } ctx.restore() for (let theta = 0; theta <= TAU + 0.001; theta += TAU / 12) { pointAtTheta(theta, point) normalVectorAt(theta, normal) normal.normalize() normal.multiply(0.2) drawDebugVector(ctx, normal, 'black', point) tangentVectorAt(theta, tangent) tangent.normalize() tangent.negate() drawDebugVector(ctx, tangent, 'blue', point) } } function drawSine( xOffset = 0, yOffset = 0, xScale = 1, yScale = 1, opacity = 0.5, ) { ctx.beginPath() ctx.globalAlpha = opacity camera.worldToScreen(samples[0], screenSpaceSample) ctx.moveTo(screen.width, screen.height) ctx.lineTo(0, screen.height) for (let i = 0; i < sampleCount; i++) { const x = samples[i].x const increasedX = x + 50 // if (!window.logged) // console.log(samples[i]); window.logged = true camera.worldToScreen(samples[i], screenSpaceSample) const y = screenSpaceSample.y + (Math.sin(x / xScale - xOffset) + 1) * camera.worldToScreenScalar(1) * yScale + yOffset * camera.worldToScreenScalar(1) ctx.lineTo(screenSpaceSample.x, y) } ctx.fillStyle = _.isString(colors.groundPattern) ? colors.groundPattern : _.sample(colors.groundPattern) ctx.fill() ctx.globalAlpha = 1 } function resize() { resample(true) } function startRunning() { resample(true) } function stopRunning() { resample(true) _.invokeEach(sledders, 'reset') } function pointAtTheta(theta, p) { const r = self.sample('theta', theta) p.set(r * Math.cos(theta), r * Math.sin(theta)) } const pAtTheta = Vector2(), pAtMinusTheta = Vector2() function thetaOfClosestSurfacePoint(position) { const theta = Math.atan2(position.y, position.x) pointAtTheta(theta, pAtTheta) pointAtTheta(-theta, pAtMinusTheta) if (pAtTheta.distance(position) < pAtMinusTheta.distance(position)) { return theta } else { return -theta } } function sampleSlopeFromTheta(theta) { const r = self.sample('theta', theta) const rPrime = self.sampleSlope('theta', theta) const sinTheta = Math.sin(theta), cosTheta = Math.cos(theta) const graphSlope = (rPrime * sinTheta + r * cosTheta) / (rPrime * cosTheta - r * sinTheta) return graphSlope } function tangentVectorAt(theta, output = Vector2()) { const r = self.sample('theta', theta), rPrime = self.sampleSlope('theta', theta), sinTheta = Math.sin(theta), cosTheta = Math.cos(theta) output.set( -r * sinTheta + cosTheta * rPrime, cosTheta * r + sinTheta * rPrime, ) output.negate() } function normalVectorAt(theta, output = Vector2()) { tangentVectorAt(theta, output) output.orthogonalize() } // Velocity vector w.r.t. time function velocityVectorAt(theta, output = Vector2()) { const rPrimeWrtTime = self.sampleSlope('t', globalScope.t, 'theta', theta), sinTheta = Math.sin(theta), cosTheta = Math.cos(theta) output.set(rPrimeWrtTime * cosTheta, rPrimeWrtTime * sinTheta) } resample(true) self.mix(sampler) return self.mix({ tick, draw, resize, velocityVectorAt, tangentVectorAt, normalVectorAt, pointAtTheta, thetaOfClosestSurfacePoint, sampleSlopeFromTheta, tVariableChanged, resample, bounds, startRunning, stopRunning, get label() { return 'R' }, get isPolar() { return true }, get maxTheta() { return maxTheta }, get minSample() { return minSample }, get maxSample() { return maxSample }, get samples() { return samples }, get expression() { return sampler.expression }, set expression(v) { sampler.expression = v resample(true) }, get strokeWidth() { return strokeWidth }, set strokeWidth(v) { strokeWidth = v }, get strokeColor() { return strokeColor }, set strokeColor(v) { strokeColor = v }, get fillColor() { return fillColor }, set fillColor(v) { fillColor = v }, get dashSettings() { return dashSettings }, set dashSettings(v) { dashSettings = v }, get dashOffset() { return dashOffset }, set dashOffset(v) { dashOffset = v }, get dashed() { return dashed }, set dashed(v) { dashed = v }, set terrainLayers(v) { terrainLayers = v generateTerrainParameters() }, set sampleCount(v) { sampleCount = v samples = sampler.generateSampleArray(sampleCount) interpolationSampler = new InterFrameSampler({ sampler, sampleCount, refreshPeriodT, }) }, }) } FILE: Types_Entities_ScreenBuffer.js function ScreenBuffer(spec) { const { self, screen } = Entity(spec, 'ScreenBuffer') let { postProcess = null, canvas: bufferCanvas, parameters } = spec if (!bufferCanvas) { bufferCanvas = document.createElement('canvas') } bufferCanvas.width = screen.width bufferCanvas.height = screen.height const bufferCtx = bufferCanvas.getContext('2d') bufferCtx.owner = 'ScreenBuffer' // For debugging function resize(width, height) { bufferCanvas.width = width bufferCanvas.height = height } function clear() { bufferCtx.clearRect(0, 0, bufferCanvas.width, bufferCanvas.height) } function draw() { if (postProcess) postProcess( bufferCtx, bufferCanvas.width, bufferCanvas.height, parameters, ) screen.ctx.drawImage(bufferCanvas, 0, 0, screen.width, screen.height) clear() } return _.mixIn(self, { draw, resize, parameters, screen, get canvas() { return bufferCanvas }, }) } FILE: Types_Entities_Sky.js function Sky(spec) { const { self, screen, globalScope, assets, ctx } = Entity(spec, 'Sky') let { asset } = spec const image = _.get(assets, asset, $('#error-sprite')) const pos = Vector2() const size = Vector2() function draw() { // Position/scale background image to always cover the screen if (image.height / image.width >= screen.height / screen.width) { const adjustmentY = (screen.width * image.height) / image.width pos.set(0, (screen.height - adjustmentY) / 2) size.set(screen.width, adjustmentY) } else { const adjustmentX = (screen.height * image.width) / image.height pos.set((screen.width - adjustmentX) / 2, 0) size.set(adjustmentX, screen.height) } screen.ctx.drawImage(image, pos[0], pos[1], size[0], size[1]) } return self.mix({ draw, asset, get width() { return image.width }, get height() { return image.height }, }) } FILE: Types_Entities_Sledder.js function Sledder(spec = {}) { const { self, screen, parent } = Entity(spec, 'Sledder') const transform = Transform(spec, self) const rigidbody = Rigidbody({ ...spec, transform, }) const originalX = transform.position.x, originalY = transform.position.y let { asset = 'images.lunchbox_sam_sled', size = 2, globalScope, camera, graph, speech, speechScreen, x: originX = 0, activeRange = [NINF, PINF], flipX, transition = null, } = spec const ctx = screen.ctx const velocity = [0, 0] const position = transform.position const slopeTangent = Vector2() const pointCloud = [ Vector2(0, 0), Vector2(-0.5, 0), Vector2(-0.5, 0.5), Vector2(-0.3, 0.8), Vector2(-0.1, 1.6), Vector2(0.1, 1.6), Vector2(0.3, 1.6), Vector2(0.3, 0.7), Vector2(0.6, 0.5), Vector2(0.6, 0), ] const trail = Trail({ parent: self, x: 0.1, y: 0.5, }) const sprite = Sprite({ asset, size, speech, globalScope, parent: self, speechScreen, // opacity: 0, y: 1, flipX, }) const shape = Rect({ transform, width: size, height: size - 0.2, center: Vector2(0, size / 2 - 0.2), }) const clickable = Clickable({ entity: self, shape, transform, camera, }) reset() function tick() { rigidbody.tick() } function drawLocal() { pointCloud.forEach((v) => { ctx.beginPath() ctx.fillStyle = 'red' ctx.arc(v.x, -v.y, camera.screenToWorldScalar(4), 0, TAU) ctx.fill() }) } const debugVectorOrigin = Vector2() const debugVectorTerminus = Vector2() function drawDebugVector(ctx, vector, color, origin = position) { camera.worldToScreen(origin, debugVectorOrigin) debugVectorTerminus.set(vector) debugVectorTerminus.add(position) camera.worldToScreen(debugVectorTerminus) ctx.beginPath() ctx.moveTo(debugVectorOrigin.x, debugVectorOrigin.y) ctx.lineTo(debugVectorTerminus.x, debugVectorTerminus.y) ctx.strokeStyle = color ctx.lineWidth = 4 ctx.stroke() } function draw() { rigidbody.draw(ctx) // camera.drawThrough(ctx, drawLocal, transform) // drawDebugVector(ctx, slopeTangent, 'blue') // drawDebugVector(ctx, rigidbody.upright, 'orange') } function startRunning() {} function stopRunning() { rigidbody.resetVelocity() reset() } function reset() { const polar = graph.isPolar if (polar) { position.set(originalX, originalY) const surfaceTheta = graph.thetaOfClosestSurfacePoint(position) graph.tangentVectorAt(surfaceTheta, slopeTangent) slopeTangent.normalize() graph.normalVectorAt(surfaceTheta, rigidbody.upright) rigidbody.upright.normalize() graph.pointAtTheta(surfaceTheta, position) } else { transform.x = originX transform.y = graph.sample('x', transform.x) slopeTangent.x = 1 slopeTangent.y = graph.sampleSlope('x', transform.x) slopeTangent.normalize() // Set the Upright vector of rigidbody to the slope normal slopeTangent.orthogonalize(rigidbody.upright) } // let angle = Math.asin(slopeTangent.y) let angle = -PI / 2 + Math.atan2(rigidbody.upright.y, rigidbody.upright.x) transform.rotation = angle trail.reset() } /* Editor logic */ const editor = parent function select() { if (!editor.editing) return editor.select(self, ['x'], false) } function deselect() { if (!editor.editing) return editor.deselect() } function dragMove(point) { if (!editor.editing) return position.set(point) editor.update(false) } function dragEnd() { if (!editor.editing) return originX = transform.x reset() editor.update() } function setX(x) { originX = x reset() } function setY(y) { position.y = y } return self.mix({ transform, clickable, tick, draw, setX, setY, dragEnd, dragMove, startRunning, stopRunning, reset, pointCloud, select, deselect, get asset() { return asset }, get x() { return position.x }, get y() { return position.y }, get transition() { return transition }, set transition(v) { transition = v }, get activeRange() { return activeRange }, get selectable() { return !globalScope.running }, get velocity() { return self.active ? rigidbody.velocity.magnitude : 0 }, get rigidbody() { return rigidbody }, }) } FILE: Types_Entities_Snow.js function Flake(spec) { const { ctx, camera, spawnHeight, colorRange } = spec let growth let size let sizeCoefficient let spawned = false const position = Vector2() const color = Color() function spawn() { position.x = math.lerp( camera.lowerLeft.x, camera.upperRight.x, Math.random(), ) position.y = math.lerp(spawnHeight[0], spawnHeight[1], Math.random()) sizeCoefficient = Math.random() size = math.lerp(0.03, 0.06, sizeCoefficient) growth = 0 // colorRange[0].lerp(colorRange[1], Math.random(), color) color.set(Math.random() < 0.7 ? colorRange[0] : colorRange[1]) spawned = true } function tick() { if (position.x > camera.upperRight.x + 1) spawned = false else if (position.x < camera.lowerLeft.x - 1) spawned = false else if (position.y < camera.lowerLeft.y - 1) spawned = false // else if (position.y > camera.upperRight.y + 1) spawned = false if (!spawned) { if (Math.random() < 0.00001 * camera.fov * camera.fov) spawn() return } if (growth < 1) growth += 0.01 position.y -= 0.015 / (1 + sizeCoefficient) } function draw() { if (!spawned) return ctx.moveTo(position.x, -position.y) ctx.arc(position.x, -position.y, size * growth, 0, TAU) } function log() { console.log(`Snowflake is at ${position.toString()}`) } return { position, tick, draw, spawn, log, } } function Snow(spec, name = 'Snow') { const { self, screen, assets, density = 0.5, maxParticles = 70, spawnHeight = [8, 16], colorRange = [Color('#b8bdda'), Color('#e1b0f0')], camera, ctx, } = Entity(spec, name) const particles = [] let countdown = 3 for (let i = 0; i < maxParticles; i++) particles.push( Flake({ ctx, camera, spawnHeight, colorRange, }), ) function tick() { if (countdown) { countdown-- return } // particles[0].log() for (let i = 0; i < maxParticles; i++) { particles[i].tick() } } function drawLocal() { ctx.beginPath() for (let i = 0; i < maxParticles; i++) { particles[i].draw() } ctx.fillStyle = colorRange[0].hex ctx.globalAlpha = 0.7 ctx.fill() } function draw() { camera.drawThrough(ctx, drawLocal) } return self.mix({ tick, draw }) } FILE: Types_Entities_Sound.js function Sound(spec) { const { self, assets } = Entity(spec, 'Sound') const { asset, domain = null, loop = false, duration = null, fadeOut = 300, volume = 1, track = 'walkers', level = null, fadeOnNavigating = true, } = spec const howl = _.get(assets, asset) let secondsPlayed = 0 let played = false let soundId let fadeDestroyProgress = 0 let fadeDestroyDuration = 0 let mapFadeProgress = 0 let mapFade = false let lastVol function awake() { if (!howl) { // TODO: Need more robust solution for either // ensuring that all assets get loaded or throwing // errors for assets that forget to be // included in biome/level spec/eden.js throw `Expected sound (${asset}) to be loaded` } if (!domain) { soundId = howl.play() played = true } howl.volume(volume) howl.loop(loop) } function tick() { let vol = volume if (domain) { const x = level.getCutsceneX() // Sounds w/ domains only play once if (x > domain[0] && !howl.playing() && !played) { played = true soundId = howl.play() } if (domain.length != 2 && domain.length != 4) throw `Expected domain in Sound to have 2 (fade in range) or 4 (+ fade out range) elements, got ${domain.length}` vol = math.clamp01(math.unlerp(domain[0], domain[1], x)) if (domain.length == 4) { if (self.debug) console.log( `${ self.name } distance parameter: ${level.getCutsceneX()} volume: ${volume}`, ) vol *= math.clamp01(math.unlerp(domain[3], domain[2], x)) } } if (duration) { const a = 1 - math.clamp01( math.unlerp(duration - fadeOut, duration, secondsPlayed * 1000), ) vol *= a } if (howl.playing()) secondsPlayed += 1 / ticksPerSecond if (fadeDestroyDuration) { fadeDestroyProgress += tickDelta / fadeDestroyDuration vol *= 1 - fadeDestroyProgress if (fadeDestroyProgress >= 1) self.destroy() } if (mapFade) mapFadeProgress += tickDelta else mapFadeProgress -= tickDelta mapFadeProgress = math.clamp01(mapFadeProgress) vol *= 1 - mapFadeProgress if (vol != lastVol) { howl.volume(vol) lastVol = vol } } function fadeDestroy(duration = 1) { fadeDestroyDuration = duration } function onLevelFadeOut(navigating, duration) { if (fadeOnNavigating) mapFade = true } function onLevelFadeIn(navigating, duration) { if (fadeOnNavigating) mapFade = false } function destroy() { howl.stop() } return self.mix({ awake, tick, destroy, fadeDestroy, onLevelFadeIn, onLevelFadeOut, howl, }) } FILE: Types_Entities_Speech.js function Speech(spec) { const { self, screen, camera, world } = Entity(spec, 'Speech') let { content = 'WORDS WORDS WORDS', direction = 'up', font = 'Patrick Hand', baseline = 'alphabetic', align = 'center', color = '#222', size = 0.5, distance = 1, speech, speakerX = 0, speakerY = 0, domain = [NINF, PINF], drawIfRunning = false, globalScope, deactivationThreshold = null, activationThreshold = null, } = spec const transform = Transform(spec, self) let domainTransform let activationThresholdMet = false let textDirection switch (direction) { case 'up-left': textDirection = Vector2(-1, 1) align = 'center' // baseline = 'middle' break case 'up-up-left': textDirection = Vector2(-1, 2) align = 'center' // baseline = 'middle' break case 'up-left-left': textDirection = Vector2(-2, 1) align = 'center' // baseline = 'middle' break case 'left': textDirection = Vector2(-1, 0) align = 'right' // baseline = 'middle' break case 'left-up': textDirection = Vector2(-1, 1) align = 'right' // baseline = 'middle' break case 'left-left-up': textDirection = Vector2(-2, 1) align = 'right' // baseline = 'middle' break case 'left-up-up': textDirection = Vector2(-1, 2) align = 'right' // baseline = 'middle' break case 'up': textDirection = Vector2(0, 1) align = 'center' // baseline = 'middle' break case 'up-right': textDirection = Vector2(1, 1) align = 'center' // baseline = 'middle' break case 'up-up-right': textDirection = Vector2(1, 2) align = 'center' // baseline = 'middle' break case 'up-right-right': textDirection = Vector2(2, 1) align = 'center' // baseline = 'middle' break case 'right': textDirection = Vector2(1, 0) align = 'left' // baseline = 'middle' break case 'right-up': textDirection = Vector2(1, 1) align = 'left' // baseline = 'middle' break case 'right-up-up': textDirection = Vector2(1, 2) align = 'left' // baseline = 'middle' break case 'right-right-up': textDirection = Vector2(2, 1) align = 'left' // baseline = 'middle' break default: textDirection = direction } textDirection.normalize() const textTangent = Vector2(textDirection).orthogonalize() const textOriginPerturbation = Vector2(textTangent).multiply(0) //Math.random()*distance/6) const worldPosition = Vector2() const worldPositionScreen = Vector2() const speakerOrigin = Vector2(speakerX, speakerY) const speakerOriginWorld = Vector2() const textOrigin = Vector2(textDirection) .multiply(distance) .add(textOriginPerturbation) const textOriginWorld = Vector2() const textOriginScreen = Vector2() const lineDirection = Vector2() const lineOriginWorld = Vector2() const lineOriginScreen = Vector2() const lineTerminusWorld = Vector2() const lineTerminusScreen = Vector2() const controlPointWorld = Vector2() const controlPointPerturbation = Vector2( (Math.random() * Math.sign(textDirection.x || Math.round(Math.random()) * 2 - 1) * distance) / 10, (Math.random() * distance) / 10, ) const controlPointScreen = Vector2() if (speech) { if (!_.isArray(speech)) speech = [speech] for (s of speech) { if (_.isString(s)) s = { content: s } Speech({ parent: self, domainTransform, domain, globalScope, x: textOrigin.x + (s.x || 0), y: textOrigin.y + (s.y || 0) + size * 0.8, ...s, }) } } function awake() { domainTransform = self.getFromAncestor('domainTransform') } function tick() { transform.rotation = -transform.parentWorldRotation } function calculatePoints() { speakerOriginWorld.set(speakerOrigin).add(transform.position) transform.parent.transformPoint(speakerOriginWorld) transform.transformPoint(worldPosition.set()) worldPosition.add(textOrigin, textOriginWorld) textOriginWorld.subtract(speakerOriginWorld, lineDirection).normalize() lineOriginWorld.set(lineDirection).multiply(0.25).add(speakerOriginWorld) lineTerminusWorld.set(lineDirection).multiply(-0.25).add(textOriginWorld) lineOriginWorld .lerp(lineTerminusWorld, 0.5, controlPointWorld) .add(controlPointPerturbation) } function transformPoints() { camera.worldToScreen(textOriginWorld, textOriginScreen) camera.worldToScreen(lineOriginWorld, lineOriginScreen) camera.worldToScreen(lineTerminusWorld, lineTerminusScreen) camera.worldToScreen(controlPointWorld, controlPointScreen) camera.worldToScreen(worldPosition, worldPositionScreen) } let inDomain = false function draw() { // Activation/deactivation threshold logic if ( deactivationThreshold && domainTransform && Math.abs(domainTransform.x - deactivationThreshold) < 0.1 ) { self.destroy() } if ( activationThreshold && domainTransform && Math.abs(domainTransform.x - activationThreshold) < 0.1 ) activationThresholdMet = true if (activationThreshold && !activationThresholdMet) return // Draw based on whether we are within the given domain if ( domainTransform && (domainTransform.x < domain[0] || domainTransform.x > domain[1]) ) return if (globalScope.running && !drawIfRunning && !world.level.isCutscene) { return } const scalar = camera.worldToScreenScalar() calculatePoints() transformPoints() ctx = screen.ctx ctx.strokeStyle = color ctx.lineWidth = scalar / 15 ctx.lineCap = 'round' // ctx.beginPath() // ctx.fillStyle = 'green' // ctx.arc(worldPositionScreen.x, worldPositionScreen.y, 4, 0, TAU) // ctx.fill() ctx.beginPath() ctx.moveTo(lineOriginScreen.x, lineOriginScreen.y) ctx.quadraticCurveTo( controlPointScreen.x, controlPointScreen.y, lineTerminusScreen.x, lineTerminusScreen.y, ) ctx.stroke() ctx.fillStyle = color ctx.textAlign = align ctx.textBaseline = baseline ctx.font = '1px ' + font ctx.save() ctx.translate(textOriginScreen.x, textOriginScreen.y) ctx.scale(size * scalar, size * scalar) ctx.fillText(content, 0, 0) ctx.restore() } return self.mix({ transform, domain, awake, tick, draw, }) } FILE: Types_Entities_Sprite.js function Sprite(spec = {}) { const { self, screen, camera, assets } = Entity(spec, 'Sprite') const transform = Transform(spec, self) let { asset, image, graph, parent, flipX = false, flipY = false, globalScope, anchored = false, sloped = false, offset = Vector2(), opacity = 1, speech, fixed = false, speechScreen, size, world, sky = null, rotatingSpeed = 0, angularVelocity = 0, } = spec const velocity = spec.velocity ? Vector2(spec.velocity) : Vector2() const origin = Vector2(spec) const worldPosition = Vector2() const pos = Vector2() const sizer = Vector2() if (spec.offset) offset = Vector2(spec.offset) if (flipX == '*') flipX = Math.random() < 0.5 if (flipY == '*') flipY = Math.random() < 0.5 if (!spec.offset && anchored) offset.y = 1 if (!spec.offset && spec.y && anchored) offset.y += spec.y const ctx = screen.ctx const slopeTangent = Vector2() if (asset) { if (asset.includes('*')) { const assetSearch = new RegExp( `${asset.split('.')[1]?.split('*')[0]}_[0-9]+`, ) let possibleSprites = Object.keys(assets.images).filter((v) => assetSearch.test(v), ) asset = 'images.' + possibleSprites[Math.floor(Math.random() * possibleSprites.length)] } image = _.get(assets, asset, $('#error-sprite')) } function onRequestAssetsPass(requestAssets) { // TODO: Either use Proxy on this pass to collect // all needed assets (then wait for those which haven't loaded yet). // Or pass callback for when assets finish loading (Promise or callback?) if (asset) { if (asset.includes('*')) { const assetSearch = new RegExp( `${asset.split('.')[1]?.split('*')[0]}_[0-9]+`, ) let possibleSprites = Object.keys(assets.images).filter((v) => assetSearch.test(v), ) asset = 'images.' + possibleSprites[Math.floor(Math.random() * possibleSprites.length)] } requestAssets([asset], ({ asset }) => { image = asset ?? $('#error-sprite') }) } } if (speech) { if (!_.isArray(speech)) speech = [speech] for (s of speech) { if (_.isString(s)) s = { content: s } Speech({ parent: self, globalScope, x: size * offset.x, y: size * offset.y, drawOrder: LAYERS.speech, screen: speechScreen, ...s, }) } } function tick() { origin.x += velocity.x * tickDelta origin.y += velocity.y * tickDelta transform.rotation += angularVelocity * tickDelta if (anchored) { transform.x = origin.x transform.y = graph.sample('x', transform.x) } if (sloped) { slopeTangent.x = 1 slopeTangent.y = graph.sampleSlope('x', transform.x) slopeTangent.normalize() let angle = Math.asin(slopeTangent.y) transform.rotation = angle } if (rotatingSpeed != 0) { transform.rotation += tickDelta * rotatingSpeed } worldPosition.set() transform.transformPoint(worldPosition) } function drawLocal() { ctx.globalAlpha = opacity ctx.scale(flipX ? -1 : 1, flipY ? -1 : 1) ctx.drawImage( image, -size / 2 + (offset.x * size) / 2, -size / 2 - (offset.y * size) / 2, size, size, ) ctx.globalAlpha = 1 } function draw() { // If it is part of the background, it is transformed if (fixed) { if (sky.height / sky.width >= screen.height / screen.width) { let adjustmentY = (screen.width * sky.height) / sky.width pos.set( spec.x * screen.width, spec.y * adjustmentY + (screen.height - adjustmentY) / 2, ) sizer.set(screen.width / sky.width, screen.width / sky.width) } else { let adjustmentX = (screen.height * sky.width) / sky.height pos.set( spec.x * adjustmentX + (screen.width - adjustmentX) / 2, spec.y * screen.height, ) sizer.set(screen.height / sky.height, screen.height / sky.height) } // Also it needs to be rotated differently ctx.save() ctx.translate( pos.x + (size * sizer[0]) / 2, pos.y + (size * sizer[0]) / 2, ) ctx.rotate(transform.rotation) screen.ctx.drawImage( image, (-size * sizer[0]) / 2, (-size * sizer[1]) / 2, size * sizer[0], size * sizer[1], ) ctx.restore() } else if ( worldPosition.x + size > camera.lowerLeft.x && worldPosition.x - size < camera.upperRight.x ) { camera.drawThrough(ctx, drawLocal, transform) } } return self.mix({ transform, get opacity() { return opacity }, set opacity(o) { opacity = o }, get size() { return size }, set size(v) { size = v }, tick, draw, get flipX() { return flipX }, set flipX(v) { flipX = v }, get flipY() { return flipY }, set flipY(v) { flipY = v }, }) } FILE: Types_Entities_Text.js function Text(spec) { const { self, screen } = Entity(spec, 'Text') const transform = Transform(spec) const { camera, size = 1, fill = '#222', stroke = false, align = 'center', baseline = 'middle', font = 'Edu QLD Beginner', } = spec let { content = 'Hello' } = spec const ctx = screen.ctx function drawLocal() { ctx.textAlign = align ctx.textBaseline = baseline ctx.scale(size, size) ctx.font = `1px ${font}` if (fill) { ctx.fillStyle = fill ctx.fillText(content, 0, 0) } if (stroke) { ctx.fillStyle = stroke ctx.strokeText(content, 0, 0) } } function draw() { camera.drawThrough(ctx, drawLocal, transform) } return self.mix({ draw, }) } FILE: Types_Entities_Tip.js /** * Create a text bubble which can be appended to a DOM Element * @param {string} spec.content text in the bubble * @param {string} spec.domSelector parent dom query selector * @param {string} spec.place either top-right top-left bottom-right bottom-left * @param {object} spec.style extra styles to be added like top/bottom/color * @returns */ function Tip(spec) { const { self } = Entity(spec, 'Tip') let { visible = true, index=0, tipCompleted, content = 'Hello', domSelector, place, destroyOnClick = true, style = {}, } = spec const domElement = $(domSelector) const helperBubble = document.createElement('div') helperBubble.className = `helper-bubble ${place}` helperBubble.innerHTML = content helperBubble.style.display = visible ? 'block' : 'none' Object.assign(helperBubble.style, style) domElement.appendChild(helperBubble) function toggleVisible(newVis = !visible) { visible = newVis helperBubble.style.display = visible ? 'block' : 'none' } function awake() { toggleVisible(true) } function destroy() { helperBubble.remove() tipCompleted() } function refreshDOM(){ index-=1 style.visibility = visible ? 'visible' : 'hidden' if (index == 0){ style= {visibility: 'visible'} } else{ style= {visibility: 'hidden'} } helperBubble.innerHTML = content Object.assign(helperBubble.style, style) } if (destroyOnClick) domElement.onmousedown = () => { self.destroy() } function onToggleMap(navigating) { toggleVisible(!navigating) } return self.mix({ toggleVisible, helperBubble, onToggleMap, refreshDOM, destroy, awake, }) } FILE: Types_Entities_Trail.js function Trail(spec) { const { self, camera, screen, ctx } = Entity(spec, 'Trail') const transform = Transform(spec, self) let { maxTime = 3, strokeStyle = 'rgba(255, 255, 255, 0.5)', alpha = 1, lineWidth = 0.3, } = spec let sampleCount = Math.ceil(maxTime / self.tickDelta) let sampleIndex = 0 const pathPoint = Vector2() const worldPoints = [] const worldPointsUpper = [] const worldPointsLower = [] for (let i = 0; i < sampleCount; i++) { worldPoints.push(Vector2()) worldPointsUpper.push(Vector2()) worldPointsLower.push(Vector2()) } function awake() { // reset() } function tick() { // Move to next sample, wrapping at sampleCount sampleIndex = (sampleIndex + 1) % sampleCount const worldPoint = worldPoints[sampleIndex] const worldPointUpper = worldPointsUpper[sampleIndex] const worldPointLower = worldPointsLower[sampleIndex] transform.transformPoint(worldPoint.set()) transform.transformPoint(worldPointUpper.set(0, 1)) transform.transformPoint(worldPointLower.set(0, -1)) } function draw() { ctx.beginPath() const screenScalar = camera.worldToScreenScalar() for (let i = 0; i < sampleCount; i++) { // Count forwards from oldest point const index = (sampleIndex + i + 1) % sampleCount const worldPoint = worldPoints[index] const worldPointUpper = worldPointsUpper[index] let progress = i / (sampleCount - 1) worldPoint.lerp(worldPointUpper, (progress * lineWidth) / 2, pathPoint) camera.worldToScreen(pathPoint) if (i == 0) ctx.moveTo(pathPoint.x, pathPoint.y) else ctx.lineTo(pathPoint.x, pathPoint.y) } // Traverse path in opposite direction, stopping just short of earliest point for (let i = sampleCount - 1; i >= 0; i--) { const index = (sampleIndex + i + 1) % sampleCount const worldPoint = worldPoints[index] const worldPointLower = worldPointsLower[index] let progress = i / (sampleCount - 1) worldPoint.lerp(worldPointLower, (progress * lineWidth) / 2, pathPoint) camera.worldToScreen(pathPoint) ctx.lineTo(pathPoint.x, pathPoint.y) } ctx.lineWidth = lineWidth * screenScalar ctx.fillStyle = strokeStyle ctx.lineJoin = 'round' ctx.lineCap = 'round' ctx.fill() } // function draw() { // ctx.beginPath() // const screenScalar = camera.worldToScreenScalar() // for (let i = 0; i < sampleCount; i++) { // // Count *backwards* from current point // const index = (sampleIndex-i+sampleCount)%sampleCount // const worldPoint = worldPoints[index] // const worldPointUpper = worldPointsUpper[index] // const worldPointLower = worldPointsLower[index] // const screenPoint = screenPoints[index] // const screenPointUpper = screenPointsUpper[index] // const screenPointLower = screenPointsLower[index] // camera.worldToScreen(worldPoint, screenPoint) // camera.worldToScreen(worldPointUpper, screenPointUpper) // camera.worldToScreen(worldPointLower, screenPointLower) // if (i == 0) // ctx.moveTo(screenPoint.x, screenPoint.y) // else // ctx.lineTo(screenPoint.x, screenPoint.y) // } // ctx.lineWidth = lineWidth*screenScalar // ctx.strokeStyle = strokeStyle // ctx.lineJoin = 'round' // ctx.lineCap = 'round' // ctx.stroke() // } function reset() { for (point of worldPoints) { transform.transformPoint(point.set()) } for (point of worldPointsUpper) { transform.transformPoint(point.set()) } for (point of worldPointsLower) { transform.transformPoint(point.set()) } } return self.mix({ transform, awake, tick, draw, reset, }) } FILE: Types_Entities_VolcanoSunsetShader.js function VolcanoSunsetShader(spec) { const { self, screen, assets } = Entity(spec, 'VolcanoSunsetShader') const { getSledderPosition, defaultExpression } = spec const ctx = screen.ctx const transform = Transform(spec, self) const quad = VolcanoSunsetQuad({ defaultExpression, assets, getSledderPosition, }) function draw() { quad.render() ctx.drawImage(quad.localCanvas, 0, 0, screen.width, screen.height) } function tick() { quad.update() } function resize(width, height) { quad.resize(width, height) } return _.mixIn(self, { draw, resize, tick, transform, }) } FILE: Types_Entities_Walker.js function Walker(spec) { const { self, camera, screen, world } = Entity(spec, 'Walker') const transform = Transform(spec, self) let { asset = 'images.benny_float', size = 2, sprite, graph, speech, globalScope, walkers = [], following = null, followDistance = 2, domainTransform = null, bobSpeed = 0.6, hasDarkMode = false, darkModeOpacity = 0, speechScreen, victoryX = null, levelCompleted, range = [NINF, PINF], flipX = false, followFlip = true, transition = null, backflip = null, } = spec if (!_.isArray(walkers)) walkers = [walkers] s = sprite sprite = Sprite({ parent: self, drawOrder: self.drawOrder, asset, size, globalScope, y: size / 2, speech, speechScreen, flipX, ...sprite, }) const darkSprite = hasDarkMode ? Sprite({ parent: self, asset: asset + '_dark', size, globalScope, y: size / 2, opacity: darkModeOpacity, drawOrder: 1000, flipX, ...s, }) : null const clickable = following ? null : Clickable({ entity: self, space: 'frame', layer: spec.layer, }) walkers = _.map(walkers, (v) => Walker({ drawOrder: self.drawOrder, ...v, parent: self.parent, domainTransform: domainTransform || transform, camera, following: self, globalScope, screen, speechScreen, graph, hasDarkMode, }), ) let walking = false let walkSign = 1 let walkSpeed = 1 let oldWalkSign = walkSign let floatCycle = 0 let floatCycleSpeed = bobSpeed let floatBob = 0 let floatBobCoefficient = 0.12 let floatBobHeight = 0.2 let floatOffset = -0.1 let jumpProgress = 0 const mousePointFrame = Vector2() const mousePoint = Vector2() let completed = false function awake() { alignToGround() } function tick() { if (followFlip && walking && !following) { camera.frameToWorld(mousePointFrame, mousePoint) transform.invertPoint(mousePoint) if (oldWalkSign == walkSign || Math.abs(mousePoint.x) > 0.5) { transform.position.x += walkSign * walkSpeed * self.tickDelta sprite.flipX = walkSign == -1 if (darkSprite) darkSprite.flipX = walkSign == -1 } } else if (followFlip && following) { if (transform.x - following.transform.x > followDistance) { transform.x = following.transform.x + followDistance sprite.flipX = true if (darkSprite) darkSprite.flipX = true } else if (following.transform.x - transform.x > followDistance) { transform.x = following.transform.x - followDistance sprite.flipX = false if (darkSprite) darkSprite.flipX = false } } transform.position.x = _.clamp(transform.position.x, range[0], range[1]) if ( backflip && transform.position.x > backflip[0] && transform.position.x < backflip[1] ) { jumpProgress = math.lerp(jumpProgress, 1, 0.05) sprite.transform.rotation += globalScope.dt * 10 } else { jumpProgress = math.lerp(jumpProgress, 0, 0.12) sprite.transform.rotation = math.lerp(sprite.transform.rotation, 0, 0.12) } while (sprite.transform.rotation > PI) sprite.transform.rotation -= TAU while (sprite.transform.rotation < -PI) sprite.transform.rotation += TAU alignToGround() if (victoryX != null && !completed) { if (Math.sign(victoryX - spec.x) == Math.sign(transform.x - victoryX)) { completed = true levelCompleted(true) } } } function draw() {} function alignToGround() { sprite.transform.y = math.lerp(size / 2, size / 2 + 1, jumpProgress) const groundHeight = graph.sample('x', transform.position.x) floatCycle -= self.tickDelta * floatCycleSpeed if (floatCycle <= 0) floatCycle = 1 floatBob = math.lerp(floatBob, floatCycle, floatBobCoefficient) transform.position.y = groundHeight + floatBob * floatBobHeight + floatOffset } function mouseDown(point) { oldWalkSign = walkSign walkSign = Math.sign(point.x) mousePointFrame.set(point) walking = true } function mouseMove(point) { oldWalkSign = walkSign walkSign = Math.sign(point.x) walking = true mousePointFrame.set(point) } function mouseUp() { walking = false } document.addEventListener('keydown', (e) => { if (e.key == 'ArrowRight') { oldWalkSign = walkSign walkSign = Math.sign(1) walking = true return } else if (e.key == 'ArrowLeft') { oldWalkSign = walkSign walkSign = Math.sign(-1) walking = true } }) document.addEventListener('keyup', (e) => { if (e.key == 'ArrowRight' || e.key == 'ArrowLeft') walking = false }) return self.mix({ transform, domainTransform: domainTransform || transform, clickable, set darkModeOpacity(o) { if (darkSprite) darkSprite.opacity = o else throw `Tried to set darken opacity of undarkenable Sprite` }, get hasDarkMode() { return hasDarkMode }, get walkers() { return walkers }, get transition() { return transition }, set transition(v) { transition = v }, mouseDown, mouseMove, mouseUp, sprite, speech, awake, tick, draw, }) } FILE: Types_Entities_Water.js function Water(spec) { const { self, screen, camera } = Entity(spec, 'Water') let { size, lava = false } = spec const waterQuad = (lava ? LavaQuad : WaterQuad)({ assets, }) const ctx = screen.ctx const transform = Transform(spec, self) function tick() { waterQuad.update() } function drawLocal() { waterQuad.draw() ctx.drawImage(waterQuad.canvas, 0, 0, size, size) } function draw() { camera.drawThrough(ctx, drawLocal, transform) } return _.mixIn(self, { tick, draw, transform, waterQuad, set size(_size) { size = _size }, }) } FILE: Types_Entities_World.js let assets, globalScope let arrowsDrawn = 0, _arrowsDrawn, originalAssets function World(spec) { const self = Entity(spec, 'World') const { ui, screen, levelData, requestDraw, tickDelta, version } = spec const playerStorage = PlayerStorage() let running = false let runTime = 0 let completionTime = null const quads = {} globalScope = { customT: 0, get t() { return running ? runTime : globalScope.customT }, get T() { return runTime }, set runTime(_runTime) { runTime = _runTime }, timescale: 1, get dt() { return tickDelta * globalScope.timescale }, lerp: (a, b, t) => { t = math.clamp01(t) return a * (1 - t) + b * t }, get running() { return running }, get completionTime() { return completionTime }, } let navigating = false let editing = false let sampleDensitySetting let terrainLayersSetting const clickableContext = ClickableContext({ entity: self, screen /* Pass screen for downsampling ratio */, }) let backgroundMusicAsset = null let backgroundMusicSound = null let navigator let level let levelDatum let levelBubble // Preprocess all level datums at start (TODO: Make lazy?) levelData.map(preprocessDatum) // Get first level datum, load general + level-specific assets, // wait and then set level originalAssets = _.cloneDeep(spec.assets) assets = Assets() const urlData = getUrlData() const firstLevelNick = getFirstLevelNick(urlData) const firstLevelDatum = getLevelDatum(firstLevelNick, urlData) if (!firstLevelDatum) { // TODO: Clean up error handling alert(`Level specified ('${firstLevelNick}') doesn\'t exist :(`) } _.merge(spec.assets, firstLevelDatum.assets) // Loading veil, loading bar handled by assets assets.load(spec.assets, assetsComplete) function start() { // Only show the level info if we're not in debug if (!window.location.hostname.endsWith('sinerider.com')) { ui.levelInfoDiv.setAttribute('hide', false) ui.hideLevelInfoButton.addEventListener('click', hideLevelInfoClicked) } } function loadWebGLQuads() { quads.water = WaterQuad(assets) quads.sunset = ConstantLakeSunsetQuad('(sin(x)-(y-2)*i)*i/2', assets) quads.volcanoSunset = VolcanoSunsetQuad( '((sin(x)*i)/2)+(x/4)+((y*i)/5)', assets, ) quads.lava = LavaQuad(assets) } function renderEntityToImage( width, height, entityGenerator, entityDatum = {}, fov = 1, ) { const canvas = document.createElement('canvas') canvas.width = width * 2 canvas.height = height * 2 const screen = Screen({ canvas, element: canvas, alpha: true, }) const camera = Camera({ screen, fov, }) const entity = entityGenerator({ ...entityDatum, camera, screen, }) entity.sendEvent('tick') entity.sendEvent('awake') entity.sendEvent('start') entity.sendEvent('tick') entity.sendEvent('draw') entity.destroy() return canvas } function renderAndAddSpawnerButtons() { // Add path goal const pathGoalImage = renderEntityToImage( 90, 45, PathGoal, { parent: self, assets, globalScope, drawOrder: LAYERS.goals, world, goalCompleted: () => {}, goalFailed: () => {}, goalDeleted: () => {}, x: -2, }, 1.5, ) ui.editorSpawner.addPath.appendChild(pathGoalImage) // Add fixed goal const fixedGoalImage = renderEntityToImage( 64, 45, FixedGoal, { parent: self, assets, globalScope, drawOrder: LAYERS.goals, world, goalCompleted: () => {}, goalFailed: () => {}, goalDeleted: () => {}, }, 0.6, ) ui.editorSpawner.addFixed.appendChild(fixedGoalImage) // Add dynamic goal const dynamicGoalImage = renderEntityToImage( 64, 45, DynamicGoal, { parent: self, // HACK: Pass height to dynamic goal via graph graph: { sample: () => -0.5, sampleSlope: () => 0, }, assets, globalScope, drawOrder: LAYERS.goals, world, goalCompleted: () => {}, goalFailed: () => {}, goalDeleted: () => {}, y: 1, }, 0.6, ) ui.editorSpawner.addDynamic.appendChild(dynamicGoalImage) } function tick() { if (self.drawArrayIsUnsorted) self.sortDrawArray() if ( window.innerHeight != screen.height || window.innerWidth != screen.width ) screen.resize() if (running) runTime += tickDelta } function draw() { levelBubblesDrawn = 0 _arrowsDrawn = arrowsDrawn arrowsDrawn = 0 } function hideLevelInfoClicked() { ui.levelInfoDiv.setAttribute('hide', true) } function getSavedLatexFromURL(urlData) { const isPuzzle = urlData?.isPuzzle ?? false if (isPuzzle) { return levelDatum.expressionOverride ? levelDatum.expressionOverride : levelDatum.defaultExpression } else { return urlData?.savedLatex ?? playerStorage.getLevel(nick)?.savedLatex } } function preprocessDatum(datum) { // Reuse datum across levels/bubbles if (datum._preprocessed) return // Add biome defaults + required assets const biomeName = datum.biome ?? 'westernSlopes' const biome = BIOMES[biomeName] _.defaults(datum, biome) if (biome.assets) _.merge(datum.assets, biome.assets) // Expand `dialogue` array to individual speech objects const dialogue = datum.dialogue const walkers = datum.walkers ?? [] const allWalkers = [ ...walkers, ..._.flatten( walkers.map((v) => _.isArray(v.walkers) ? v.walkers : _.isObject(v.walkers) ? [v.walkers] : [], ), ), ] if (dialogue) { for (const line of dialogue) { if (!line.speaker) { throw new Error( `Line of dialogue '${line.content}' has no speaker specified in level '${datum.name}'`, ) } const speaker = allWalkers.find((walker) => walker.name == line.speaker) if (!speaker) { throw new Error( `Tried to add line of dialogue for unknown speaker '${line.speaker}' in level '${datum.name}'`, ) } // Add default length and gap values if (_.isUndefined(line.domain)) { if (_.isUndefined(line.length)) line.length = 3 if (_.isUndefined(line.gap)) line.gap = 2 } // Add default positions if (line.speaker == 'Ada') { if (_.isUndefined(line.x)) line.x = 0 if (_.isUndefined(line.y)) line.y = 0.7 } else if (line.speaker == 'Jack') { if (_.isUndefined(line.x)) line.x = 0 if (_.isUndefined(line.y)) line.y = 0.8 } if (!speaker.speech) speaker.speech = [] speaker.speech.push(line) } let previous = null for (const line of dialogue) { if (line.length) { if (line.domain) { throw new Error( `Tried to set length of line of dialogue in level '${datum.name}' with existing domain`, ) } // Hard-coded assumption that the speaker we care about is the first one. // We only support a single top-level walker anyway… const speaker = walkers[0] // By default start at speaker x const endOfLastLine = (previous && previous.domain && previous.domain[1]) || speaker.x const start = endOfLastLine + line.gap const domain = [start, start + line.length] line.domain = domain } previous = line } } // Also split up processed speech bubbles with \n for (const walker of walkers) { if (!walker.speech) continue const newLines = [] const removeIndices = [] for (let i = 0; i < walker.speech.length; i++) { const speech = walker.speech[i] const content = speech.content if (!content) continue let lines = content.split('\n') if (lines.length > 0) { removeIndices.push(i) let previous = null for (let line of lines) { const newSpeech = _.cloneDeep(speech) newSpeech.content = line newSpeech.distance = ((previous && previous.distance) || lines.length * 0.4 + 1.1) - 0.4 if (!newSpeech.content) console.log( 'adding new lines', newLines.filter((line) => line.content == null), newSpeech, ) newLines.push(newSpeech) previous = newSpeech } } } for (let i = removeIndices.length; i--; i >= 0) { walker.speech.splice(removeIndices[i], 1) } walker.speech = walker.speech.concat(newLines) } datum._preprocessed = true } function getLevelDatum(nick, urlData = null) { let levelDatum isPuzzle = urlData?.isPuzzle ?? false // Ensure URL Data is, at minimum, an empty object instead of an unqueryable null. Needs to be explicitly redefined because sometimes null is explicitly passed. if (urlData == null) urlData = {} if (isPuzzle) { levelDatum = generatePuzzleLevel(urlData) savedLatex = levelDatum.expressionOverride ? levelDatum.expressionOverride : levelDatum.defaultExpression } else { if (nick == 'RANDOM') { levelDatum = generateRandomLevel() } else if (nick == 'CUSTOM_LEVEL') { // console.log('Custom Level URL Data:', urlData) levelDatum = { nick, goals: urlData.goals ?? [], sledders: urlData.sledders ?? [], defaultExpression: urlData.defaultExpression ?? '0', biome: urlData.biome ?? 'westernSlopes', } // Load assets from the specific biome specified in the URL data if (urlData.biome) _.mixIn(levelDatum, BIOMES[urlData.biome]) // console.log('Custom Level Datum:', levelDatum) } else if (nick == 'LEVEL_EDITOR') { // console.log('Level Editor URL Data:', urlData) levelDatum = { nick, } // Mix in EDITOR level properties from editor.js _.mixIn(levelDatum, EDITOR[0]) // Mix in any overrides contained in url data _.mixIn(levelDatum, urlData) // Mix in any specific properties associated with this biome if (urlData.biome) _.mixIn(levelDatum, BIOMES[urlData.biome]) // But still guarantee that all assets required for all biomes are loaded to editor levelDatum.assets = EDITOR[0].assets // console.log('Level Editor Datum:', levelDatum) } else { levelDatum = _.find(levelData, (v) => v.nick == nick) } } return levelDatum } function getUrlData() { const url = new URL(location) if (url.search) { try { const urlData = JSON.parse( LZString.decompressFromBase64(url.search.slice(1)), ) return urlData } catch (err) { console.error('Error loading url:', err) // TODO: Maybe switch to modal alert('Sorry, this URL is malformed :(') } } return null } function getFirstLevelNick(urlData) { if (IS_DEVELOPMENT && DEBUG_LEVEL_NICK) return DEBUG_LEVEL_NICK if (urlData?.nick) return urlData.nick if (_.endsWith(location.href, '#random')) return 'RANDOM' else if ( playerStorage.activeLevel && _.find(levelData, (v) => v.nick == playerStorage.activeLevel) ) return playerStorage.activeLevel else return levelData[0].nick } function assetsComplete() { // Show loading veil again for click ui.loadingVeil.setAttribute('hide', false) ui.loadingVeilString.innerHTML = 'click to begin' ui.loadingVeil.addEventListener('click', loadingVeilClicked) const c = playerStorage.getCompletedLevels().length const total = levelData.length const percent = Math.round((100 * c) / total) const congrats = percent < 100 ? '' : 'well done, ' if (c) { ui.resetSolutionsString.setAttribute('hide', false) ui.resetSolutionsString.innerHTML = `You have completed ${c}/${total} levels (${congrats}${percent}%). Reset?` ui.resetSolutionsString.addEventListener('click', (event) => { ui.resetProgressConfirmationDialog.showModal(), event.stopPropagation() }) ui.resetProgressConfirmButton.addEventListener('click', (event) => { resetSavedSolutions() ui.resetProgressConfirmationDialog.close() }) ui.resetProgressCancelButton.addEventListener('click', (event) => { ui.resetProgressConfirmationDialog.close() event.stopPropagation() }) } } function loadingVeilClicked() { ui.loadingVeil.setAttribute('hide', true) // TODO: Encapsulate loading veil click vs. loading ui.loadingVeilString.innerHTML = '' ui.loadingVeil.removeEventListener('onclick', loadingVeilClicked) navigator = Navigator({ ui, screen, assets, levelData, getEditing, setLevel, playerStorage, active: false, parent: self, world: self, drawOrder: LAYERS.navigator, }) navigator.active = false const startingBubble = navigator.getBubbleByNick(firstLevelNick) navigator.initialBubble = startingBubble // Set misc. page settings (e.g. volume) initPageState() renderAndAddSpawnerButtons() setLevel(firstLevelNick, urlData) } function resetSavedSolutions() { ui.resetSolutionsString.remove() playerStorage.clear() } function assetsProgress(progress, total) { const percent = Math.round((100 * progress) / total) ui.loadingProgressBar.style.width = `${percent}%` } function setLevel(nick, urlData = null) { if (level) level.destroy() emitEvent('setLevel', { event_category: nick.toLowerCase().startsWith('puzzle_') ? 'daily' : 'campaign', value: nick, }) // Don't bother recalculating level datum if first levelDatum = nick === firstLevelNick ? firstLevelDatum : getLevelDatum(nick, urlData) levelBubble = navigator.getBubbleByNick(nick) let savedLatex const isPuzzle = urlData?.isPuzzle ?? false if (isPuzzle) { savedLatex = levelDatum.expressionOverride ? levelDatum.expressionOverride : levelDatum.defaultExpression } else { savedLatex = urlData?.savedLatex ?? playerStorage.getLevel(nick)?.savedLatex } const generator = { CONSTANT_LAKE: ConstantLake, VOLCANO: Volcano, DESERT: Desert, LEVEL_EDITOR: LevelEditor, CREDITS: Credits, }[levelDatum.nick] || Level const completed = playerStorage.getLevel(nick)?.completed || urlData?.completed || false level = generator({ ui, screen, assets, parent: self, name: levelDatum.nick, datum: levelDatum, globalScope, active: !navigating, levelCompleted, playBackgroundMusic, tickDelta, isBubbleLevel: false, world: self, quads, storage: playerStorage, savedLatex, completed, hasBeenCompleted: completed, urlData, }) level.playOpenMusic() level.restart() if (ui.levelText) { ui.levelText.value = levelDatum.name ui.levelButtonString.innerHTML = levelDatum.name ui.levelInfoNameStr.innerHTML = levelDatum.name ui.levelInfoNickStr.innerHTML = levelDatum.nick } setNavigating(false) } function setNavigating(_navigating) { // TODO: Either collect assets needed for level bubbles (backgrounds) // load and wait, or generate small 512x512 backgrounds for level bubbles // (GitHub action?) navigating = _navigating if (navigating) self.sendEvent('onToggleMap', [_navigating]) level.active = !navigating navigator.active = navigating if (!navigating) self.sendEvent('onToggleMap', [_navigating]) ui.controlBar.setAttribute('hide', navigating) ui.navigatorFloatingBar.setAttribute('hide', !navigating) // ui.topBar.setAttribute('hide', navigating) if (navigating) { navigator.revealHighlightedLevels(levelDatum.nick) navigator.refreshBubbles() canvas.classList.add('map') } else { // ui.variablesBar.setAttribute('hide', true) canvas.classList.remove('map') navigator.showAll = false // if (navigator.showAllUsed) ui.showAllButton.setAttribute('hide', false) } } function makeTwitterSubmissionUrl() { const linkToPuzzle = `https://sinerider.com/puzzle/${levelDatum.nick}` const solution = ui.mathField.getPlainExpression().replace(/\s/g, '') const twitterPrefill = `#${ levelDatum.nick } My solution for the #sinerider puzzle of the day took ${ Math.round(timeTaken() * 10) / 10 } seconds & ${charCount()} characters ${solution} Try solving it yourself: ${linkToPuzzle}` return `https://twitter.com/intent/tweet?text=${encodeURIComponent( twitterPrefill, )}` // @msw: While implementing a quick-fix, the following is a noop return ( 'https://twitter.com/intent/tweet?text=' + encodeURIComponent( 'I did it! #sinerider ' + levelDatum.nick + ' ' + level.currentLatex.replace(/\s/g, ''), ) ) } function openRedditModal() { ui.redditOpenModal.setAttribute('hide', false) ui.redditOpenCommand.setAttribute('hide', false) $('#submit-reddit-score-subreddit').setAttribute('hide', false) let text = `#sinerider ${levelDatum.nick} \`${level.currentLatex.replace( /\s/g, '', )}\`` $('#submit-reddit-score-subreddit').setAttribute( 'href', `https://sinerider.com/reddit/${levelDatum.nick}`, ) ui.redditOpenCommand.setAttribute('value', text) // It would be nice to use this, but it doesn't work for whatever reason // navigator.clipboard.writeText(text) ui.redditOpenCommand.select() document.execCommand('copy') } function charCount() { return ui.mathFieldStatic.latex().length } function timeTaken() { return Math.round(runTime * 100) / 100 } function levelCompleted(soft = false) { setCompletionTime(runTime) emitEvent('levelCompleted', { event_category: level.name.toLowerCase().startsWith('puzzle_') ? 'daily' : 'campaign', value: level.name, }) ui.timeTaken.innerHTML = timeTaken() + ' second' + (timeTaken() === 1 ? '' : 's') ui.charCount.innerHTML = charCount() + ' character' + (charCount() === 1 ? '' : 's') if (soft) { nextLevel(2.5) } else { ui.victoryBar.setAttribute('hide', false) ui.expressionEnvelope.setAttribute('hide', true) // Disabled this behavior because it's just too annoying when trying to show the game to people, and it kinda feels like a bug. // So now the "Show All" button will always be visible. // ui.showAllButton.setAttribute('hide', true) } const isPuzzle = true if ('isPuzzle' in levelDatum && levelDatum['isPuzzle']) { // hide next button ui.nextButton.setAttribute('hide', true) const puzzleMsgs = document.getElementsByClassName('puzzle-msg') for (let idx = 0; idx < puzzleMsgs.length; idx++) { puzzleMsgs[idx].setAttribute('hide', false) } $('#twitter-submission-url').setAttribute( 'href', makeTwitterSubmissionUrl(), ) // Hide reddit for the time being $('#submit-reddit-score').onclick = openRedditModal ui.redditOpenCloseButton.onclick = () => ui.redditOpenModal.setAttribute('hide', true) } else { // TODO: Causing error… commenting for now // ui.redditOpenCommand // ?.setAttribute( // 'hide', // false, // )('#submit-reddit-score-subreddit') // ?.setAttribute('hide', false) ui.submitTwitterScoreDiv?.setAttribute('hide', true) ui.submitRedditScoreDiv?.setAttribute('hide', true) } levelBubble?.complete() } function transitionNavigating(_navigating, duration = 1, cb) { ui.veil.setAttribute('style', `transition-duration: ${duration}s;`) ui.veil.setAttribute('hide', false) if (_navigating) self.sendEvent('onLevelFadeOut', [_navigating, duration]) else self.sendEvent('onMapFadeOut', [_navigating, duration]) setTimeout(() => { // HACK: to fix camera flicker setTimeout(() => { ui.veil.setAttribute('style', `transition-duration: ${1}s;`) ui.veil.setAttribute('hide', true) }, 100) setNavigating(_navigating) if (_navigating) self.sendEvent('onMapFadeIn', [_navigating, duration]) else self.sendEvent('onLevelFadeIn', [_navigating, duration]) if (cb) cb() }, duration * 1000) } function nextLevel(transitionDuration = 1) { if ('isPuzzle' in levelDatum && levelDatum['isPuzzle']) { ui.victoryBar.setAttribute('hide', true) stopRunning(false) level.restart() } else { transitionNavigating(true, transitionDuration, () => { stopRunning(false) }) } } function onClickNextButton() { assets.sounds.next_button.play() nextLevel() } function getEditing() { return editing } function setEditing(_editing) { editing = _editing } function startRunning(playSound = true, hideNavigator = true) { running = true setCompletionTime(null) ui.mathField.blur() ui.menuBar.setAttribute('hide', true) ui.runButton.setAttribute('hide', true) ui.stopButton.setAttribute('hide', false) if (hideNavigator) ui.navigatorButton.setAttribute('hide', true) ui.resetButton.setAttribute('hide', true) ui.tryAgainButton.setAttribute('hide', true) if (playSound) assets.sounds.start_running.play() self.sendEvent('startRunning', []) requestDraw() } function setCompletionTime(t) { completionTime = t ui.completionTime.innerHTML = t } function stopRunning(playSound = true) { runTime = 0 running = false setCompletionTime(null) ui.mathField.blur() ui.expressionEnvelope.setAttribute('disabled', false) ui.menuBar.setAttribute('hide', false) ui.victoryBar.setAttribute('hide', true) ui.controlBar.setAttribute('hide', navigating) ui.expressionEnvelope.setAttribute('hide', false) ui.runButton.setAttribute('hide', false) ui.tryAgainButton.setAttribute('hide', true) ui.stopButton.setAttribute('hide', true) ui.resetButton.setAttribute('hide', false) if (!level.name.startsWith('puzzle_')) ui.navigatorButton.setAttribute('hide', false) if (!navigating) { // HACK: Timed to avoid bug in Safari (at least) that causes whole page to be permanently offset when off-screen text input is focused setTimeout(() => ui.expressionText.focus(), 250) } if (playSound) assets.sounds.stop_running.play() self.sendEvent('stopRunning', []) requestDraw() } function toggleRunning() { if (running) stopRunning() else startRunning() } function generateRandomLevel() { const goals = [] // Skew towards 3-goal levels, with potential for as many as 7 const goalCount = _.random(_.random(2, 3), _.random(3, 7)) // Generate goals at random locations for (let i = 0; i < goalCount; i++) { let x = 0 let y = 0 const type = Math.random() < 1 / (goalCount + 1) ? 'dynamic' : 'fixed' do { x = _.random(-16, 16) y = _.random(-12, 12) } while ( _.find(goals, (v) => { const d = Math.sqrt(Math.pow(x - v.x, 2) + Math.pow(y - v.y, 2)) if (type == 'dynamic' && v.type == 'dynamic') { if (Math.abs(v.x - x) < 1.5) return true } else if (type == 'dynamic' || v.type == 'dynamic') { if (Math.abs(v.x - x) < 1.5) return true } else { if (d <= 1.9) return true } }) ) goals.push({ x, y, type, }) } // Apply ordering to goals { const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('') const ordered = Math.random() < 0.5 || _.filter(goals, (v) => v.type == 'dynamic').length > 1 const orderCount = !ordered ? 0 : Math.max(2, _.random(0, goalCount)) let i = 0 let a = [...goals] while (i < orderCount) { i++ a = _.shuffle(a) const v = a.pop() if (alphabet[0] == 'A' && i == orderCount) v.order = 'B' else if (Math.random() < 0.5) v.order = alphabet[0] else v.order = alphabet.shift() } } // Guarantee that there is never more than one unordered dynamic goal { let unorderedDynamicGoals = 0 for (goal of goals) { if (goal.type == 'dynamic') { if (unorderedDynamicGoals > 0) goal.type = 'fixed' unorderedDynamicGoals++ } } } const sledderCount = goalCount > 2 && Math.random() < 0.5 ? _.random(1, 2) : 1 const sledders = [] const sledderAssets = [ 'images.ada_sled', 'images.jack_sled', 'images.ada_jack_sled', ] for (let i = 0; i < sledderCount; i++) { let sledderX do { sledderX = _.random(-16, 16) } while ( _.find(goals, (v) => Math.abs(v.x - sledderX) < 1.5) || _.find(sledders, (v) => Math.abs(v.x - sledderX) < 3.5) ) sledders.push({ x: sledderX, asset: sledderCount == 1 ? _.sample(sledderAssets) : sledderAssets[i], }) } const biome = _.sample([ 'westernSlopes', 'valleyParabola', 'eternalCanyon', 'sinusoidalDesert', 'logisticDunes', 'hilbertDelta', ]) return { name: 'Random Level', nick: 'RANDOM', drawOrder: LAYERS.level, x: -10, y: 0, defaultExpression: '0', goals, sledders, biome, } } function generatePuzzleLevel(urlData) { return { isPuzzle: true, name: urlData.name, nick: urlData.nick, drawOrder: LAYERS.level, slider: urlData.slider, x: urlData.x, y: urlData.y, biome: urlData.biome, colors: urlData.colors, defaultExpression: urlData.defaultExpression, expressionOverride: urlData.expressionOverride, hint: urlData.hint, goals: urlData.goals, sledders: urlData.sledders, sprites: urlData.sprites, } } function playBackgroundMusic(datum, level) { let asset if (!_.isObject(datum)) datum = { asset: datum } asset = datum.asset if (asset == backgroundMusicAsset) return if (backgroundMusicSound != null) { backgroundMusicSound.fadeDestroy() backgroundMusicAsset = null backgroundMusicSound = null } if (asset == null) return backgroundMusicAsset = asset backgroundMusicSound = Sound({ asset, assets, level, name: 'Sound ' + asset, parent: self, loop: true, fadeOnNavigating: false, ...datum, }) } function onClickMapButton() { transitionNavigating(!navigating) assets.sounds.map_button.play() } function onResetConfirm() { level.restart() assets.sounds.restart_button.play() } function onMathFieldFocus() { self.sendEvent('mathFieldFocused') } function onMathFieldBlur() { self.sendEvent('mathFieldBlurred') } return self.mix({ start, tick, draw, globalScope, _startRunning: startRunning, _stopRunning: stopRunning, toggleRunning, nextLevel, setLevel, clickableContext, setNavigating, transitionNavigating, onClickMapButton, onResetConfirm, onClickNextButton, onMathFieldFocus, onMathFieldBlur, get navigator() { return navigator }, get running() { return running }, get editing() { return editing }, set editing(v) { setEditing(v) }, get level() { return level }, get navigating() { return navigating }, set navigating(v) { setNavigating(v) }, get terrainLayersSetting() { return terrainLayersSetting }, set terrainLayersSetting(v) { terrainLayersSetting = v if (level && level.graph) level.graph.terrainLayers = v }, set sampleDensitySetting(v) { sampleDensitySetting = v if (level && level.graph) level.graph.sampleCount = v }, get sampleDensitySetting() { return sampleDensitySetting }, }) } FILE: Types_InterFrameSampler.js /* Wraps two BufferSamplers representing a graph at two points in time, and samples between them */ function InterFrameSampler(spec) { const self = {} const { sampler, sampleCount, refreshPeriodT = 10, samplePadding = 10, useQuadraticInterpolation = true, } = spec let sampleTimeA const samplesA = BufferSampler(sampler, sampleCount) let sampleTimeB const samplesB = BufferSampler(sampler, sampleCount) let sampleTimeC, samplesC if (useQuadraticInterpolation) { samplesC = BufferSampler(sampler, sampleCount) } let min = Infinity, max = -Infinity let minX, maxX // TODO: Clean up and optimize function secondOrderInterp(x0, x1, x2, y0, y1, y2, x) { const a0 = ((x1 * x2) / ((x2 - x0) * (x1 - x0))) * y0 + ((-x0 * x2) / ((x1 - x0) * (x2 - x1))) * y1 + ((x0 * x1) / ((x2 - x0) * (x2 - x1))) * y2 const a1 = (-(x2 + x1) / ((x2 - x0) * (x1 - x0))) * y0 + ((x2 + x0) / ((x1 - x0) * (x2 - x1))) * y1 + (-(x1 + x0) / ((x2 - x0) * (x2 - x1))) * y2 const a2 = (1 / ((x2 - x0) * (x1 - x0))) * y0 + (-1 / ((x1 - x0) * (x2 - x1))) * y1 + (1 / ((x2 - x0) * (x2 - x1))) * y2 return a2 * x ** 2 + a1 * x + a0 } function sampleRange(_scope, sampleArray, sampleCount, _minX, _maxX) { if (!_.has(_scope, 't')) { throw `Expected time parameter for InterFrameSampler in sampleRange()` } let t = _scope['t'] let deltaT = t - sampleTimeA if ( deltaT >= refreshPeriodT || _maxX > maxX + samplePadding / 2 || _minX < minX - samplePadding / 2 ) { refresh(_minX, _maxX, t) deltaT = 0 } const span = _maxX - _minX const step = span / (sampleCount - 1) for (let i = 0; i < sampleCount; i++) { let x = _minX + step * i let y if (useQuadraticInterpolation) { let a = samplesA.sample(x) let b = samplesB.sample(x) let c = samplesC.sample(x) y = secondOrderInterp(0, 0.5, 1, a, b, c, deltaT / refreshPeriodT) } else { let a = samplesA.sample(x) let b = samplesB.sample(x) y = a + ((b - a) / (sampleTimeB - sampleTimeA)) * deltaT } if (y < min) min = y if (y > max) max = y sampleArray[i].set(x, y) } } function refresh(_minX, _maxX, currentT) { minX = _minX maxX = _maxX let nextT = currentT + refreshPeriodT if (useQuadraticInterpolation) { sampleTimeA = currentT samplesA.refreshSamples( minX - samplePadding, maxX + samplePadding, 't', sampleTimeA, ) sampleTimeC = nextT samplesC.refreshSamples(minX - 10, maxX + 10, 't', sampleTimeC) sampleTimeB = sampleTimeA + (nextT - currentT) / 2 samplesB.refreshSamples(minX - 10, maxX + 10, 't', sampleTimeB) } else { sampleTimeA = currentT samplesA.refreshSamples( minX - samplePadding, maxX + samplePadding, 't', sampleTimeA, ) sampleTimeB = nextT samplesB.refreshSamples(minX - 10, maxX + 10, 't', sampleTimeB) } } function resetExtrema() { min = Infinity max = -Infinity } return _.mix(self, { refresh, sampleRange, resetExtrema, get min() { return min }, get max() { return max }, }) } FILE: Types_Rigidbody.js function Rigidbody(spec) { let { transform, graph, globalScope, camera, screen, fixed = false, fixedRotation = false, positionOffset = Vector2(), invertGravity, } = spec let grounded = false const velocity = Vector2() const originalVelocity = Vector2() const collisionThreshold = 0.0 const collisionTangent = Vector2() const collisionNormal = Vector2() const upright = Vector2(0, 1) const samplePosition = Vector2() const graphVelocity = Vector2() const depenetration = Vector2() const debugVectorOrigin = Vector2() const debugVectorTerminus = Vector2() const gravitySign = invertGravity ? 1 : -1 function tick() { if (!globalScope.running || fixed) { return } // Graph can change from cartesian/polar mid-level (editor) let polar = graph.isPolar // polar = false // Gravity if (polar) { // Polar (towards or away from center) const theta = Math.atan2(transform.y, transform.x) velocity[0] += gravitySign * Math.cos(theta) * 9.8 * 3 * globalScope.dt velocity[1] += gravitySign * Math.sin(theta) * 9.8 * 3 * globalScope.dt } else { // Cartesian (down) velocity[1] += gravitySign * 9.8 * globalScope.dt } // Integrate velocity. TODO: Verlet integration transform.x += velocity[0] * globalScope.dt transform.y += velocity[1] * globalScope.dt // Sample graph height samplePosition.set(transform.position) samplePosition.add(positionOffset) let penetrationDepth, surfaceTheta if (polar) { surfaceTheta = graph.thetaOfClosestSurfacePoint(transform.position) const surfaceR = graph.sample('theta', surfaceTheta) const r = transform.position.magnitude penetrationDepth = surfaceR - r } else { const graphY = graph.sample('x', samplePosition.x) penetrationDepth = graphY - samplePosition.y } grounded = penetrationDepth > collisionThreshold if (grounded) { // Set collision tangent, normal, velocity if (polar) { // Calculate vectors from graph position graph.tangentVectorAt(surfaceTheta, collisionTangent) collisionTangent.normalize() collisionTangent.negate() graph.normalVectorAt(surfaceTheta, collisionNormal) collisionNormal.normalize() graph.velocityVectorAt(surfaceTheta, graphVelocity) /* Collision correction */ // Project velocity onto collision normal and tangent const tangentScalar = collisionTangent.dot(velocity) / velocity.magnitude const depenetrationScalar = penetrationDepth // Calculate depenetration collisionNormal.multiply(depenetrationScalar, depenetration) // Calculate graph velocity graphVelocity.multiply(graphVelocity.dot(collisionNormal)) // collisionNormal.multiply(graphVelocityScalar, graphVelocity) // Save current velocity originalVelocity.set(velocity) // Reset velocity to limits imposed by ground angle collisionTangent.multiply(tangentScalar, velocity) // Add velocity of ground velocity.add(graphVelocity) // Prevent "sticking" to ground in cases where upward velocity was higher before collision correction // velocity.max(originalVelocity) // Smoothly move upright vector toward ground normal upright.lerp(collisionNormal, 0.15) upright.normalize() const uprightAngle = math.atan2(-upright.x, upright.y) // Write new rotation transform.rotation = fixedRotation ? 0 : uprightAngle // transform.rotation = uprightAngle // Depenetrate from ground transform.position.add(depenetration) } else { const graphSlope = graph.sampleSlope('x', samplePosition.x) // Set collision tangent collisionTangent.x = 1 collisionTangent.y = graphSlope collisionTangent.normalize() // Set collision normal collisionTangent.orthogonalize(collisionNormal) const verticalGraphVelocity = graph.sampleSlope( 't', globalScope.t, 'x', samplePosition.x, ) // Project velocity onto collision normal and tangent const tangentScalar = collisionTangent.dot(velocity) const normalScalar = collisionNormal.dot(velocity) // Project penetration onto collision normal const depenetrationScalar = collisionNormal.y * penetrationDepth // Project graph velocity onto collision normal const graphVelocityScalar = collisionNormal.y * verticalGraphVelocity // Calculate depenetration collisionNormal.multiply(depenetrationScalar, depenetration) // Calculate graph velocity collisionNormal.multiply(graphVelocityScalar, graphVelocity) // Save current upward velocity const velocityY = velocity.y // Reset velocity to limits imposed by ground angle collisionTangent.multiply(tangentScalar, velocity) // Add velocity of ground velocity.add(graphVelocity) // Prevent "sticking" to ground in cases where upward velocity was higher before collision correction velocity.y = Math.max(velocity.y, velocityY) // Smoothly move upright vector toward ground normal upright.lerp(collisionNormal, 0.15) upright.normalize() const uprightAngle = math.atan2(-upright.x, upright.y) // Write new rotation transform.rotation = fixedRotation ? 0 : uprightAngle // transform.rotation = uprightAngle // Depenetrate from ground transform.position.add(depenetration) } } } function drawDebugVector(ctx, vector, color) { camera.worldToScreen(transform.position, debugVectorOrigin) debugVectorTerminus.set(vector) debugVectorTerminus.add(transform.position) camera.worldToScreen(debugVectorTerminus) ctx.beginPath() ctx.moveTo(debugVectorOrigin.x, debugVectorOrigin.y) ctx.lineTo(debugVectorTerminus.x, debugVectorTerminus.y) ctx.strokeStyle = color ctx.lineWidth = 4 ctx.stroke() } function draw(ctx) { if (grounded) { // drawDebugVector(ctx, collisionNormal, 'green') // drawDebugVector(ctx, collisionTangent, 'red') // drawDebugVector(ctx, upright, 'yellow') // drawDebugVector(ctx, graphVelocity, 'cyan') // drawDebugVector(ctx, depenetration, 'pink') } // drawDebugVector(ctx, velocity, 'orange') } function resetVelocity() { velocity.x = 0 velocity.y = 0 } return { get velocity() { return velocity }, set velocity(v) { velocity.set(v) }, get upright() { return upright }, set upright(v) { upright.set(v) }, resetVelocity, tick, draw, } } FILE: Types_Sampler.js // Sampler is a helpful object for sampling functions. // // Samplers are basically a wrapper around a compiled math.js expression. Samplers allow you to do extra things that are not built into math.js (For example: sampling a function over a range of values for a given variable) function Sampler(spec = {}) { let { scope: referenceScope = {}, defaultToLastValidExpression = true, allowInfinity = false, cap = 1000, } = spec let scope = {} let expression let lastValidExpression = '0' let evaluator let lastValidEvaluator = math.compile(lastValidExpression) let min = PINF let max = NINF let valid = false setExpression(spec.expression || '0') function decomment(expression) { return expression.split('//')[0] } function resetExtrema() { min = PINF max = NINF } function evaluate(scope) { if (expression == '') return 0 let e = defaultToLastValidExpression ? lastValidEvaluator : evaluator let v = 0 try { v = e.evaluate(scope) if (!allowInfinity) { if (v == PINF || v == NINF) v = 0 if (v.re == PINF || v.re == NINF) v.re = 0 if (v.im == PINF || v.im == NINF) v.im = 0 } if (_.isObject(v)) v = v.re ?? 0 else if (!_.isNumber(v)) v = 0 else if (_.isNaN(v)) v = 0 } catch (err) { v = 0 } if (cap) v = math.clamp(-cap, cap, v) if (v < min) min = v if (v > max) max = v return v } function sample() { _.assign(scope, referenceScope) // Assign variable/value pairs if (arguments.length >= 2) { for (let i = 0; i < arguments.length; i += 2) { scope[arguments[i]] = arguments[i + 1] } } return evaluate(scope) } function sampleSlope(variable, value) { _.assign(scope, referenceScope) // Assign variable/value pairs *except* first pair if (arguments.length >= 3) { for (let i = 2; i < arguments.length; i += 2) { scope[arguments[i]] = arguments[i + 1] } } const epsilon = 0.01 scope[variable] = value const sample0 = evaluate(scope) scope[variable] = value + epsilon const sample1 = evaluate(scope) return (sample1 - sample0) / epsilon } function sampleRange( _scope, sampleArray, sampleCount, rangeVariable, range0, range1, ) { _.assign(scope, referenceScope) const span = range1 - range0 const step = span / (sampleCount - 1) scope[rangeVariable] = range0 for (let i = 0; i < sampleCount; i++) { let rangeValue = range0 + step * i scope[rangeVariable] = rangeValue sampleArray[i][0] = rangeValue try { sampleArray[i][1] = evaluate(scope) } catch (err) { sampleArray[i][1] = 0 } } } function setExpression(_expression) { if (expression == _expression) return expression = _expression try { evaluator = math.compile(decomment(expression)) lastValidExpression = expression lastValidEvaluator = evaluator valid = true } catch (err) { evaluator = math.compile('0') valid = false } } function generateSampleArray(pointCount) { return _.map(Array(pointCount), () => Vector2(0, 0)) } return { sample, sampleRange, sampleSlope, _evaluate: evaluate, generateSampleArray, resetExtrema, setExpression, get expression() { return expression }, set expression(v) { setExpression(v) }, get max() { return max }, get min() { return min }, get valid() { return valid }, } } FILE: Types_Screen.js // The Screen transforms the "Frame" into screen-space coordinates. // // The Frame is a normalized representation of the screen, where a square quadrant is fitted into the center. // // A helpful diagram of the Frame: // // +1 (screen top) // +----+---------+----+ // | | | | | // | | | | | // | -1|----+----|+1 | (screen right) // | | | | | // | | | | | // +----+---------+----+ // -1 // // The screen will notify a list of subscribers whenever it is resized. (For example: when the screen size changes, the main Graph needs to recompute all its points to match the new boundaries of the screen) function Screen(spec = {}) { const transform = Transform() const self = {} let { canvas, element = window, // Disable blending by default for performance alpha = false, parentScreen, } = spec const ctx = canvas.getContext('2d', { alpha }) if (parentScreen) parentScreen.resizeSubs.push(self) let width let height let vertical let aspect const resizeSubs = [] const minFramePoint = Vector2() const maxFramePoint = Vector2() let resolutionScalingFactor = 1.0 function resize() { if (parentScreen) { resolutionScalingFactor = parentScreen.resolutionScalingFactor width = parentScreen.width height = parentScreen.height } else { width = element.innerWidth || element.width height = element.innerHeight || element.height width *= resolutionScalingFactor height *= resolutionScalingFactor } canvas.width = width canvas.height = height transform.x = width / 2 transform.y = height / 2 transform.scale = math.min(width, height) / 2 vertical = height > width aspect = width / height minFramePoint[0] = vertical ? -1 : -aspect minFramePoint[1] = vertical ? -1 / aspect : -1 maxFramePoint[0] = vertical ? 1 : aspect maxFramePoint[1] = vertical ? 1 / aspect : 1 _.callEach(resizeSubs) } function screenToFrame(point, output) { if (!output) output = point transform.invertPoint(point, output) output.y *= -1 return output } function frameToScreen(point, output) { if (!output) output = point output.set(point) output.y *= -1 transform.transformPoint(output) return output } function screenToFrameDirection(direction, output) { if (!output) output = direction transform.invertDirection(direction, output) output.y *= -1 return output } function frameToScreenDirection(direction, output) { if (!output) output = direction output.set(direction) output.y *= -1 transform.transformDirection(output) return output } resize() return _.mixIn(self, { transform, canvas, ctx, resize, resizeSubs, screenToFrame, frameToScreen, screenToFrameDirection, frameToScreenDirection, get width() { return width }, get height() { return height }, get vertical() { return height }, get aspect() { return aspect }, get minFramePoint() { return minFramePoint }, get maxFramePoint() { return maxFramePoint }, get resolutionScalingFactor() { return resolutionScalingFactor }, set resolutionScalingFactor(o) { resolutionScalingFactor = o resize() }, }) } FILE: Types_Structs_Bounds.js function Bounds() { const self = {} const center = Vector2() const size = Vector2() const extents = Vector2() const min = Vector2() const max = Vector2() let recomputing = false center.observe(centerModified) size.observe(sizeModified) extents.observe(extentsModified) min.observe(minModified) max.observe(maxModified) set.apply(self, arguments) function set() { recomputing = true if (arguments.length == 1) { const spec = arguments[0] center.set(spec.center) size.set(spec.size) } else if (arguments.length == 2) { center.set(arguments[0]) size.set(arguments[1]) } else if (arguments.length == 4) { center.set(arguments[0], arguments[1]) size.set(arguments[2], arguments[3]) } size.abs() recomputeFromCenterSize() } function setMinMax() { if (arguments.length == 2) { min.set(arguments[0]) max.set(arguments[1]) } else if (arguments.length == 4) { min.set(arguments[0], arguments[1]) max.set(arguments[2], arguments[3]) } recomputeFromMinMax() } function clone() { return Bounds(center, size) } function equals(other) { const equalCenter = center.equals(other.center) const equalSize = size.equals(other.size) return equalCenter && equalSize } function centerModified() { if (recomputing) return recomputing = true recomputeFromCenter() } function sizeModified() { if (recomputing) return recomputing = true size.abs() recomputeFromCenterSize() } function extentsModified() { if (recomputing) return recomputing = true extents.abs() recomputeFromCenterExtents() } function minModified() { if (recomputing) return recomputing = true recomputeFromMinMax() } function maxModified() { if (recomputing) return recomputing = true recomputeFromMinMax() } function recomputeFromCenter() { center.add(extents, max) center.subtract(extents, min) recomputing = false } function recomputeFromCenterSize() { size.divide(2, extents) center.add(extents, max) center.subtract(extents, min) recomputing = false } function recomputeFromCenterExtents() { extents.multiply(2, size) center.add(extents, max) center.subtract(extents, min) recomputing = false } function recomputeFromMinMax() { if (min.x > max.x) min.swapX(max) if (min.y > max.y) min.swapY(max) max.subtract(min, size) size.divide(2, extents) min.add(extents, center) recomputing = false } function expand(other) { if (other.generator == Vector2) { min.min(other) max.max(other) } else if (other.generator == Bounds) { self.expand(other.min) self.expand(other.max) } else { throw new Error( `Cannot expand bounds to fit other of invalid type: `, other, ) } } function contains(other) { if (other.generator == Vector2) { const cx = other.x >= min.x && other.x <= max.x const cy = other.y >= min.y && other.y <= max.y return cx && cy } else if (other.generator == Bounds) { return self.contains(other.min) && self.contains(other.max) } else { throw new Error( `Cannot check if bounds contains other of invalid type: `, other, ) } } function toString() { return `{center: ${center.toString()}, size: ${size.toString()}}` } return _.mixIn(self, { generator: Bounds, set, setMinMax, clone, equals, expand, contains, toString, get center() { return center }, set center(v) { center.set(v) }, get size() { return size }, set size(v) { size.set(v) }, get extents() { return extents }, set extents(v) { setSize(v) }, get min() { return min }, set min(v) { min.set(v) }, get max() { return max }, set max(v) { max.set(v) }, }) } // Tests LOGTESTS = false ;(() => { let a let b let c let d let p function Validate(bounds) { // center+extents=max Test(Vector2(bounds.center).add(bounds.extents)).equals(bounds.max) // center-extents=min Test(Vector2(bounds.center).subtract(bounds.extents)).equals(bounds.min) // size/2=extents Test(Vector2(bounds.size).divide(2)).equals(bounds.extents) // min.x <= max.x Test(bounds.min.x).lessThanOrEquals(bounds.max.x) // min.y <= max.y Test(bounds.min.y).lessThanOrEquals(bounds.max.y) // size.x >= 0 Test(bounds.size.x).greaterThanOrEquals(0) // size.y >= 0 Test(bounds.size.y).greaterThanOrEquals(0) // extents.x >= 0 Test(bounds.extents.x).greaterThanOrEquals(0) // extents.y >= 0 Test(bounds.extents.y).greaterThanOrEquals(0) } function ValidateProperty(bounds, path) { const property = bounds[path] // Randomize property.randomize(-10, 10) Validate(bounds) // Set property.set(property.x + 1, property.y + 1) Validate(bounds) // Set x property.x = property.x + 1 Validate(bounds) // Set y property.y = property.y + 1 Validate(bounds) // Add property.add(Vector2(1, 1)) Validate(bounds) // Multiply property.multiply(2) Validate(bounds) } // Test basic instantiation a = Bounds(1, 2, 3, 4) Test(a.center).equals(Vector2(1, 2)) Test(a.size).equals(Vector2(3, 4)) Test(a.min).equals(Vector2(-0.5, 0)) Test(a.max).equals(Vector2(2.5, 4)) Test(a.extents).equals(Vector2(1.5, 2)) b = Bounds(a.center, a.size) c = Bounds(a) d = a.clone() Test(a).equals(a) Test(a).equals(b) Test(a).equals(c) Test(a).equals(d) Validate(a) Validate(b) Validate(c) Validate(d) // Test modification of values a = Bounds(1, 2, 3, 4) ValidateProperty(a, 'center') ValidateProperty(a, 'size') ValidateProperty(a, 'min') ValidateProperty(a, 'max') ValidateProperty(a, 'extents') // Test contains/expand on Vector2 a = Bounds(1, 2, 3, 4) p = Vector2(2, 1) Test(a.contains(p)).isTrue() p = Vector2(5, -7) Test(a.contains(p)).isFalse() a.expand(p) Validate(a) Test(a.contains(p)).isTrue() // Test contains/expand on Bounds a = Bounds(1, 2, 3, 4) b = Bounds(1, 1, 1, 1) Test(a.contains(b)).isTrue() b = Bounds(-6, 4, 3, 2) Test(a.contains(b)).isFalse() a.expand(b) Validate(a) Test(a.contains(b)).isTrue() Test(b.contains(a)).isFalse() b.expand(a) Validate(b) Test(b.contains(a)).isTrue() })() FILE: Types_Structs_CameraState.js function CameraState(spec = {}) { const self = {} let { rotation = 0, fov = 10 } = spec const position = spec.position ? Vector2(spec.position) : Vector2(spec) function set(state) { position.x = state.position ? state.position.x : state.x || 0 position.y = state.position ? state.position.y : state.y || 0 rotation = state.rotation || 0 fov = state.fov || 10 } function lerp(b, progress, output, smooth = true) { if (!output) output = self if (smooth) progress = math.smooth(progress) position.lerp(b.position, progress, output.position) output.rotation = math.modLerp(rotation, b.rotation, progress, TAU, true) output.fov = math.lerp(fov, b.fov, progress) } function toString() { return `{position: ${position.toString()}, rotation: ${rotation}, fov: ${fov}}` } return _.mixIn(self, { set, lerp, toString, get position() { return position }, set position(v) { position.set(v) }, get x() { return position.x }, set x(v) { position.x = _.isNumber(v) ? v : 0 }, get y() { return position.y }, set y(v) { position.y = _.isNumber(v) ? v : 0 }, get rotation() { return rotation }, set rotation(v) { rotation = _.isNumber(v) ? v : 0 }, get fov() { return fov > 0 ? fov : 10 }, set fov(v) { fov = _.isNumber(v) ? v : 10 }, }) } FILE: Types_Structs_Circle.js function Circle(spec) { const self = Shape(spec) let { center = Vector2(), radius = 1, color = 'green' } = spec function getBounds() { return Rect({ width: radius, height: radius, }) } function intersectPoint(point, hit) { let p = self.localize(point) p.subtract(center) const intersecting = p.magnitude < radius if (hit && intersecting) { // TODO: Intersection hit data } return intersecting } function intersectCircle(circle, hit) { // TODO: Circle intersection return false } function intersectRect(rect, hit) { // TODO: Rect intersection return false } function drawLocal(ctx) { ctx.strokeStyle = color ctx.lineWidth = 0.1 ctx.beginPath() ctx.arc(center.x, -center.y, radius, 0, TAU) ctx.stroke() } function draw(ctx, camera) { camera.drawThrough(ctx, drawLocal, self.transform) } return _.mixIn(self, { shapeType: 'circle', draw, getBounds, intersectPoint, intersectCircle, intersectRect, get center() { return center }, set center(v) { center.set(v) }, get radius() { return getRadius() }, set radius(v) { radius = v }, }) } FILE: Types_Structs_Color.js function Color() { const self = {} let r = 0 let g = 0 let b = 0 let h = 0 let s = 0 let v = 0 let stringsDirty = true let hexString let rgbString let hsvString set.apply(self, arguments) function set() { if (arguments.length == 1) { let o = arguments[0] if (_.isString(o)) { setHex(o) computeHsv() } else if (_.isObject(o)) { if (_.has(o, ['h', 's', 'v'])) { setHsv(o.h, o.s, o.v) } else { r = o.r || 0 g = o.g || 0 b = o.b || 0 computeHsv() } } else if (_.isArray(o)) { r = o.r g = o.g b = o.b computeHsv() } } else if (arguments.length < 3) { r = 0 g = 0 b = 0 computeHsv() } else { r = arguments[0] || 0 g = arguments[1] || 0 b = arguments[2] || 0 computeHsv() } return self } function clone() { return Color(r, g, b) } function computeStrings() { if (!stringsDirty) return hexString = '#' + componentToHex(r) + componentToHex(g) + componentToHex(b) rgbString = `rgb(${Math.round(r * 255)}, ${Math.round( g * 255, )}, ${Math.round(b * 255)}` hsvString = `hsv(${Math.round(h * 255)}, ${Math.round( s * 255, )}, ${Math.round(v * 255)}` } function componentToHex(c) { c = math.clamp01(c) const hex = Math.round(c * 255).toString(16) return hex.length == 1 ? '0' + hex : hex } function computeHsv() { const max = Math.max(r, g, b) const min = Math.min(r, g, b) h, s, (v = max) let d = max - min s = max == 0 ? 0 : d / max if (max == min) { h = 0 // achromatic } else { switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0) break case g: h = (b - r) / d + 2 break case b: h = (r - g) / d + 4 break } h /= 6 } } function setHsv(_h, _s, _v) { h = _h s = _s v = _v const i = Math.floor(h * 6) const f = h * 6 - i const p = v * (1 - s) const q = v * (1 - f * s) const t = v * (1 - (1 - f) * s) switch (i % 6) { case 0: ;(r = v), (g = t), (b = p) break case 1: ;(r = q), (g = v), (b = p) break case 2: ;(r = p), (g = v), (b = t) break case 3: ;(r = p), (g = q), (b = v) break case 4: ;(r = t), (g = p), (b = v) break case 5: ;(r = v), (g = p), (b = q) break } stringsDirty = true return self } function setR(_r) { r = _r computeHsv() stringsDirty = true return self } function setG(_g) { g = _g computeHsv() stringsDirty = true return self } function setB(_b) { b = _b computeHsv() stringsDirty = true return self } function setH(_h) { setHsv(_h, s, v) stringsDirty = true return self } function setS(_s) { setHsv(h, _s, v) computeHsv() stringsDirty = true return self } function setV(_v) { setHsv(h, s, _v) computeHsv() stringsDirty = true return self } function setRgb(_r, _g, _b) { r = _r g = _g b = _b computeHsv() stringsDirty = true return self } function getHex() { computeStrings() return hexString } function setHex(_hexString) { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(_hexString) r = parseInt(result[1], 16) / 255 g = parseInt(result[2], 16) / 255 b = parseInt(result[3], 16) / 255 computeHsv() stringsDirty = true } function add(other, output) { if (!output) output = this output.r = r + other.r output.g = g + other.g output.b = b + other.b return output } function subtract(other, output) { if (!output) output = this output.r = r - other.r output.g = g - other.g output.b = b - other.b return output } function multiply(other, output) { if (!output) output = this if (_.isObject(other)) { output.r = r * other.r output.g = g * other.g output.b = b * other.b } else { output.r = r * other output.g = g * other output.b = b * other } return output } function divide(scalar, output) { if (!output) output = this output.r = r / scalar output.g = g / scalar output.b = b / scalar return output } function invertRgb(output) { if (!output) output = this output.r = 1 - r output.g = 1 - g output.b = 1 - b return output } function lerp(other, t, output) { if (!output) output = this output.r = math.lerp(r, other.r, t) output.g = math.lerp(g, other.g, t) output.b = math.lerp(b, other.b, t) return output } function toString() { return `RGB(${math.truncate(r, 2)}, ${math.truncate(g, 2)}, ${math.truncate( b, 2, )})` } return _.mixIn(self, { get r() { return r }, set r(_r) { setR(_r) }, get g() { return g }, set g(_g) { setG(_g) }, get b() { return b }, set b(_b) { setB(_b) }, get h() { return h }, set h(_h) { setHsv(_h, s, v) }, get s() { return s }, set s(_s) { setHsv(h, _s, v) }, get v() { return v }, set v(_v) { setHsv(h, s, _v) }, get hex() { return getHex() }, set hex(_hex) { setHex(_hex) }, set, clone, setHsv, setRgb, setHex, setR, setG, setB, setH, setS, setV, add, subtract, multiply, divide, invertRgb, lerp, toString, }) } function ColorHSV(h, s, v) { const color = Color() color.setHsv(h, s, v) return color } FILE: Types_Structs_EntityGroup.js function EntityGroup(entities = []) { function blur(blurAmount) { entities.forEach((e) => (e.blur = blurAmount)) } return { blur, } } FILE: Types_Structs_PlayerStorage.js function PlayerStorage() { let levels = localStorage.getItem('levels') levels = levels ? JSON.parse(levels) : {} let activeLevel = localStorage.getItem('activeLevel') function getLevel(name) { return levels[name] } function getCompletedLevels() { return _.filter( levels, (v, k) => v?.completed && !k.toLowerCase().startsWith('puzzle_'), ) } function save() { localStorage.setItem('levels', JSON.stringify(levels)) localStorage.setItem('activeLevel', activeLevel) } function setLevel(name, data, activate = true) { if (!data) return levels[name] = data if (activate && !name.startsWith('puzzle_')) activeLevel = name save() } function clear() { levels = {} activeLevel = null save() } return { get levels() { return levels }, clear, getCompletedLevels, getLevel, setLevel, get activeLevel() { return activeLevel }, set activeLevel(v) { activeLevel = v save() }, } } FILE: Types_Structs_Rect.js function Rect(spec) { const self = Shape(spec) let { center = Vector2(), width = 1, height = 1 } = spec function getBounds() { return self } function intersectPoint(point, hit) { let p = self.localize(point) p.subtract(center) const insideX = Math.abs(p.x) < width / 2 const insideY = Math.abs(p.y) < height / 2 const intersecting = insideX && insideY if (hit && intersecting) { // TODO: Intersection hit data } return intersecting } function intersectCircle(circle, hit) { // TODO: Circle intersection return false } function intersectRect(rect, hit) { // TODO: Rect intersection return false } function drawLocal(ctx) { ctx.strokeStyle = 'green' ctx.lineWidth = 0.1 ctx.beginPath() ctx.rect(center.x - width / 2, -center.y - height / 2, width, height) ctx.stroke() } function draw(ctx, camera) { camera.drawThrough(ctx, drawLocal, self.transform) } return _.mixIn(self, { shapeType: 'rect', draw, intersectPoint, intersectCircle, intersectRect, getBounds, get center() { return center }, set center(v) { center.set(v) }, get width() { return width }, set width(v) { width = v }, get height() { return height }, set height(v) { height = v }, }) } FILE: Types_Structs_Shape.js function Shape(spec) { const { transform } = spec const p = Vector2() function localize(point) { p.set(point) if (transform) transform.invertPoint(p) return p } return { localize, transform, } } FILE: Types_Structs_Test.js let LOGTESTS = true function Test(value) { const self = {} function stringify(v) { if (_.isArray(v)) return `[${_.join(_.map(v, stringify), ', ')}]` else if (_.isObject(v) && _.has(v, 'toString')) return v.toString() else if (_.isObject(v)) return JSON.stringify(v) else if (_.isString(v)) return `"${v}"` else if (v == null) return 'null' else return _.toString(v) } function assert(boolean, successMessage, failureMessage) { if (boolean) { // if (LOGTESTS) } else throw new Error(failureMessage) } function isTrue() { const successMessage = `As expected, ${stringify(value)} is true` const failureMessage = `${stringify(value)} is false` assert(value === true, successMessage, failureMessage) } function isFalse() { const successMessage = `As expected, ${stringify(value)} is false` const failureMessage = `${stringify(value)} is true` assert(value === false, successMessage, failureMessage) } function equals(other) { const successMessage = `As expected, ${stringify(value)} equals ${stringify( other, )}` const failureMessage = `${stringify(value)} does not equal ${stringify( other, )}` if (_.isObject(value)) { assert(value.equals(other), successMessage, failureMessage) } else { assert(value === other, successMessage, failureMessage) } } function notEquals(other) { const successMessage = `As expected, ${stringify( value, )} does not equal ${stringify(other)}` const failureMessage = `${stringify(value)} equals ${stringify(other)}` if (_.isObject(value)) { assert(!value.equals(other), successMessage, failureMessage) } else { assert(value !== other, successMessage, failureMessage) } } function lessThan(other) { const successMessage = `As expected, ${stringify( value, )} is less than ${stringify(other)}` const failureMessage = `${stringify( value, )} is greater than or equal to ${stringify(other)}` assert(value < other, successMessage, failureMessage) } function greaterThan(other) { const successMessage = `As expected, ${stringify( value, )} is greater than ${stringify(other)}` const failureMessage = `${stringify( value, )} is less than or equal to ${stringify(other)}` assert(value > other, successMessage, failureMessage) } function lessThanOrEquals(other) { const successMessage = `As expected, ${stringify( value, )} is less than or equal to ${stringify(other)}` const failureMessage = `${stringify(value)} is greater than ${stringify( other, )}` assert(value <= other, successMessage, failureMessage) } function greaterThanOrEquals(other) { const successMessage = `As expected, ${stringify( value, )} is greater than or equal to ${stringify(other)}` const failureMessage = `${stringify(value)} is less than ${stringify( other, )}` assert(value >= other, successMessage, failureMessage) } function fails() { const successMessage = `As expected, this function throws an error with arguments ${stringify( [...arguments], )}` const failureMessage = `An error was not thrown with arguments ${stringify([ ...arguments, ])}` try { value.apply(null, arguments) assert(false, successMessage, failureMessage) } catch (ex) { assert(true, successMessage, failureMessage) } } function succeeds() { const successMessage = `As expected, this function did not throw an error with arguments ${stringify( [...arguments], )}` const failureMessage = `An error was thrown with arguments ${stringify([ ...arguments, ])}` try { value.apply(null, arguments) assert(true, successMessage, failureMessage) } catch (ex) { assert(false, successMessage, failureMessage) } } return _.mixIn(self, { isTrue, isFalse, equals, notEquals, lessThan, greaterThan, lessThanOrEquals, greaterThanOrEquals, fails, succeeds, }) } // Tests LOGTESTS = false ;(() => { function TestObject(value) { function equals(other) { return value === other.value } return { equals, get value() { return value }, } } // Test booleans Test(true).isTrue() Test(false).isFalse() Test(Test(true).isFalse).fails() Test(Test(false).isTrue).fails() Test(Test(null).isTrue).fails() Test(Test(null).isFalse).fails() Test(Test(0).isTrue).fails() Test(Test(0).isFalse).fails() Test(Test(1).isTrue).fails() Test(Test(1).isFalse).fails() Test(Test('').isTrue).fails() Test(Test('').isFalse).fails() // Test boolean equality Test(true).equals(true) Test(false).equals(false) Test(Test(true).equals).fails(false) Test(Test(false).equals).fails(true) Test(true).notEquals(false) Test(false).notEquals(true) Test(Test(true).notEquals).fails(true) Test(Test(false).notEquals).fails(false) Test(true).notEquals(null) Test(false).notEquals(null) Test(Test(true).equals).fails(false) Test(Test(false).equals).fails(true) // Test numerical equality Test(0).equals(0) Test(1).equals(1) Test(Test(0).equals).fails(1) Test(Test(1).equals).fails(0) Test(0).notEquals(1) Test(1).notEquals(0) Test(Test(0).notEquals).fails(0) Test(Test(1).notEquals).fails(1) Test(0).notEquals(null) Test(1).notEquals(null) Test(Test(0).equals).fails(null) Test(Test(1).equals).fails(null) // Test numerical inequalities Test(10).lessThan(11) Test(10).greaterThan(9) Test(10).lessThanOrEquals(11) Test(10).greaterThanOrEquals(9) Test(10).lessThanOrEquals(10) Test(10).greaterThanOrEquals(10) Test(Test(10).greaterThan).fails(11) Test(Test(10).lessThan).fails(9) Test(Test(10).lessThanOrEquals).fails(9) Test(Test(10).greaterThanOrEquals).fails(11) Test(Test(10).greaterThan).fails(10) Test(Test(10).lessThan).fails(10) // Test string equality Test('abc').equals('abc') Test('abc').notEquals('xyz') Test(Test('abc').equals).fails('xyz') Test(Test('abc').notEquals).fails('abc') // Test object equality const a = TestObject('a') const b = TestObject('b') Test(a).equals(a) Test(a).notEquals(b) Test(Test(a).equals).fails(b) Test(Test(a).notEquals).fails(a) })() FILE: Types_Structs_Union.js function Union(spec) { const self = Shape(spec) const { shapes } = spec const center = Vector2() let width = math.ninf let height = math.ninf for (const shape of shapes) { width = Math.max(width, shape.width) height = Math.max(height, shape.height) center.add(shape.center) } center.divide(shapes.length) function getBounds() { return self } function intersectPoint(point, hit) { return shapes.some((shape) => shape.intersectPoint(point, hit)) } function intersectCircle(circle, hit) { return shapes.some((shape) => shape.intersectCircle(circle, hit)) } function intersectRect(rect, hit) { return shapes.some((shape) => shape.intersectRect(rect, hit)) } function draw(ctx, camera) { for (const shape of shapes) shape.draw(ctx, camera) } return _.mixIn(self, { shapeType: 'union', draw, intersectPoint, intersectCircle, intersectRect, getBounds, get center() { return center }, set center(v) { center.set(v) }, get width() { return width }, set width(v) { width = v }, get height() { return height }, set height(v) { height = v }, }) } FILE: Types_Structs_Vector2.js function Vector2() { const self = {} let x let y let observerCallback = null let observerCallbacks = null let magnitude = 0 let magnitudeDirty = true set.apply(self, arguments) function setX(_x) { if (x == _x) return x = _.isNumber(_x) ? _x : 0 magnitudeDirty = true notifyObservers() } function setY(_y) { if (y == _y) return y = _.isNumber(_y) ? _y : 0 magnitudeDirty = true notifyObservers() } function set() { if (arguments.length == 0) { x = 0 y = 0 } else if (arguments.length == 1) { let o = arguments[0] if (_.isArray(o)) { x = o[0] y = o[1] } else if (_.isObject(o)) { x = o.x ?? 0 y = o.y ?? 0 } else if (_.isNumber(o)) { x = Math.cos(o) y = Math.sin(o) } } else { x = arguments[0] y = arguments[1] } if (_.isNaN(x)) { throw 'Setting x to NaN' console.trace() } if (_.isNaN(y)) { throw 'Setting y to NaN' console.trace() } magnitudeDirty = true notifyObservers() return self } function observe(cb) { if (!observerCallback) observerCallback = cb else if (!observerCallbacks) { observerCallbacks = [observerCallback] observerCallback = () => { for (let i = 0; i < observerCallbacks.length; i++) { observerCallbacks[i]() } } } else { observerCallbacks.push(cb) } } function notifyObservers() { if (observerCallback) observerCallback() } function clone() { return Vector2(x, y) } function equals(other) { return x === other.x && y === other.y } function swapX(other) { const ox = other.x other.x = x setX(ox) return self } function swapY(other) { const oy = other.y other.y = y setY(oy) return self } function add(other, output) { if (!output) output = self const ox = x + other.x const oy = y + other.y output.set(ox, oy) return output } function subtract(other, output) { if (!output) output = self const ox = x - other.x const oy = y - other.y output.set(ox, oy) return output } function multiply(scalar, output) { if (!output) output = self const ox = x * scalar const oy = y * scalar output.set(ox, oy) return output } function divide(scalar, output) { if (!output) output = self const ox = x / scalar const oy = y / scalar output.set(ox, oy) return output } function negate(output) { if (!output) output = self const ox = -x const oy = -y output.set(ox, oy) return output } function lerp(other, t, output) { if (!output) output = self const ox = math.lerp(x, other.x, t) const oy = math.lerp(y, other.y, t) output.set(ox, oy) return output } function min(other, output) { if (!output) output = self const ox = Math.min(x, other.x) const oy = Math.min(y, other.y) output.set(ox, oy) return output } function max(other, output) { if (!output) output = self const ox = Math.max(x, other.x) const oy = Math.max(y, other.y) output.set(ox, oy) return output } function abs(output) { if (!output) output = self const ox = Math.abs(x) const oy = Math.abs(y) output.set(ox, oy) return output } function distance(other) { const dx = other.x - x const dy = other.y - y return Math.sqrt(dx * dx + dy * dy) } function getMagnitude() { if (magnitudeDirty) { magnitude = Math.sqrt(x * x + y * y) magnitudeDirty = false } return magnitude } function dot(other) { return x * other.x + y * other.y } function rotate(angle, output) { if (!output) output = self let sin let cos if (_.isObject(angle)) { sin = angle.y cos = angle.x } else { sin = Math.sin(angle) cos = Math.cos(angle) } let newX = x * cos - y * sin let newY = x * sin + y * cos const ox = newX const oy = newY output.set(ox, oy) return output } function perturb(radius = 1, output) { if (!output) output = self const a = Math.random() * TAU const c = Math.cos(a) * radius const s = Math.sin(a) * radius const ox = x + c const oy = y + s output.set(ox, oy) return output } function randomize(min = 0, max = 1) { const ox = _.random(min, max) const oy = _.random(min, max) self.set(ox, oy) } function normalize(output) { if (!output) output = self const m = getMagnitude() const ox = x / m const oy = y / m output.set(ox, oy) return output } // Counter-clockwise perpendicular function orthogonalize(output) { if (!output) output = self const orthogonalX = -y const orthogonalY = x const ox = orthogonalX const oy = orthogonalY output.set(ox, oy) return output } function toString(digits = 3) { const sx = math.truncate(x, digits) const sy = math.truncate(y, digits) return `[${sx}, ${sy}]` } return _.mixIn(self, { generator: Vector2, set, clone, equals, observe, swapX, swapY, add, subtract, multiply, divide, negate, min, max, abs, dot, lerp, rotate, perturb, distance, randomize, normalize, orthogonalize, toString, get 0() { return x }, set 0(v) { setX(v) }, get 1() { return y }, set 1(v) { setY(v) }, get x() { return x }, set x(v) { if (_.isNaN(v)) { throw 'Setting x to NaN' console.trace() } setX(v) }, get y() { return y }, set y(v) { if (_.isNaN(v)) { throw 'Setting y to NaN' console.trace() } setY(v) }, get z() { return 0 }, get magnitude() { return getMagnitude() }, get normalized() { return normalize() }, }) } function Vector2Pinf() { return Vector2(math.pinf, math.pinf) } function Vector2Ninf() { return Vector2(math.ninf, math.ninf) } FILE: Types_Transform.js function Transform(spec = {}, entity = null) { let { scale = 1, rotation = 0 } = spec if (!entity && spec.entity) entity = spec.entity // If specified, use cartesian coordinates (x, y) // or polar coordinates (r, theta) converted to cartesian. // Else, position is <0, 0> by default const position = spec.position ? Vector2(spec.position) : spec.theta && spec.r ? Vector2(spec.r * Math.cos(spec.theta), spec.r * Math.sin(spec.theta)) : Vector2(spec.x || 0, spec.y || 0) const rotator = Vector2(Math.cos(rotation), Math.sin(rotation)) const inverseRotator = Vector2(Math.cos(-rotation), Math.sin(-rotation)) function getParent() { if (!entity) return null if (!entity.parent) return null if (!entity.parent.transform) return null return entity.parent.transform } function tryApplyParent(path, ...args) { let parent = getParent() if (!parent) return false parent[path].apply(parent, args) return true } function setRotation(_rotation) { if (rotation != _rotation) { rotation = _rotation rotator.x = Math.cos(rotation) rotator.y = Math.sin(rotation) inverseRotator.x = Math.cos(-rotation) inverseRotator.y = Math.sin(-rotation) } } function transformPoint(point, output = null) { if (!output) output = point else output.set(point) output.multiply(scale) output.rotate(rotator) output.add(position) tryApplyParent('transformPoint', point, output) return output } function invertPoint(point, output = null) { if (!output) output = point else output.set(point) tryApplyParent('invertPoint', point, output) output.subtract(position) output.rotate(-rotation) output.divide(scale) return output } function transformDirection(point, output = null) { if (!output) output = point else output.set(point) output.multiply(scale) output.rotate(rotator) tryApplyParent('transformDirection', point, output) return output } function invertDirection(point, output = null) { if (!output) output = point else output.set(point) tryApplyParent('invertDirection', point, output) output.rotate(-rotation) output.divide(scale) return output } function transformScalar(scalar) { tryApplyParent('transformScalar', scalar) return scalar * scale } function invertScalar(scalar) { tryApplyParent('invertScalar', scalar) return scalar / scale } function transformCanvas(ctx) { ctx.scale(1 / scale, 1 / scale) ctx.rotate(-rotation) ctx.translate(-position.x, -position.y) tryApplyParent('transformCanvas', ctx) } function invertCanvas(ctx) { tryApplyParent('invertCanvas', ctx) ctx.translate(position.x, position.y) ctx.rotate(rotation) ctx.scale(scale, scale) } return { getParent, transformPoint, invertPoint, transformDirection, invertDirection, transformScalar, invertScalar, transformCanvas, invertCanvas, get position() { return position }, set position(v) { position.x = v[0] position.y = v[1] }, get x() { return position.x }, set x(v) { position.x = v }, get y() { return position.y }, set y(v) { position.y = v }, get scale() { return scale }, set scale(v) { scale = v }, get rotation() { return rotation }, set rotation(v) { setRotation(v) }, get parent() { return getParent() }, get worldRotation() { let p = getParent() return (p ? p.worldRotation : 0) + rotation }, get parentWorldRotation() { let p = getParent() return p ? p.worldRotation : 0 }, } } FILE: Types_WebGL_ConstantLakeSunsetQuad.js /** * Sunset shader class for Constant Lake scene */ function ConstantLakeSunsetQuad(spec) { let canvas = document.createElement('canvas') const { defaultExpression, assets } = spec canvas.width = innerWidth canvas.height = innerHeight gl = canvas.getContext('webgl') if (!gl) { return alert( 'Your browser does not support WebGL. Try switching or updating your browser!', ) } gl.enable(gl.BLEND) gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) const utils = GLUtils(gl) const ext = utils.InstancingExtension() const line = utils.Vertices(gl.STATIC_DRAW, { vertexId: { type: 'float', data: [0, 1, 2, 3], }, }) const quad = utils.Vertices(gl.STATIC_DRAW, { aCoords: { type: 'vec2', data: [-1, -1, -1, 1, 1, -1, 1, 1], }, aTexCoords: { type: 'vec2', data: [0, 0, 0, 1, 1, 0, 1, 1], }, }) const quadNoUV = utils.quadNoUV const shaders = assets.shaders const quadProgram = utils.Program({ vert: shaders.quad_vert, frag: shaders.quad_frag, }) const blendProgram = utils.Program({ vert: shaders.quad_vert, frag: shaders.blend_frag, }) const pointsProgram = utils.Program({ vert: shaders.points_vert, frag: shaders.points_frag, }) const sunsetProgram = utils.Program({ vert: shaders.sunset_vert, frag: shaders.sunset_frag, }) const particleCount = 500 // [ x, y ] const oldParticlePositions = new Float32Array(particleCount * 2) const newParticlePositions = new Float32Array(particleCount * 2) const particleColors = new Float32Array(particleCount * 3) const percentLifeLived = new Float32Array(particleCount) const lifetimes = new Float32Array(particleCount) // Only used CPU side function genParticleColor() { // const c = Math.random() * 0.5 + 0.5 const c1 = Math.random() * 0.3 + 0.0 const c2 = Math.random() * 0.5 + 0.3 const c3 = Math.random() * 0.2 + 0.8 const scale = 1.0 // Math.pow(Math.random(), 2.0) * 0.8 + 0.2 return [c1 * scale, c2 * scale, c3 * scale] } function createParticleAt(index) { const posIndex = 2 * index const colIndex = 3 * index const x = Math.random() const y = Math.random() oldParticlePositions[posIndex] = x oldParticlePositions[posIndex + 1] = y newParticlePositions[posIndex] = x newParticlePositions[posIndex + 1] = y const col = genParticleColor() particleColors[colIndex] = col[0] particleColors[colIndex + 1] = col[1] particleColors[colIndex + 2] = col[2] const lifetime = Math.random() * 4 + 4 // 5-10 lifetimes[index] = lifetime percentLifeLived[index] = 0 } for (let i = 0; i < particleCount; i++) { createParticleAt(i) } const oldParticlePositionsBuffer = utils.Array( oldParticlePositions, gl.DYNAMIC_DRAW, ) const newParticlePositionsBuffer = utils.Array( newParticlePositions, gl.DYNAMIC_DRAW, ) const percentLifeLivedBuffer = utils.Array(percentLifeLived, gl.DYNAMIC_DRAW) const particleColorBuffer = utils.Array(particleColors, gl.DYNAMIC_DRAW) // const input = document.querySelector('input') const eta = 0.12 t = 0 function updateParticlePositions(vectorField) { for (let i = 0; i < particleCount; i++) { const index = 2 * i const normX = newParticlePositions[index] const normY = newParticlePositions[index + 1] // If out of bounds of canvas or end of life then reset if ( percentLifeLived[i] > 1 || Math.abs(normX) > 1 || Math.abs(normY) > 1 ) { createParticleAt(i) continue } const x = (normX - 0.5) * 10 const y = (normY - 0.5) * 10 const [dx, dy] = vectorField(x, y, world.level.getCutsceneX) const newX = eta * tickDelta * dx + normX const newY = eta * tickDelta * dy + normY // otherwise nudge w/ gradient oldParticlePositions[index] = normX oldParticlePositions[index + 1] = normY newParticlePositions[index] = newX newParticlePositions[index + 1] = newY percentLifeLived[i] += 1 / (lifetimes[i] * ticksPerSecond) } } /* triple buffer swap: render new frame to current store old in acc blend current and acc to blend swap blend and acc display acc */ // TODO: Handle resizing let current = utils.Texture([canvas.width, canvas.height], gl.RGBA) let acc = utils.Texture([canvas.width, canvas.height], gl.RGBA) let blend = utils.Texture([canvas.width, canvas.height], gl.RGBA) const step = utils.Framebuffer() function resize(width, height) { canvas.width = width canvas.height = height current.destroy() current = utils.Texture([canvas.width, canvas.height], gl.RGBA) acc.destroy() acc = utils.Texture([canvas.width, canvas.height], gl.RGBA) blend.destroy() blend = utils.Texture([canvas.width, canvas.height], gl.RGBA) } let last = null let evaluator = math.compile(defaultExpression) function setVectorFieldExpression(text) { try { const e = math.compile(text) e.evaluate({ x: 0, y: 0, t: 0 }) // Make sure can evaluate properly evaluator = e } catch (err) {} } function vectorField(x, y, t) { const c = evaluator.evaluate({ x, y, t }) try { // Either real or complex return typeof c === 'number' ? [c, 0] : [math.re(c), math.im(c)] } catch (ex) { return [0, 0] } } function update() { const now = performance.now() const delta = last ? now - last : 0 last = now t += delta * 0.00005 // // console.log(vectorField) updateParticlePositions(vectorField) oldParticlePositionsBuffer.data(oldParticlePositions) newParticlePositionsBuffer.data(newParticlePositions) percentLifeLivedBuffer.data(percentLifeLived) particleColorBuffer.data(particleColors) } // `START_STARS_FADE_IN` constant as defined in sunset.frag const START_STARS_FADE_IN = 0.0 // Pass in progress parameter (x distance) function render(walkerX) { // console.log('time', progress, 'iTime', 5 * progress) const iTime = (walkerX / 20.0 + 0.7) * 5 // Only bother rendering stars if faded in at all // subtract 1 b/c uv and length(skyCol) // if (iTime > START_STARS_FADE_IN - 2) { // Draw points step.bind() step.setColorAttachment(current) pointsProgram .use() .vertices(line) .instancedAttributes(ext, oldParticlePositionsBuffer, [ { type: 'vec2', name: 'oldParticlePos', perInstance: 1 }, ]) .instancedAttributes(ext, newParticlePositionsBuffer, [ { type: 'vec2', name: 'newParticlePos', perInstance: 1 }, ]) .instancedAttributes(ext, particleColorBuffer, [ { type: 'vec3', name: 'particleColor', perInstance: 1 }, ]) .instancedAttributes(ext, percentLifeLivedBuffer, [ { type: 'float', name: 'percentLifeLived', perInstance: 1 }, ]) .uniform('resolution', [canvas.width, canvas.height]) .viewport(canvas.width, canvas.height) .drawInstanced(ext, gl.TRIANGLE_STRIP, 4, particleCount) // Blend step.bind() step.setColorAttachment(blend) current.bind(0) acc.bind(1) blendProgram .use() .resetVerticesInstancing(ext, quad) .vertices(quad) .uniform('resolution', [canvas.width, canvas.height]) .uniformi('current', 0) .uniformi('acc', 1) .viewport(canvas.width, canvas.height) .draw(gl.TRIANGLE_STRIP, 4) // Swap blend and acc let tmp = acc acc = blend blend = tmp // } // Draw acc utils.bindDisplay() acc.bind(0) sunsetProgram .use() .vertices(quadNoUV) .uniform('resolution', [canvas.width, canvas.height]) .uniform('time', iTime) .uniformi('texture', 0) .viewport(canvas.width, canvas.height) .draw(gl.TRIANGLE_STRIP, 4) } return { render, resize, update, setVectorFieldExpression, resize, get localCanvas() { return canvas }, } } FILE: Types_WebGL_LavaQuad.js function LavaQuad(spec) { const { assets } = spec const canvas = document.createElement('canvas') canvas.width = 1024 canvas.height = 1024 gl = canvas.getContext('webgl') if (!gl) { return alert( 'Your browser does not support WebGL. Try switching or updating your browser!', ) } gl.enable(gl.BLEND) gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) const utils = GLUtils(gl) const shaders = assets.shaders const lavaProgram = utils.Program({ vert: shaders.quad_vert, frag: shaders.volcano.lava, }) let t = 0 function update() { t += 0.01 } function draw() { lavaProgram .use() .vertices(utils.quad) .uniform('t', t) .viewport(canvas.width, canvas.height) .draw(gl.TRIANGLE_STRIP, 4) } return { update, draw, name: 'LavaQuad', get canvas() { return canvas }, } } FILE: Types_WebGL_VolcanoSunsetQuad.js let sunsetTime = 0 function VolcanoSunsetQuad(spec) { const { defaultExpression, getSledderPosition, assets } = spec let local = document.createElement('canvas') local.width = innerWidth local.height = innerHeight // What percentage of the screen is visible (since background hides much of it) const visibleFraction = 0.3 gl = local.getContext('webgl') if (!gl) { return alert( 'Your browser does not support WebGL. Try switching or updating your browser!', ) } gl.enable(gl.BLEND) gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) const utils = GLUtils(gl) const ext = utils.InstancingExtension() const line = utils.Vertices(gl.STATIC_DRAW, { vertexId: { type: 'float', data: [0, 1, 2, 3], }, }) const shaders = assets.shaders const quad = utils.quad const blendProgram = utils.Program({ vert: shaders.quad_vert, frag: shaders.volcano.volcano_blend, }) const pointsProgram = utils.Program({ vert: shaders.volcano.volcano_stars_vert, frag: shaders.volcano.volcano_stars_frag, }) const sunsetProgram = utils.Program({ vert: shaders.quad_vert, frag: shaders.volcano.volcano_sunset, }) const particleCount = 100 // [ x, y ] const oldParticlePositions = new Float32Array(particleCount * 2) const newParticlePositions = new Float32Array(particleCount * 2) const particleColors = new Float32Array(particleCount * 3) const percentLifeLived = new Float32Array(particleCount) const lifetimes = new Float32Array(particleCount) // Only used CPU side function genParticleColor() { // const c = Math.random() * 0.5 + 0.5 const c1 = Math.random() * 0.3 + 0.7 const c2 = Math.random() * 0.2 + 0.1 const c3 = Math.random() * 0.1 const scale = 1.0 // Math.pow(Math.random(), 2.0) * 0.8 + 0.2 return [c1 * scale, c2 * scale, c3 * scale] } function createParticleAt(index) { const posIndex = 2 * index const colIndex = 3 * index const x = Math.random() const y = Math.random() * (1 - visibleFraction) oldParticlePositions[posIndex] = x oldParticlePositions[posIndex + 1] = y newParticlePositions[posIndex] = x newParticlePositions[posIndex + 1] = y const col = genParticleColor() particleColors[colIndex] = col[0] particleColors[colIndex + 1] = col[1] particleColors[colIndex + 2] = col[2] const lifetime = Math.random() * 4 + 4 // 5-10 lifetimes[index] = lifetime percentLifeLived[index] = 0 } for (let i = 0; i < particleCount; i++) { createParticleAt(i) } const oldParticlePositionsBuffer = utils.Array( oldParticlePositions, gl.DYNAMIC_DRAW, ) const newParticlePositionsBuffer = utils.Array( newParticlePositions, gl.DYNAMIC_DRAW, ) const percentLifeLivedBuffer = utils.Array(percentLifeLived, gl.DYNAMIC_DRAW) const particleColorBuffer = utils.Array(particleColors, gl.DYNAMIC_DRAW) // const input = document.querySelector('input') const eta = 0.05 t = 0 function updateParticlePositions(vectorField) { for (let i = 0; i < particleCount; i++) { const index = 2 * i const normX = newParticlePositions[index] const normY = newParticlePositions[index + 1] // If out of bounds of canvas or end of life then reset if ( percentLifeLived[i] > 1 || Math.abs(normX) > 1 || Math.abs(normY) > 1 - visibleFraction ) { createParticleAt(i) continue } const x = (normX - 0.5) * 10 const y = (normY - 0.5) * 10 const [dx, dy] = vectorField(x, y, t) const newX = eta * tickDelta * dx + normX const newY = eta * tickDelta * dy + normY // otherwise nudge w/ gradient oldParticlePositions[index] = normX oldParticlePositions[index + 1] = normY newParticlePositions[index] = newX newParticlePositions[index + 1] = newY percentLifeLived[i] += 1 / (lifetimes[i] * ticksPerSecond) } } /* triple buffer swap: render new frame to current store old in acc blend current and acc to blend swap blend and acc display acc */ // TODO: Handle resizing let current = utils.Texture([local.width, local.height], gl.RGBA) let acc = utils.Texture([local.width, local.height], gl.RGBA) let blend = utils.Texture([local.width, local.height], gl.RGBA) const step = utils.Framebuffer() function resize(width, height) { local.width = width local.height = height current.destroy() current = utils.Texture([width, height], gl.RGBA) acc = acc.resize(width, height, gl.RGBA) blend.destroy() blend = utils.Texture([width, height], gl.RGBA) } let last = null let evaluator = math.compile(defaultExpression) function setVectorFieldExpression(text) { try { const e = math.compile(text) e.evaluate({ x: 0, y: 0 }) // Make sure can evaluate properly evaluator = e } catch (err) {} } function vectorField(x, y, t) { const c = evaluator.evaluate({ x, y, t }) try { // Either real or complex return typeof c === 'number' ? [c, 0] : [math.re(c), math.im(c)] } catch (ex) { return [0, 0] } } function update() { const now = performance.now() const delta = last ? now - last : 0 last = now t += delta * 0.00005 // console.log(vectorField) updateParticlePositions(vectorField) oldParticlePositionsBuffer.data(oldParticlePositions) newParticlePositionsBuffer.data(newParticlePositions) percentLifeLivedBuffer.data(percentLifeLived) particleColorBuffer.data(particleColors) // sunsetTime = world.level.sledders[0].transform.x/221 * 12 const x = getSledderPosition() sunsetTime = 12 * Math.exp(-(((x - 221) / 100) ** 2)) } // `START_STARS_FADE_IN` constant as defined in sunset.frag const START_STARS_FADE_IN = 0.0 // Pass in progress parameter (x distance) function render() { // console.log('time', progress, 'iTime', 5 * progress) // const iTime = (world.level.firstWalkerX / 20.0 + 0.7) * 5 const iTime = 10 // Only bother rendering stars if faded in at all // subtract 1 b/c uv and length(skyCol) // Draw points step.bind() step.setColorAttachment(current) pointsProgram .use() .vertices(line) .instancedAttributes(ext, oldParticlePositionsBuffer, [ { type: 'vec2', name: 'oldParticlePos', perInstance: 1 }, ]) .instancedAttributes(ext, newParticlePositionsBuffer, [ { type: 'vec2', name: 'newParticlePos', perInstance: 1 }, ]) .instancedAttributes(ext, particleColorBuffer, [ { type: 'vec3', name: 'particleColor', perInstance: 1 }, ]) .instancedAttributes(ext, percentLifeLivedBuffer, [ { type: 'float', name: 'percentLifeLived', perInstance: 1 }, ]) .uniform('resolution', [local.width, local.height]) .viewport(local.width, local.height) .drawInstanced(ext, gl.TRIANGLE_STRIP, 4, particleCount) // Blend step.bind() step.setColorAttachment(blend) current.bind(0) acc.bind(1) blendProgram .use() .resetVerticesInstancing(ext, quad) .vertices(quad) .uniform('resolution', [local.width, local.height]) .uniformi('current', 0) .uniformi('acc', 1) .viewport(local.width, local.height) .draw(gl.TRIANGLE_STRIP, 4) // Swap blend and acc let tmp = acc acc = blend blend = tmp // Draw acc utils.bindDisplay() acc.bind(0) sunsetProgram .use() .vertices(quad) .uniform('resolution', [innerWidth, innerHeight]) .uniform('time', sunsetTime) .uniformi('stars', 0) .viewport(local.width, local.height) .draw(gl.TRIANGLE_STRIP, 4) } return { render, resize, update, setVectorFieldExpression, resize, get localCanvas() { return local }, } } FILE: Types_WebGL_WaterQuad.js function WaterQuad(spec) { const { assets } = spec const canvas = document.createElement('canvas') canvas.width = 1024 canvas.height = 1024 gl = canvas.getContext('webgl') if (!gl) { return alert( 'Your browser does not support WebGL. Try switching or updating your browser!', ) } gl.enable(gl.BLEND) gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) const utils = GLUtils(gl) const quad = utils.Vertices(gl.STATIC_DRAW, { aCoords: { type: 'vec2', data: [-1, -1, -1, 1, 1, -1, 1, 1], }, aTexCoords: { type: 'vec2', data: [0, 0, 0, 1, 1, 0, 1, 1], }, }) const shaders = assets.shaders const ctx = screen.ctx const waterProgram = utils.Program({ vert: shaders.quad_vert, frag: shaders.lake_frag, }) let t = 0 function update() { t += 0.01 } function draw() { waterProgram .use() .vertices(quad) .uniform('time', t) .viewport(canvas.width, canvas.height) .draw(gl.TRIANGLE_STRIP, 4) } return { update, draw, name: 'WaterQuad', get canvas() { return canvas }, } } FILE: equation-input.js // Native plain-expression input. No rich-text dependency or remote resources. const MathQuill={getInterface(){return { StaticMath(el){let raw='';return {latex(v){if(v===undefined)return raw;raw=v;el.textContent=mathquillToMathJS(String(v));return this}}}, MathField(el,options){const input=document.createElement('input');input.type='text';input.setAttribute('aria-label','Equation in x and t');input.placeholder='Try -x/2, x^2, or sin(x)';input.autocomplete='off';input.spellcheck=false;el.replaceChildren(input);let raw='';const api={latex(v){if(v===undefined)return raw;raw=String(v);input.value=mathquillToMathJS(raw);return api},blur(){input.blur()},focus(){input.focus()}};input.addEventListener('input',()=>{raw=input.value;options.handlers.edit()});input.addEventListener('focus',()=>el.classList.add('mq-focused'));input.addEventListener('blur',()=>el.classList.remove('mq-focused'));return api} }}}; FILE: index.html SineRider · Curvebound
t=0
⚙️
Level name:
N/A
Level nick:
N/A
FPS:
N/A
X
0/20
Goals
Share
Order
Position
Timer
Start
End
X
📝 Hint!
↺
t=
Y=
GO
Skip Cutscene
STOP
Try Again
Level Options
Share this puzzle as a link:
Reset your equation?  This cannot be undone.
Cancel
Reset
Are you sure you want to unlock all levels?
Cancel
Unlock All
Are you sure you want to permanently delete your progress?
Cancel
Reset
x
Graphics Settings
MESSAGE

Complete!

Your equation solved the puzzle in

,

and

.


Show off your solution and see how you rank up against other solvers on Twitter or Reddit.


Think you can do better? Try again!

NEXT

Reddit Command Copied!

The command has been copied in your clipboard.
Please paste it into the daily level thread.
Command:


Open Thread
X


Original game by Hack Club. Source and notices: Hack Club / SineRider
Silent edition · source & license FILE: init-1.js var MQ = MathQuill.getInterface(2) FILE: lodash.js /** * @license * Lodash * Copyright OpenJS Foundation and other contributors * Released under MIT license * Based on Underscore.js 1.8.3 * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors */ (function(){function n(n,t,r){switch(r.length){case 0:return n.call(t);case 1:return n.call(t,r[0]);case 2:return n.call(t,r[0],r[1]);case 3:return n.call(t,r[0],r[1],r[2])}return n.apply(t,r)}function t(n,t,r,e){for(var u=-1,i=null==n?0:n.length;++u-1}function f(n,t,r){for(var e=-1,u=null==n?0:n.length;++e-1;);return r}function L(n,t){for(var r=n.length;r--&&y(t,n[r],0)>-1;);return r}function C(n,t){for(var r=n.length,e=0;r--;)n[r]===t&&++e; return e}function U(n){return"\\"+Yr[n]}function B(n,t){return null==n?X:n[t]}function T(n){return Nr.test(n)}function $(n){return Pr.test(n)}function D(n){for(var t,r=[];!(t=n.next()).done;)r.push(t.value);return r}function M(n){var t=-1,r=Array(n.size);return n.forEach(function(n,e){r[++t]=[e,n]}),r}function F(n,t){return function(r){return n(t(r))}}function N(n,t){for(var r=-1,e=n.length,u=0,i=[];++r>>1,$n=[["ary",mn],["bind",_n],["bindKey",vn],["curry",yn],["curryRight",dn],["flip",jn],["partial",bn],["partialRight",wn],["rearg",xn]],Dn="[object Arguments]",Mn="[object Array]",Fn="[object AsyncFunction]",Nn="[object Boolean]",Pn="[object Date]",qn="[object DOMException]",Zn="[object Error]",Kn="[object Function]",Vn="[object GeneratorFunction]",Gn="[object Map]",Hn="[object Number]",Jn="[object Null]",Yn="[object Object]",Qn="[object Promise]",Xn="[object Proxy]",nt="[object RegExp]",tt="[object Set]",rt="[object String]",et="[object Symbol]",ut="[object Undefined]",it="[object WeakMap]",ot="[object WeakSet]",ft="[object ArrayBuffer]",ct="[object DataView]",at="[object Float32Array]",lt="[object Float64Array]",st="[object Int8Array]",ht="[object Int16Array]",pt="[object Int32Array]",_t="[object Uint8Array]",vt="[object Uint8ClampedArray]",gt="[object Uint16Array]",yt="[object Uint32Array]",dt=/\b__p \+= '';/g,bt=/\b(__p \+=) '' \+/g,wt=/(__e\(.*?\)|\b__t\)) \+\n'';/g,mt=/&(?:amp|lt|gt|quot|#39);/g,xt=/[&<>"']/g,jt=RegExp(mt.source),At=RegExp(xt.source),kt=/<%-([\s\S]+?)%>/g,Ot=/<%([\s\S]+?)%>/g,It=/<%=([\s\S]+?)%>/g,Rt=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,zt=/^\w*$/,Et=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,St=/[\\^$.*+?()[\]{}|]/g,Wt=RegExp(St.source),Lt=/^\s+/,Ct=/\s/,Ut=/\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/,Bt=/\{\n\/\* \[wrapped with (.+)\] \*/,Tt=/,? & /,$t=/[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g,Dt=/[()=,{}\[\]\/\s]/,Mt=/\\(\\)?/g,Ft=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,Nt=/\w*$/,Pt=/^[-+]0x[0-9a-f]+$/i,qt=/^0b[01]+$/i,Zt=/^\[object .+?Constructor\]$/,Kt=/^0o[0-7]+$/i,Vt=/^(?:0|[1-9]\d*)$/,Gt=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,Ht=/($^)/,Jt=/['\n\r\u2028\u2029\\]/g,Yt="\\ud800-\\udfff",Qt="\\u0300-\\u036f",Xt="\\ufe20-\\ufe2f",nr="\\u20d0-\\u20ff",tr=Qt+Xt+nr,rr="\\u2700-\\u27bf",er="a-z\\xdf-\\xf6\\xf8-\\xff",ur="\\xac\\xb1\\xd7\\xf7",ir="\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf",or="\\u2000-\\u206f",fr=" \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000",cr="A-Z\\xc0-\\xd6\\xd8-\\xde",ar="\\ufe0e\\ufe0f",lr=ur+ir+or+fr,sr="['\u2019]",hr="["+Yt+"]",pr="["+lr+"]",_r="["+tr+"]",vr="\\d+",gr="["+rr+"]",yr="["+er+"]",dr="[^"+Yt+lr+vr+rr+er+cr+"]",br="\\ud83c[\\udffb-\\udfff]",wr="(?:"+_r+"|"+br+")",mr="[^"+Yt+"]",xr="(?:\\ud83c[\\udde6-\\uddff]){2}",jr="[\\ud800-\\udbff][\\udc00-\\udfff]",Ar="["+cr+"]",kr="\\u200d",Or="(?:"+yr+"|"+dr+")",Ir="(?:"+Ar+"|"+dr+")",Rr="(?:"+sr+"(?:d|ll|m|re|s|t|ve))?",zr="(?:"+sr+"(?:D|LL|M|RE|S|T|VE))?",Er=wr+"?",Sr="["+ar+"]?",Wr="(?:"+kr+"(?:"+[mr,xr,jr].join("|")+")"+Sr+Er+")*",Lr="\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])",Cr="\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])",Ur=Sr+Er+Wr,Br="(?:"+[gr,xr,jr].join("|")+")"+Ur,Tr="(?:"+[mr+_r+"?",_r,xr,jr,hr].join("|")+")",$r=RegExp(sr,"g"),Dr=RegExp(_r,"g"),Mr=RegExp(br+"(?="+br+")|"+Tr+Ur,"g"),Fr=RegExp([Ar+"?"+yr+"+"+Rr+"(?="+[pr,Ar,"$"].join("|")+")",Ir+"+"+zr+"(?="+[pr,Ar+Or,"$"].join("|")+")",Ar+"?"+Or+"+"+Rr,Ar+"+"+zr,Cr,Lr,vr,Br].join("|"),"g"),Nr=RegExp("["+kr+Yt+tr+ar+"]"),Pr=/[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/,qr=["Array","Buffer","DataView","Date","Error","Float32Array","Float64Array","Function","Int8Array","Int16Array","Int32Array","Map","Math","Object","Promise","RegExp","Set","String","Symbol","TypeError","Uint8Array","Uint8ClampedArray","Uint16Array","Uint32Array","WeakMap","_","clearTimeout","isFinite","parseInt","setTimeout"],Zr=-1,Kr={}; Kr[at]=Kr[lt]=Kr[st]=Kr[ht]=Kr[pt]=Kr[_t]=Kr[vt]=Kr[gt]=Kr[yt]=!0,Kr[Dn]=Kr[Mn]=Kr[ft]=Kr[Nn]=Kr[ct]=Kr[Pn]=Kr[Zn]=Kr[Kn]=Kr[Gn]=Kr[Hn]=Kr[Yn]=Kr[nt]=Kr[tt]=Kr[rt]=Kr[it]=!1;var Vr={};Vr[Dn]=Vr[Mn]=Vr[ft]=Vr[ct]=Vr[Nn]=Vr[Pn]=Vr[at]=Vr[lt]=Vr[st]=Vr[ht]=Vr[pt]=Vr[Gn]=Vr[Hn]=Vr[Yn]=Vr[nt]=Vr[tt]=Vr[rt]=Vr[et]=Vr[_t]=Vr[vt]=Vr[gt]=Vr[yt]=!0,Vr[Zn]=Vr[Kn]=Vr[it]=!1;var Gr={"\xc0":"A","\xc1":"A","\xc2":"A","\xc3":"A","\xc4":"A","\xc5":"A","\xe0":"a","\xe1":"a","\xe2":"a","\xe3":"a","\xe4":"a","\xe5":"a", "\xc7":"C","\xe7":"c","\xd0":"D","\xf0":"d","\xc8":"E","\xc9":"E","\xca":"E","\xcb":"E","\xe8":"e","\xe9":"e","\xea":"e","\xeb":"e","\xcc":"I","\xcd":"I","\xce":"I","\xcf":"I","\xec":"i","\xed":"i","\xee":"i","\xef":"i","\xd1":"N","\xf1":"n","\xd2":"O","\xd3":"O","\xd4":"O","\xd5":"O","\xd6":"O","\xd8":"O","\xf2":"o","\xf3":"o","\xf4":"o","\xf5":"o","\xf6":"o","\xf8":"o","\xd9":"U","\xda":"U","\xdb":"U","\xdc":"U","\xf9":"u","\xfa":"u","\xfb":"u","\xfc":"u","\xdd":"Y","\xfd":"y","\xff":"y","\xc6":"Ae", "\xe6":"ae","\xde":"Th","\xfe":"th","\xdf":"ss","\u0100":"A","\u0102":"A","\u0104":"A","\u0101":"a","\u0103":"a","\u0105":"a","\u0106":"C","\u0108":"C","\u010a":"C","\u010c":"C","\u0107":"c","\u0109":"c","\u010b":"c","\u010d":"c","\u010e":"D","\u0110":"D","\u010f":"d","\u0111":"d","\u0112":"E","\u0114":"E","\u0116":"E","\u0118":"E","\u011a":"E","\u0113":"e","\u0115":"e","\u0117":"e","\u0119":"e","\u011b":"e","\u011c":"G","\u011e":"G","\u0120":"G","\u0122":"G","\u011d":"g","\u011f":"g","\u0121":"g", "\u0123":"g","\u0124":"H","\u0126":"H","\u0125":"h","\u0127":"h","\u0128":"I","\u012a":"I","\u012c":"I","\u012e":"I","\u0130":"I","\u0129":"i","\u012b":"i","\u012d":"i","\u012f":"i","\u0131":"i","\u0134":"J","\u0135":"j","\u0136":"K","\u0137":"k","\u0138":"k","\u0139":"L","\u013b":"L","\u013d":"L","\u013f":"L","\u0141":"L","\u013a":"l","\u013c":"l","\u013e":"l","\u0140":"l","\u0142":"l","\u0143":"N","\u0145":"N","\u0147":"N","\u014a":"N","\u0144":"n","\u0146":"n","\u0148":"n","\u014b":"n","\u014c":"O", "\u014e":"O","\u0150":"O","\u014d":"o","\u014f":"o","\u0151":"o","\u0154":"R","\u0156":"R","\u0158":"R","\u0155":"r","\u0157":"r","\u0159":"r","\u015a":"S","\u015c":"S","\u015e":"S","\u0160":"S","\u015b":"s","\u015d":"s","\u015f":"s","\u0161":"s","\u0162":"T","\u0164":"T","\u0166":"T","\u0163":"t","\u0165":"t","\u0167":"t","\u0168":"U","\u016a":"U","\u016c":"U","\u016e":"U","\u0170":"U","\u0172":"U","\u0169":"u","\u016b":"u","\u016d":"u","\u016f":"u","\u0171":"u","\u0173":"u","\u0174":"W","\u0175":"w", "\u0176":"Y","\u0177":"y","\u0178":"Y","\u0179":"Z","\u017b":"Z","\u017d":"Z","\u017a":"z","\u017c":"z","\u017e":"z","\u0132":"IJ","\u0133":"ij","\u0152":"Oe","\u0153":"oe","\u0149":"'n","\u017f":"s"},Hr={"&":"&","<":"<",">":">",'"':""","'":"'"},Jr={"&":"&","<":"<",">":">",""":'"',"'":"'"},Yr={"\\":"\\","'":"'","\n":"n","\r":"r","\u2028":"u2028","\u2029":"u2029"},Qr=parseFloat,Xr=parseInt,ne="object"==typeof global&&global&&global.Object===Object&&global,te="object"==typeof self&&self&&self.Object===Object&&self,re=ne||te||globalThis,ee="object"==typeof exports&&exports&&!exports.nodeType&&exports,ue=ee&&"object"==typeof module&&module&&!module.nodeType&&module,ie=ue&&ue.exports===ee,oe=ie&&ne.process,fe=function(){ try{var n=ue&&ue.require&&ue.require("util").types;return n?n:oe&&oe.binding&&oe.binding("util")}catch(n){}}(),ce=fe&&fe.isArrayBuffer,ae=fe&&fe.isDate,le=fe&&fe.isMap,se=fe&&fe.isRegExp,he=fe&&fe.isSet,pe=fe&&fe.isTypedArray,_e=m("length"),ve=x(Gr),ge=x(Hr),ye=x(Jr),de=function p(x){function Z(n){if(cc(n)&&!bh(n)&&!(n instanceof Ct)){if(n instanceof Y)return n;if(bl.call(n,"__wrapped__"))return eo(n)}return new Y(n)}function J(){}function Y(n,t){this.__wrapped__=n,this.__actions__=[],this.__chain__=!!t, this.__index__=0,this.__values__=X}function Ct(n){this.__wrapped__=n,this.__actions__=[],this.__dir__=1,this.__filtered__=!1,this.__iteratees__=[],this.__takeCount__=Un,this.__views__=[]}function $t(){var n=new Ct(this.__wrapped__);return n.__actions__=Tu(this.__actions__),n.__dir__=this.__dir__,n.__filtered__=this.__filtered__,n.__iteratees__=Tu(this.__iteratees__),n.__takeCount__=this.__takeCount__,n.__views__=Tu(this.__views__),n}function Yt(){if(this.__filtered__){var n=new Ct(this);n.__dir__=-1, n.__filtered__=!0}else n=this.clone(),n.__dir__*=-1;return n}function Qt(){var n=this.__wrapped__.value(),t=this.__dir__,r=bh(n),e=t<0,u=r?n.length:0,i=Oi(0,u,this.__views__),o=i.start,f=i.end,c=f-o,a=e?f:o-1,l=this.__iteratees__,s=l.length,h=0,p=Hl(c,this.__takeCount__);if(!r||!e&&u==c&&p==c)return wu(n,this.__actions__);var _=[];n:for(;c--&&h-1}function lr(n,t){var r=this.__data__,e=Wr(r,n);return e<0?(++this.size,r.push([n,t])):r[e][1]=t,this}function sr(n){var t=-1,r=null==n?0:n.length;for(this.clear();++t=t?n:t)),n}function Fr(n,t,e,u,i,o){var f,c=t&an,a=t&ln,l=t&sn;if(e&&(f=i?e(n,u,i,o):e(n)),f!==X)return f;if(!fc(n))return n;var s=bh(n);if(s){if(f=zi(n),!c)return Tu(n,f)}else{var h=zs(n),p=h==Kn||h==Vn;if(mh(n))return Iu(n,c);if(h==Yn||h==Dn||p&&!i){if(f=a||p?{}:Ei(n),!c)return a?Mu(n,Ur(f,n)):Du(n,Cr(f,n))}else{if(!Vr[h])return i?n:{};f=Si(n,h,c)}}o||(o=new wr);var _=o.get(n);if(_)return _;o.set(n,f),kh(n)?n.forEach(function(r){f.add(Fr(r,t,e,r,n,o))}):jh(n)&&n.forEach(function(r,u){ f.set(u,Fr(r,t,e,u,n,o))});var v=l?a?di:yi:a?qc:Pc,g=s?X:v(n);return r(g||n,function(r,u){g&&(u=r,r=n[u]),Sr(f,u,Fr(r,t,e,u,n,o))}),f}function Nr(n){var t=Pc(n);return function(r){return Pr(r,n,t)}}function Pr(n,t,r){var e=r.length;if(null==n)return!e;for(n=ll(n);e--;){var u=r[e],i=t[u],o=n[u];if(o===X&&!(u in n)||!i(o))return!1}return!0}function Gr(n,t,r){if("function"!=typeof n)throw new pl(en);return Ws(function(){n.apply(X,r)},t)}function Hr(n,t,r,e){var u=-1,i=o,a=!0,l=n.length,s=[],h=t.length; if(!l)return s;r&&(t=c(t,z(r))),e?(i=f,a=!1):t.length>=tn&&(i=S,a=!1,t=new yr(t));n:for(;++uu?0:u+r), e=e===X||e>u?u:kc(e),e<0&&(e+=u),e=r>e?0:Oc(e);r0&&r(f)?t>1?ee(f,t-1,r,e,u):a(u,f):e||(u[u.length]=f)}return u}function ue(n,t){return n&&bs(n,t,Pc)}function oe(n,t){return n&&ws(n,t,Pc)}function fe(n,t){return i(t,function(t){return uc(n[t])})}function _e(n,t){t=ku(t,n);for(var r=0,e=t.length;null!=n&&rt}function xe(n,t){return null!=n&&bl.call(n,t)}function je(n,t){return null!=n&&t in ll(n)}function Ae(n,t,r){return n>=Hl(t,r)&&n=120&&p.length>=120)?new yr(a&&p):X}p=n[0]; var _=-1,v=l[0];n:for(;++_-1;)f!==n&&Ll.call(f,a,1),Ll.call(n,a,1);return n}function nu(n,t){for(var r=n?t.length:0,e=r-1;r--;){ var u=t[r];if(r==e||u!==i){var i=u;Ci(u)?Ll.call(n,u,1):yu(n,u)}}return n}function tu(n,t){return n+Nl(Ql()*(t-n+1))}function ru(n,t,r,e){for(var u=-1,i=Gl(Fl((t-n)/(r||1)),0),o=il(i);i--;)o[e?i:++u]=n,n+=r;return o}function eu(n,t){var r="";if(!n||t<1||t>Wn)return r;do t%2&&(r+=n),t=Nl(t/2),t&&(n+=n);while(t);return r}function uu(n,t){return Ls(Vi(n,t,La),n+"")}function iu(n){return Ir(ra(n))}function ou(n,t){var r=ra(n);return Xi(r,Mr(t,0,r.length))}function fu(n,t,r,e){if(!fc(n))return n;t=ku(t,n); for(var u=-1,i=t.length,o=i-1,f=n;null!=f&&++uu?0:u+t),r=r>u?u:r,r<0&&(r+=u),u=t>r?0:r-t>>>0,t>>>=0;for(var i=il(u);++e>>1,o=n[i];null!==o&&!bc(o)&&(r?o<=t:o=tn){var s=t?null:ks(n);if(s)return P(s);c=!1,u=S,l=new yr}else l=t?[]:a;n:for(;++e=e?n:au(n,t,r)}function Iu(n,t){if(t)return n.slice();var r=n.length,e=zl?zl(r):new n.constructor(r); return n.copy(e),e}function Ru(n){var t=new n.constructor(n.byteLength);return new Rl(t).set(new Rl(n)),t}function zu(n,t){return new n.constructor(t?Ru(n.buffer):n.buffer,n.byteOffset,n.byteLength)}function Eu(n){var t=new n.constructor(n.source,Nt.exec(n));return t.lastIndex=n.lastIndex,t}function Su(n){return _s?ll(_s.call(n)):{}}function Wu(n,t){return new n.constructor(t?Ru(n.buffer):n.buffer,n.byteOffset,n.length)}function Lu(n,t){if(n!==t){var r=n!==X,e=null===n,u=n===n,i=bc(n),o=t!==X,f=null===t,c=t===t,a=bc(t); if(!f&&!a&&!i&&n>t||i&&o&&c&&!f&&!a||e&&o&&c||!r&&c||!u)return 1;if(!e&&!i&&!a&&n=f)return c;return c*("desc"==r[e]?-1:1)}}return n.index-t.index}function Uu(n,t,r,e){for(var u=-1,i=n.length,o=r.length,f=-1,c=t.length,a=Gl(i-o,0),l=il(c+a),s=!e;++f1?r[u-1]:X,o=u>2?r[2]:X;for(i=n.length>3&&"function"==typeof i?(u--,i):X,o&&Ui(r[0],r[1],o)&&(i=u<3?X:i,u=1),t=ll(t);++e-1?u[i?t[o]:o]:X}}function Yu(n){return gi(function(t){var r=t.length,e=r,u=Y.prototype.thru;for(n&&t.reverse();e--;){var i=t[e];if("function"!=typeof i)throw new pl(en);if(u&&!o&&"wrapper"==bi(i))var o=new Y([],!0)}for(e=o?e:r;++e1&&d.reverse(),s&&cf))return!1;var a=i.get(n),l=i.get(t);if(a&&l)return a==t&&l==n;var s=-1,p=!0,_=r&pn?new yr:X;for(i.set(n,t),i.set(t,n);++s1?"& ":"")+t[e],t=t.join(r>2?", ":" "),n.replace(Ut,"{\n/* [wrapped with "+t+"] */\n")}function Li(n){return bh(n)||dh(n)||!!(Cl&&n&&n[Cl])}function Ci(n,t){var r=typeof n; return t=null==t?Wn:t,!!t&&("number"==r||"symbol"!=r&&Vt.test(n))&&n>-1&&n%1==0&&n0){if(++t>=On)return arguments[0]}else t=0; return n.apply(X,arguments)}}function Xi(n,t){var r=-1,e=n.length,u=e-1;for(t=t===X?e:t;++r=this.__values__.length;return{done:n,value:n?X:this.__values__[this.__index__++]}}function uf(){return this}function of(n){for(var t,r=this;r instanceof J;){var e=eo(r);e.__index__=0,e.__values__=X,t?u.__wrapped__=e:t=e;var u=e;r=r.__wrapped__}return u.__wrapped__=n,t}function ff(){var n=this.__wrapped__;if(n instanceof Ct){var t=n;return this.__actions__.length&&(t=new Ct(this)),t=t.reverse(),t.__actions__.push({func:nf,args:[Eo],thisArg:X}),new Y(t,this.__chain__)}return this.thru(Eo); }function cf(){return wu(this.__wrapped__,this.__actions__)}function af(n,t,r){var e=bh(n)?u:Jr;return r&&Ui(n,t,r)&&(t=X),e(n,mi(t,3))}function lf(n,t){return(bh(n)?i:te)(n,mi(t,3))}function sf(n,t){return ee(yf(n,t),1)}function hf(n,t){return ee(yf(n,t),Sn)}function pf(n,t,r){return r=r===X?1:kc(r),ee(yf(n,t),r)}function _f(n,t){return(bh(n)?r:ys)(n,mi(t,3))}function vf(n,t){return(bh(n)?e:ds)(n,mi(t,3))}function gf(n,t,r,e){n=Hf(n)?n:ra(n),r=r&&!e?kc(r):0;var u=n.length;return r<0&&(r=Gl(u+r,0)), dc(n)?r<=u&&n.indexOf(t,r)>-1:!!u&&y(n,t,r)>-1}function yf(n,t){return(bh(n)?c:Pe)(n,mi(t,3))}function df(n,t,r,e){return null==n?[]:(bh(t)||(t=null==t?[]:[t]),r=e?X:r,bh(r)||(r=null==r?[]:[r]),He(n,t,r))}function bf(n,t,r){var e=bh(n)?l:j,u=arguments.length<3;return e(n,mi(t,4),r,u,ys)}function wf(n,t,r){var e=bh(n)?s:j,u=arguments.length<3;return e(n,mi(t,4),r,u,ds)}function mf(n,t){return(bh(n)?i:te)(n,Uf(mi(t,3)))}function xf(n){return(bh(n)?Ir:iu)(n)}function jf(n,t,r){return t=(r?Ui(n,t,r):t===X)?1:kc(t), (bh(n)?Rr:ou)(n,t)}function Af(n){return(bh(n)?zr:cu)(n)}function kf(n){if(null==n)return 0;if(Hf(n))return dc(n)?V(n):n.length;var t=zs(n);return t==Gn||t==tt?n.size:Me(n).length}function Of(n,t,r){var e=bh(n)?h:lu;return r&&Ui(n,t,r)&&(t=X),e(n,mi(t,3))}function If(n,t){if("function"!=typeof t)throw new pl(en);return n=kc(n),function(){if(--n<1)return t.apply(this,arguments)}}function Rf(n,t,r){return t=r?X:t,t=n&&null==t?n.length:t,ai(n,mn,X,X,X,X,t)}function zf(n,t){var r;if("function"!=typeof t)throw new pl(en); return n=kc(n),function(){return--n>0&&(r=t.apply(this,arguments)),n<=1&&(t=X),r}}function Ef(n,t,r){t=r?X:t;var e=ai(n,yn,X,X,X,X,X,t);return e.placeholder=Ef.placeholder,e}function Sf(n,t,r){t=r?X:t;var e=ai(n,dn,X,X,X,X,X,t);return e.placeholder=Sf.placeholder,e}function Wf(n,t,r){function e(t){var r=h,e=p;return h=p=X,d=t,v=n.apply(e,r)}function u(n){return d=n,g=Ws(f,t),b?e(n):v}function i(n){var r=n-y,e=n-d,u=t-r;return w?Hl(u,_-e):u}function o(n){var r=n-y,e=n-d;return y===X||r>=t||r<0||w&&e>=_; }function f(){var n=fh();return o(n)?c(n):(g=Ws(f,i(n)),X)}function c(n){return g=X,m&&h?e(n):(h=p=X,v)}function a(){g!==X&&As(g),d=0,h=y=p=g=X}function l(){return g===X?v:c(fh())}function s(){var n=fh(),r=o(n);if(h=arguments,p=this,y=n,r){if(g===X)return u(y);if(w)return As(g),g=Ws(f,t),e(y)}return g===X&&(g=Ws(f,t)),v}var h,p,_,v,g,y,d=0,b=!1,w=!1,m=!0;if("function"!=typeof n)throw new pl(en);return t=Ic(t)||0,fc(r)&&(b=!!r.leading,w="maxWait"in r,_=w?Gl(Ic(r.maxWait)||0,t):_,m="trailing"in r?!!r.trailing:m), s.cancel=a,s.flush=l,s}function Lf(n){return ai(n,jn)}function Cf(n,t){if("function"!=typeof n||null!=t&&"function"!=typeof t)throw new pl(en);var r=function(){var e=arguments,u=t?t.apply(this,e):e[0],i=r.cache;if(i.has(u))return i.get(u);var o=n.apply(this,e);return r.cache=i.set(u,o)||i,o};return r.cache=new(Cf.Cache||sr),r}function Uf(n){if("function"!=typeof n)throw new pl(en);return function(){var t=arguments;switch(t.length){case 0:return!n.call(this);case 1:return!n.call(this,t[0]);case 2: return!n.call(this,t[0],t[1]);case 3:return!n.call(this,t[0],t[1],t[2])}return!n.apply(this,t)}}function Bf(n){return zf(2,n)}function Tf(n,t){if("function"!=typeof n)throw new pl(en);return t=t===X?t:kc(t),uu(n,t)}function $f(t,r){if("function"!=typeof t)throw new pl(en);return r=null==r?0:Gl(kc(r),0),uu(function(e){var u=e[r],i=Ou(e,0,r);return u&&a(i,u),n(t,this,i)})}function Df(n,t,r){var e=!0,u=!0;if("function"!=typeof n)throw new pl(en);return fc(r)&&(e="leading"in r?!!r.leading:e,u="trailing"in r?!!r.trailing:u), Wf(n,t,{leading:e,maxWait:t,trailing:u})}function Mf(n){return Rf(n,1)}function Ff(n,t){return ph(Au(t),n)}function Nf(){if(!arguments.length)return[];var n=arguments[0];return bh(n)?n:[n]}function Pf(n){return Fr(n,sn)}function qf(n,t){return t="function"==typeof t?t:X,Fr(n,sn,t)}function Zf(n){return Fr(n,an|sn)}function Kf(n,t){return t="function"==typeof t?t:X,Fr(n,an|sn,t)}function Vf(n,t){return null==t||Pr(n,t,Pc(t))}function Gf(n,t){return n===t||n!==n&&t!==t}function Hf(n){return null!=n&&oc(n.length)&&!uc(n); }function Jf(n){return cc(n)&&Hf(n)}function Yf(n){return n===!0||n===!1||cc(n)&&we(n)==Nn}function Qf(n){return cc(n)&&1===n.nodeType&&!gc(n)}function Xf(n){if(null==n)return!0;if(Hf(n)&&(bh(n)||"string"==typeof n||"function"==typeof n.splice||mh(n)||Oh(n)||dh(n)))return!n.length;var t=zs(n);if(t==Gn||t==tt)return!n.size;if(Mi(n))return!Me(n).length;for(var r in n)if(bl.call(n,r))return!1;return!0}function nc(n,t){return Se(n,t)}function tc(n,t,r){r="function"==typeof r?r:X;var e=r?r(n,t):X;return e===X?Se(n,t,X,r):!!e; }function rc(n){if(!cc(n))return!1;var t=we(n);return t==Zn||t==qn||"string"==typeof n.message&&"string"==typeof n.name&&!gc(n)}function ec(n){return"number"==typeof n&&Zl(n)}function uc(n){if(!fc(n))return!1;var t=we(n);return t==Kn||t==Vn||t==Fn||t==Xn}function ic(n){return"number"==typeof n&&n==kc(n)}function oc(n){return"number"==typeof n&&n>-1&&n%1==0&&n<=Wn}function fc(n){var t=typeof n;return null!=n&&("object"==t||"function"==t)}function cc(n){return null!=n&&"object"==typeof n}function ac(n,t){ return n===t||Ce(n,t,ji(t))}function lc(n,t,r){return r="function"==typeof r?r:X,Ce(n,t,ji(t),r)}function sc(n){return vc(n)&&n!=+n}function hc(n){if(Es(n))throw new fl(rn);return Ue(n)}function pc(n){return null===n}function _c(n){return null==n}function vc(n){return"number"==typeof n||cc(n)&&we(n)==Hn}function gc(n){if(!cc(n)||we(n)!=Yn)return!1;var t=El(n);if(null===t)return!0;var r=bl.call(t,"constructor")&&t.constructor;return"function"==typeof r&&r instanceof r&&dl.call(r)==jl}function yc(n){ return ic(n)&&n>=-Wn&&n<=Wn}function dc(n){return"string"==typeof n||!bh(n)&&cc(n)&&we(n)==rt}function bc(n){return"symbol"==typeof n||cc(n)&&we(n)==et}function wc(n){return n===X}function mc(n){return cc(n)&&zs(n)==it}function xc(n){return cc(n)&&we(n)==ot}function jc(n){if(!n)return[];if(Hf(n))return dc(n)?G(n):Tu(n);if(Ul&&n[Ul])return D(n[Ul]());var t=zs(n);return(t==Gn?M:t==tt?P:ra)(n)}function Ac(n){if(!n)return 0===n?n:0;if(n=Ic(n),n===Sn||n===-Sn){return(n<0?-1:1)*Ln}return n===n?n:0}function kc(n){ var t=Ac(n),r=t%1;return t===t?r?t-r:t:0}function Oc(n){return n?Mr(kc(n),0,Un):0}function Ic(n){if("number"==typeof n)return n;if(bc(n))return Cn;if(fc(n)){var t="function"==typeof n.valueOf?n.valueOf():n;n=fc(t)?t+"":t}if("string"!=typeof n)return 0===n?n:+n;n=R(n);var r=qt.test(n);return r||Kt.test(n)?Xr(n.slice(2),r?2:8):Pt.test(n)?Cn:+n}function Rc(n){return $u(n,qc(n))}function zc(n){return n?Mr(kc(n),-Wn,Wn):0===n?n:0}function Ec(n){return null==n?"":vu(n)}function Sc(n,t){var r=gs(n);return null==t?r:Cr(r,t); }function Wc(n,t){return v(n,mi(t,3),ue)}function Lc(n,t){return v(n,mi(t,3),oe)}function Cc(n,t){return null==n?n:bs(n,mi(t,3),qc)}function Uc(n,t){return null==n?n:ws(n,mi(t,3),qc)}function Bc(n,t){return n&&ue(n,mi(t,3))}function Tc(n,t){return n&&oe(n,mi(t,3))}function $c(n){return null==n?[]:fe(n,Pc(n))}function Dc(n){return null==n?[]:fe(n,qc(n))}function Mc(n,t,r){var e=null==n?X:_e(n,t);return e===X?r:e}function Fc(n,t){return null!=n&&Ri(n,t,xe)}function Nc(n,t){return null!=n&&Ri(n,t,je); }function Pc(n){return Hf(n)?Or(n):Me(n)}function qc(n){return Hf(n)?Or(n,!0):Fe(n)}function Zc(n,t){var r={};return t=mi(t,3),ue(n,function(n,e,u){Br(r,t(n,e,u),n)}),r}function Kc(n,t){var r={};return t=mi(t,3),ue(n,function(n,e,u){Br(r,e,t(n,e,u))}),r}function Vc(n,t){return Gc(n,Uf(mi(t)))}function Gc(n,t){if(null==n)return{};var r=c(di(n),function(n){return[n]});return t=mi(t),Ye(n,r,function(n,r){return t(n,r[0])})}function Hc(n,t,r){t=ku(t,n);var e=-1,u=t.length;for(u||(u=1,n=X);++et){ var e=n;n=t,t=e}if(r||n%1||t%1){var u=Ql();return Hl(n+u*(t-n+Qr("1e-"+((u+"").length-1))),t)}return tu(n,t)}function fa(n){return Qh(Ec(n).toLowerCase())}function ca(n){return n=Ec(n),n&&n.replace(Gt,ve).replace(Dr,"")}function aa(n,t,r){n=Ec(n),t=vu(t);var e=n.length;r=r===X?e:Mr(kc(r),0,e);var u=r;return r-=t.length,r>=0&&n.slice(r,u)==t}function la(n){return n=Ec(n),n&&At.test(n)?n.replace(xt,ge):n}function sa(n){return n=Ec(n),n&&Wt.test(n)?n.replace(St,"\\$&"):n}function ha(n,t,r){n=Ec(n),t=kc(t); var e=t?V(n):0;if(!t||e>=t)return n;var u=(t-e)/2;return ri(Nl(u),r)+n+ri(Fl(u),r)}function pa(n,t,r){n=Ec(n),t=kc(t);var e=t?V(n):0;return t&&e>>0)?(n=Ec(n),n&&("string"==typeof t||null!=t&&!Ah(t))&&(t=vu(t),!t&&T(n))?Ou(G(n),0,r):n.split(t,r)):[]}function ba(n,t,r){return n=Ec(n),r=null==r?0:Mr(kc(r),0,n.length),t=vu(t),n.slice(r,r+t.length)==t}function wa(n,t,r){var e=Z.templateSettings;r&&Ui(n,t,r)&&(t=X),n=Ec(n),t=Sh({},t,e,li);var u,i,o=Sh({},t.imports,e.imports,li),f=Pc(o),c=E(o,f),a=0,l=t.interpolate||Ht,s="__p += '",h=sl((t.escape||Ht).source+"|"+l.source+"|"+(l===It?Ft:Ht).source+"|"+(t.evaluate||Ht).source+"|$","g"),p="//# sourceURL="+(bl.call(t,"sourceURL")?(t.sourceURL+"").replace(/\s/g," "):"lodash.templateSources["+ ++Zr+"]")+"\n"; n.replace(h,function(t,r,e,o,f,c){return e||(e=o),s+=n.slice(a,c).replace(Jt,U),r&&(u=!0,s+="' +\n__e("+r+") +\n'"),f&&(i=!0,s+="';\n"+f+";\n__p += '"),e&&(s+="' +\n((__t = ("+e+")) == null ? '' : __t) +\n'"),a=c+t.length,t}),s+="';\n";var _=bl.call(t,"variable")&&t.variable;if(_){if(Dt.test(_))throw new fl(un)}else s="with (obj) {\n"+s+"\n}\n";s=(i?s.replace(dt,""):s).replace(bt,"$1").replace(wt,"$1;"),s="function("+(_||"obj")+") {\n"+(_?"":"obj || (obj = {});\n")+"var __t, __p = ''"+(u?", __e = _.escape":"")+(i?", __j = Array.prototype.join;\nfunction print() { __p += __j.call(arguments, '') }\n":";\n")+s+"return __p\n}"; var v=Xh(function(){return cl(f,p+"return "+s).apply(X,c)});if(v.source=s,rc(v))throw v;return v}function ma(n){return Ec(n).toLowerCase()}function xa(n){return Ec(n).toUpperCase()}function ja(n,t,r){if(n=Ec(n),n&&(r||t===X))return R(n);if(!n||!(t=vu(t)))return n;var e=G(n),u=G(t);return Ou(e,W(e,u),L(e,u)+1).join("")}function Aa(n,t,r){if(n=Ec(n),n&&(r||t===X))return n.slice(0,H(n)+1);if(!n||!(t=vu(t)))return n;var e=G(n);return Ou(e,0,L(e,G(t))+1).join("")}function ka(n,t,r){if(n=Ec(n),n&&(r||t===X))return n.replace(Lt,""); if(!n||!(t=vu(t)))return n;var e=G(n);return Ou(e,W(e,G(t))).join("")}function Oa(n,t){var r=An,e=kn;if(fc(t)){var u="separator"in t?t.separator:u;r="length"in t?kc(t.length):r,e="omission"in t?vu(t.omission):e}n=Ec(n);var i=n.length;if(T(n)){var o=G(n);i=o.length}if(r>=i)return n;var f=r-V(e);if(f<1)return e;var c=o?Ou(o,0,f).join(""):n.slice(0,f);if(u===X)return c+e;if(o&&(f+=c.length-f),Ah(u)){if(n.slice(f).search(u)){var a,l=c;for(u.global||(u=sl(u.source,Ec(Nt.exec(u))+"g")),u.lastIndex=0;a=u.exec(l);)var s=a.index; c=c.slice(0,s===X?f:s)}}else if(n.indexOf(vu(u),f)!=f){var h=c.lastIndexOf(u);h>-1&&(c=c.slice(0,h))}return c+e}function Ia(n){return n=Ec(n),n&&jt.test(n)?n.replace(mt,ye):n}function Ra(n,t,r){return n=Ec(n),t=r?X:t,t===X?$(n)?Q(n):_(n):n.match(t)||[]}function za(t){var r=null==t?0:t.length,e=mi();return t=r?c(t,function(n){if("function"!=typeof n[1])throw new pl(en);return[e(n[0]),n[1]]}):[],uu(function(e){for(var u=-1;++uWn)return[];var r=Un,e=Hl(n,Un);t=mi(t),n-=Un;for(var u=O(e,t);++r1?n[t-1]:X;return r="function"==typeof r?(n.pop(), r):X,Ho(n,r)}),Qs=gi(function(n){var t=n.length,r=t?n[0]:0,e=this.__wrapped__,u=function(t){return Tr(t,n)};return!(t>1||this.__actions__.length)&&e instanceof Ct&&Ci(r)?(e=e.slice(r,+r+(t?1:0)),e.__actions__.push({func:nf,args:[u],thisArg:X}),new Y(e,this.__chain__).thru(function(n){return t&&!n.length&&n.push(X),n})):this.thru(u)}),Xs=Fu(function(n,t,r){bl.call(n,r)?++n[r]:Br(n,r,1)}),nh=Ju(ho),th=Ju(po),rh=Fu(function(n,t,r){bl.call(n,r)?n[r].push(t):Br(n,r,[t])}),eh=uu(function(t,r,e){var u=-1,i="function"==typeof r,o=Hf(t)?il(t.length):[]; return ys(t,function(t){o[++u]=i?n(r,t,e):Ie(t,r,e)}),o}),uh=Fu(function(n,t,r){Br(n,r,t)}),ih=Fu(function(n,t,r){n[r?0:1].push(t)},function(){return[[],[]]}),oh=uu(function(n,t){if(null==n)return[];var r=t.length;return r>1&&Ui(n,t[0],t[1])?t=[]:r>2&&Ui(t[0],t[1],t[2])&&(t=[t[0]]),He(n,ee(t,1),[])}),fh=Dl||function(){return re.Date.now()},ch=uu(function(n,t,r){var e=_n;if(r.length){var u=N(r,wi(ch));e|=bn}return ai(n,e,t,r,u)}),ah=uu(function(n,t,r){var e=_n|vn;if(r.length){var u=N(r,wi(ah));e|=bn; }return ai(t,e,n,r,u)}),lh=uu(function(n,t){return Gr(n,1,t)}),sh=uu(function(n,t,r){return Gr(n,Ic(t)||0,r)});Cf.Cache=sr;var hh=js(function(t,r){r=1==r.length&&bh(r[0])?c(r[0],z(mi())):c(ee(r,1),z(mi()));var e=r.length;return uu(function(u){for(var i=-1,o=Hl(u.length,e);++i=t}),dh=Re(function(){return arguments}())?Re:function(n){return cc(n)&&bl.call(n,"callee")&&!Wl.call(n,"callee")},bh=il.isArray,wh=ce?z(ce):ze,mh=ql||qa,xh=ae?z(ae):Ee,jh=le?z(le):Le,Ah=se?z(se):Be,kh=he?z(he):Te,Oh=pe?z(pe):$e,Ih=ii(Ne),Rh=ii(function(n,t){return n<=t}),zh=Nu(function(n,t){if(Mi(t)||Hf(t))return $u(t,Pc(t),n),X;for(var r in t)bl.call(t,r)&&Sr(n,r,t[r])}),Eh=Nu(function(n,t){$u(t,qc(t),n)}),Sh=Nu(function(n,t,r,e){$u(t,qc(t),n,e)}),Wh=Nu(function(n,t,r,e){$u(t,Pc(t),n,e); }),Lh=gi(Tr),Ch=uu(function(n,t){n=ll(n);var r=-1,e=t.length,u=e>2?t[2]:X;for(u&&Ui(t[0],t[1],u)&&(e=1);++r1),t}),$u(n,di(n),r),e&&(r=Fr(r,an|ln|sn,hi));for(var u=t.length;u--;)yu(r,t[u]);return r}),Nh=gi(function(n,t){return null==n?{}:Je(n,t)}),Ph=ci(Pc),qh=ci(qc),Zh=Vu(function(n,t,r){return t=t.toLowerCase(),n+(r?fa(t):t)}),Kh=Vu(function(n,t,r){return n+(r?"-":"")+t.toLowerCase()}),Vh=Vu(function(n,t,r){return n+(r?" ":"")+t.toLowerCase()}),Gh=Ku("toLowerCase"),Hh=Vu(function(n,t,r){ return n+(r?"_":"")+t.toLowerCase()}),Jh=Vu(function(n,t,r){return n+(r?" ":"")+Qh(t)}),Yh=Vu(function(n,t,r){return n+(r?" ":"")+t.toUpperCase()}),Qh=Ku("toUpperCase"),Xh=uu(function(t,r){try{return n(t,X,r)}catch(n){return rc(n)?n:new fl(n)}}),np=gi(function(n,t){return r(t,function(t){t=no(t),Br(n,t,ch(n[t],n))}),n}),tp=Yu(),rp=Yu(!0),ep=uu(function(n,t){return function(r){return Ie(r,n,t)}}),up=uu(function(n,t){return function(r){return Ie(n,r,t)}}),ip=ti(c),op=ti(u),fp=ti(h),cp=ui(),ap=ui(!0),lp=ni(function(n,t){ return n+t},0),sp=fi("ceil"),hp=ni(function(n,t){return n/t},1),pp=fi("floor"),_p=ni(function(n,t){return n*t},1),vp=fi("round"),gp=ni(function(n,t){return n-t},0);return Z.after=If,Z.ary=Rf,Z.assign=zh,Z.assignIn=Eh,Z.assignInWith=Sh,Z.assignWith=Wh,Z.at=Lh,Z.before=zf,Z.bind=ch,Z.bindAll=np,Z.bindKey=ah,Z.castArray=Nf,Z.chain=Qo,Z.chunk=uo,Z.compact=io,Z.concat=oo,Z.cond=za,Z.conforms=Ea,Z.constant=Sa,Z.countBy=Xs,Z.create=Sc,Z.curry=Ef,Z.curryRight=Sf,Z.debounce=Wf,Z.defaults=Ch,Z.defaultsDeep=Uh, Z.defer=lh,Z.delay=sh,Z.difference=Us,Z.differenceBy=Bs,Z.differenceWith=Ts,Z.drop=fo,Z.dropRight=co,Z.dropRightWhile=ao,Z.dropWhile=lo,Z.fill=so,Z.filter=lf,Z.flatMap=sf,Z.flatMapDeep=hf,Z.flatMapDepth=pf,Z.flatten=_o,Z.flattenDeep=vo,Z.flattenDepth=go,Z.flip=Lf,Z.flow=tp,Z.flowRight=rp,Z.fromPairs=yo,Z.functions=$c,Z.functionsIn=Dc,Z.groupBy=rh,Z.initial=mo,Z.intersection=$s,Z.intersectionBy=Ds,Z.intersectionWith=Ms,Z.invert=Bh,Z.invertBy=Th,Z.invokeMap=eh,Z.iteratee=Ca,Z.keyBy=uh,Z.keys=Pc,Z.keysIn=qc, Z.map=yf,Z.mapKeys=Zc,Z.mapValues=Kc,Z.matches=Ua,Z.matchesProperty=Ba,Z.memoize=Cf,Z.merge=Dh,Z.mergeWith=Mh,Z.method=ep,Z.methodOf=up,Z.mixin=Ta,Z.negate=Uf,Z.nthArg=Ma,Z.omit=Fh,Z.omitBy=Vc,Z.once=Bf,Z.orderBy=df,Z.over=ip,Z.overArgs=hh,Z.overEvery=op,Z.overSome=fp,Z.partial=ph,Z.partialRight=_h,Z.partition=ih,Z.pick=Nh,Z.pickBy=Gc,Z.property=Fa,Z.propertyOf=Na,Z.pull=Fs,Z.pullAll=Oo,Z.pullAllBy=Io,Z.pullAllWith=Ro,Z.pullAt=Ns,Z.range=cp,Z.rangeRight=ap,Z.rearg=vh,Z.reject=mf,Z.remove=zo,Z.rest=Tf, Z.reverse=Eo,Z.sampleSize=jf,Z.set=Jc,Z.setWith=Yc,Z.shuffle=Af,Z.slice=So,Z.sortBy=oh,Z.sortedUniq=$o,Z.sortedUniqBy=Do,Z.split=da,Z.spread=$f,Z.tail=Mo,Z.take=Fo,Z.takeRight=No,Z.takeRightWhile=Po,Z.takeWhile=qo,Z.tap=Xo,Z.throttle=Df,Z.thru=nf,Z.toArray=jc,Z.toPairs=Ph,Z.toPairsIn=qh,Z.toPath=Ha,Z.toPlainObject=Rc,Z.transform=Qc,Z.unary=Mf,Z.union=Ps,Z.unionBy=qs,Z.unionWith=Zs,Z.uniq=Zo,Z.uniqBy=Ko,Z.uniqWith=Vo,Z.unset=Xc,Z.unzip=Go,Z.unzipWith=Ho,Z.update=na,Z.updateWith=ta,Z.values=ra,Z.valuesIn=ea, Z.without=Ks,Z.words=Ra,Z.wrap=Ff,Z.xor=Vs,Z.xorBy=Gs,Z.xorWith=Hs,Z.zip=Js,Z.zipObject=Jo,Z.zipObjectDeep=Yo,Z.zipWith=Ys,Z.entries=Ph,Z.entriesIn=qh,Z.extend=Eh,Z.extendWith=Sh,Ta(Z,Z),Z.add=lp,Z.attempt=Xh,Z.camelCase=Zh,Z.capitalize=fa,Z.ceil=sp,Z.clamp=ua,Z.clone=Pf,Z.cloneDeep=Zf,Z.cloneDeepWith=Kf,Z.cloneWith=qf,Z.conformsTo=Vf,Z.deburr=ca,Z.defaultTo=Wa,Z.divide=hp,Z.endsWith=aa,Z.eq=Gf,Z.escape=la,Z.escapeRegExp=sa,Z.every=af,Z.find=nh,Z.findIndex=ho,Z.findKey=Wc,Z.findLast=th,Z.findLastIndex=po, Z.findLastKey=Lc,Z.floor=pp,Z.forEach=_f,Z.forEachRight=vf,Z.forIn=Cc,Z.forInRight=Uc,Z.forOwn=Bc,Z.forOwnRight=Tc,Z.get=Mc,Z.gt=gh,Z.gte=yh,Z.has=Fc,Z.hasIn=Nc,Z.head=bo,Z.identity=La,Z.includes=gf,Z.indexOf=wo,Z.inRange=ia,Z.invoke=$h,Z.isArguments=dh,Z.isArray=bh,Z.isArrayBuffer=wh,Z.isArrayLike=Hf,Z.isArrayLikeObject=Jf,Z.isBoolean=Yf,Z.isBuffer=mh,Z.isDate=xh,Z.isElement=Qf,Z.isEmpty=Xf,Z.isEqual=nc,Z.isEqualWith=tc,Z.isError=rc,Z.isFinite=ec,Z.isFunction=uc,Z.isInteger=ic,Z.isLength=oc,Z.isMap=jh, Z.isMatch=ac,Z.isMatchWith=lc,Z.isNaN=sc,Z.isNative=hc,Z.isNil=_c,Z.isNull=pc,Z.isNumber=vc,Z.isObject=fc,Z.isObjectLike=cc,Z.isPlainObject=gc,Z.isRegExp=Ah,Z.isSafeInteger=yc,Z.isSet=kh,Z.isString=dc,Z.isSymbol=bc,Z.isTypedArray=Oh,Z.isUndefined=wc,Z.isWeakMap=mc,Z.isWeakSet=xc,Z.join=xo,Z.kebabCase=Kh,Z.last=jo,Z.lastIndexOf=Ao,Z.lowerCase=Vh,Z.lowerFirst=Gh,Z.lt=Ih,Z.lte=Rh,Z.max=Ya,Z.maxBy=Qa,Z.mean=Xa,Z.meanBy=nl,Z.min=tl,Z.minBy=rl,Z.stubArray=Pa,Z.stubFalse=qa,Z.stubObject=Za,Z.stubString=Ka, Z.stubTrue=Va,Z.multiply=_p,Z.nth=ko,Z.noConflict=$a,Z.noop=Da,Z.now=fh,Z.pad=ha,Z.padEnd=pa,Z.padStart=_a,Z.parseInt=va,Z.random=oa,Z.reduce=bf,Z.reduceRight=wf,Z.repeat=ga,Z.replace=ya,Z.result=Hc,Z.round=vp,Z.runInContext=p,Z.sample=xf,Z.size=kf,Z.snakeCase=Hh,Z.some=Of,Z.sortedIndex=Wo,Z.sortedIndexBy=Lo,Z.sortedIndexOf=Co,Z.sortedLastIndex=Uo,Z.sortedLastIndexBy=Bo,Z.sortedLastIndexOf=To,Z.startCase=Jh,Z.startsWith=ba,Z.subtract=gp,Z.sum=el,Z.sumBy=ul,Z.template=wa,Z.times=Ga,Z.toFinite=Ac,Z.toInteger=kc, Z.toLength=Oc,Z.toLower=ma,Z.toNumber=Ic,Z.toSafeInteger=zc,Z.toString=Ec,Z.toUpper=xa,Z.trim=ja,Z.trimEnd=Aa,Z.trimStart=ka,Z.truncate=Oa,Z.unescape=Ia,Z.uniqueId=Ja,Z.upperCase=Yh,Z.upperFirst=Qh,Z.each=_f,Z.eachRight=vf,Z.first=bo,Ta(Z,function(){var n={};return ue(Z,function(t,r){bl.call(Z.prototype,r)||(n[r]=t)}),n}(),{chain:!1}),Z.VERSION=nn,r(["bind","bindKey","curry","curryRight","partial","partialRight"],function(n){Z[n].placeholder=Z}),r(["drop","take"],function(n,t){Ct.prototype[n]=function(r){ r=r===X?1:Gl(kc(r),0);var e=this.__filtered__&&!t?new Ct(this):this.clone();return e.__filtered__?e.__takeCount__=Hl(r,e.__takeCount__):e.__views__.push({size:Hl(r,Un),type:n+(e.__dir__<0?"Right":"")}),e},Ct.prototype[n+"Right"]=function(t){return this.reverse()[n](t).reverse()}}),r(["filter","map","takeWhile"],function(n,t){var r=t+1,e=r==Rn||r==En;Ct.prototype[n]=function(n){var t=this.clone();return t.__iteratees__.push({iteratee:mi(n,3),type:r}),t.__filtered__=t.__filtered__||e,t}}),r(["head","last"],function(n,t){ var r="take"+(t?"Right":"");Ct.prototype[n]=function(){return this[r](1).value()[0]}}),r(["initial","tail"],function(n,t){var r="drop"+(t?"":"Right");Ct.prototype[n]=function(){return this.__filtered__?new Ct(this):this[r](1)}}),Ct.prototype.compact=function(){return this.filter(La)},Ct.prototype.find=function(n){return this.filter(n).head()},Ct.prototype.findLast=function(n){return this.reverse().find(n)},Ct.prototype.invokeMap=uu(function(n,t){return"function"==typeof n?new Ct(this):this.map(function(r){ return Ie(r,n,t)})}),Ct.prototype.reject=function(n){return this.filter(Uf(mi(n)))},Ct.prototype.slice=function(n,t){n=kc(n);var r=this;return r.__filtered__&&(n>0||t<0)?new Ct(r):(n<0?r=r.takeRight(-n):n&&(r=r.drop(n)),t!==X&&(t=kc(t),r=t<0?r.dropRight(-t):r.take(t-n)),r)},Ct.prototype.takeRightWhile=function(n){return this.reverse().takeWhile(n).reverse()},Ct.prototype.toArray=function(){return this.take(Un)},ue(Ct.prototype,function(n,t){var r=/^(?:filter|find|map|reject)|While$/.test(t),e=/^(?:head|last)$/.test(t),u=Z[e?"take"+("last"==t?"Right":""):t],i=e||/^find/.test(t); u&&(Z.prototype[t]=function(){var t=this.__wrapped__,o=e?[1]:arguments,f=t instanceof Ct,c=o[0],l=f||bh(t),s=function(n){var t=u.apply(Z,a([n],o));return e&&h?t[0]:t};l&&r&&"function"==typeof c&&1!=c.length&&(f=l=!1);var h=this.__chain__,p=!!this.__actions__.length,_=i&&!h,v=f&&!p;if(!i&&l){t=v?t:new Ct(this);var g=n.apply(t,o);return g.__actions__.push({func:nf,args:[s],thisArg:X}),new Y(g,h)}return _&&v?n.apply(this,o):(g=this.thru(s),_?e?g.value()[0]:g.value():g)})}),r(["pop","push","shift","sort","splice","unshift"],function(n){ var t=_l[n],r=/^(?:push|sort|unshift)$/.test(n)?"tap":"thru",e=/^(?:pop|shift)$/.test(n);Z.prototype[n]=function(){var n=arguments;if(e&&!this.__chain__){var u=this.value();return t.apply(bh(u)?u:[],n)}return this[r](function(r){return t.apply(bh(r)?r:[],n)})}}),ue(Ct.prototype,function(n,t){var r=Z[t];if(r){var e=r.name+"";bl.call(fs,e)||(fs[e]=[]),fs[e].push({name:t,func:r})}}),fs[Qu(X,vn).name]=[{name:"wrapper",func:X}],Ct.prototype.clone=$t,Ct.prototype.reverse=Yt,Ct.prototype.value=Qt,Z.prototype.at=Qs, Z.prototype.chain=tf,Z.prototype.commit=rf,Z.prototype.next=ef,Z.prototype.plant=of,Z.prototype.reverse=ff,Z.prototype.toJSON=Z.prototype.valueOf=Z.prototype.value=cf,Z.prototype.first=Z.prototype.head,Ul&&(Z.prototype[Ul]=uf),Z},be=de();"function"==typeof define&&"object"==typeof define.amd&&define.amd?(re._=be,define(function(){return be})):ue?((ue.exports=be)._=be,ee._=be):re._=be}).call(this); FILE: math.js /** * math.js * https://github.com/josdejong/mathjs * * Math.js is an extensive math library for JavaScript and Node.js, * It features real and complex numbers, units, matrices, a large set of * mathematical functions, and a flexible expression parser. * * @version 7.3.0 * @date 2020-09-26 * * @license * Copyright (C) 2013-2020 Jos de Jong * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy * of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.math=t():e.math=t()}(this,function(){return n={},i.m=r=[function(e,t,r){"use strict";r.d(t,"a",function(){return n}),r.d(t,"b",function(){return i}),r.d(t,"c",function(){return o});r(2);var a=r(3);function n(r,n,i,e){function t(e){var t=Object(a.j)(e,n.map(o));return function(e,t,r){if(!t.filter(function(e){return!((t=e)&&"?"===t[0]);var t}).every(function(e){return void 0!==r[e]})){var n=t.filter(function(e){return void 0===r[e]});throw new Error('Cannot create function "'.concat(e,'", ')+"some dependencies are missing: ".concat(n.map(function(e){return'"'.concat(e,'"')}).join(", "),"."))}}(r,n,e),i(t)}return t.isFactory=!0,t.fn=r,t.dependencies=n.slice().sort(),e&&(t.meta=e),t}function i(e){return"function"==typeof e&&"string"==typeof e.fn&&Array.isArray(e.dependencies)}function o(e){return e&&"?"===e[0]?e.slice(1):e}},function(e,t,r){"use strict";function n(e){return(n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function i(e){return"number"==typeof e}function a(e){return e&&!0===e.constructor.prototype.isBigNumber||!1}function o(e){return e&&"object"===n(e)&&!0===Object.getPrototypeOf(e).isComplex||!1}function s(e){return e&&"object"===n(e)&&!0===Object.getPrototypeOf(e).isFraction||!1}function u(e){return e&&!0===e.constructor.prototype.isUnit||!1}function c(e){return"string"==typeof e}r.d(t,"y",function(){return i}),r.d(t,"e",function(){return a}),r.d(t,"j",function(){return o}),r.d(t,"o",function(){return s}),r.d(t,"L",function(){return u}),r.d(t,"I",function(){return c}),r.d(t,"b",function(){return f}),r.d(t,"v",function(){return l}),r.d(t,"i",function(){return p}),r.d(t,"n",function(){return m}),r.d(t,"H",function(){return h}),r.d(t,"D",function(){return d}),r.d(t,"t",function(){return y}),r.d(t,"g",function(){return g}),r.d(t,"G",function(){return b}),r.d(t,"s",function(){return v}),r.d(t,"p",function(){return x}),r.d(t,"m",function(){return w}),r.d(t,"F",function(){return N}),r.d(t,"z",function(){return O}),r.d(t,"x",function(){return M}),r.d(t,"K",function(){return j}),r.d(t,"a",function(){return E}),r.d(t,"c",function(){return S}),r.d(t,"d",function(){return A}),r.d(t,"f",function(){return C}),r.d(t,"k",function(){return _}),r.d(t,"l",function(){return T}),r.d(t,"q",function(){return q}),r.d(t,"r",function(){return I}),r.d(t,"u",function(){return k}),r.d(t,"w",function(){return B}),r.d(t,"A",function(){return z}),r.d(t,"B",function(){return D}),r.d(t,"C",function(){return R}),r.d(t,"E",function(){return P}),r.d(t,"J",function(){return U}),r.d(t,"h",function(){return F}),r.d(t,"M",function(){return L});var f=Array.isArray;function l(e){return e&&!0===e.constructor.prototype.isMatrix||!1}function p(e){return Array.isArray(e)||l(e)}function m(e){return e&&e.isDenseMatrix&&!0===e.constructor.prototype.isMatrix||!1}function h(e){return e&&e.isSparseMatrix&&!0===e.constructor.prototype.isMatrix||!1}function d(e){return e&&!0===e.constructor.prototype.isRange||!1}function y(e){return e&&!0===e.constructor.prototype.isIndex||!1}function g(e){return"boolean"==typeof e}function b(e){return e&&!0===e.constructor.prototype.isResultSet||!1}function v(e){return e&&!0===e.constructor.prototype.isHelp||!1}function x(e){return"function"==typeof e}function w(e){return e instanceof Date}function N(e){return e instanceof RegExp}function O(e){return!(!e||"object"!==n(e)||e.constructor!==Object||o(e)||s(e))}function M(e){return null===e}function j(e){return void 0===e}function E(e){return e&&!0===e.isAccessorNode&&!0===e.constructor.prototype.isNode||!1}function S(e){return e&&!0===e.isArrayNode&&!0===e.constructor.prototype.isNode||!1}function A(e){return e&&!0===e.isAssignmentNode&&!0===e.constructor.prototype.isNode||!1}function C(e){return e&&!0===e.isBlockNode&&!0===e.constructor.prototype.isNode||!1}function _(e){return e&&!0===e.isConditionalNode&&!0===e.constructor.prototype.isNode||!1}function T(e){return e&&!0===e.isConstantNode&&!0===e.constructor.prototype.isNode||!1}function q(e){return e&&!0===e.isFunctionAssignmentNode&&!0===e.constructor.prototype.isNode||!1}function I(e){return e&&!0===e.isFunctionNode&&!0===e.constructor.prototype.isNode||!1}function k(e){return e&&!0===e.isIndexNode&&!0===e.constructor.prototype.isNode||!1}function B(e){return e&&!0===e.isNode&&!0===e.constructor.prototype.isNode||!1}function z(e){return e&&!0===e.isObjectNode&&!0===e.constructor.prototype.isNode||!1}function D(e){return e&&!0===e.isOperatorNode&&!0===e.constructor.prototype.isNode||!1}function R(e){return e&&!0===e.isParenthesisNode&&!0===e.constructor.prototype.isNode||!1}function P(e){return e&&!0===e.isRangeNode&&!0===e.constructor.prototype.isNode||!1}function U(e){return e&&!0===e.isSymbolNode&&!0===e.constructor.prototype.isNode||!1}function F(e){return e&&!0===e.constructor.prototype.isChain||!1}function L(e){var t=n(e);return"object"===t?null===e?"null":Array.isArray(e)?"Array":e instanceof Date?"Date":e instanceof RegExp?"RegExp":a(e)?"BigNumber":o(e)?"Complex":s(e)?"Fraction":l(e)?"Matrix":u(e)?"Unit":y(e)?"Index":d(e)?"Range":b(e)?"ResultSet":B(e)?e.type:F(e)?"Chain":v(e)?"Help":"Object":"function"===t?"Function":t}},function(e,t,r){"use strict";r.d(t,"a",function(){return s}),r.d(t,"p",function(){return c}),r.d(t,"q",function(){return f}),r.d(t,"m",function(){return l}),r.d(t,"l",function(){return p}),r.d(t,"n",function(){return m}),r.d(t,"o",function(){return h}),r.d(t,"e",function(){return d}),r.d(t,"k",function(){return y}),r.d(t,"f",function(){return g}),r.d(t,"c",function(){return b}),r.d(t,"d",function(){return v}),r.d(t,"j",function(){return x}),r.d(t,"i",function(){return w}),r.d(t,"g",function(){return N}),r.d(t,"h",function(){return O}),r.d(t,"b",function(){return M});var n=r(4),i=r(1),a=r(5),u=r(6),o=r(9);function s(e){for(var t=[];Array.isArray(e);)t.push(e.length),e=e[0];return t}function c(e,t){if(0===t.length){if(Array.isArray(e))throw new u.a(e.length,0)}else!function e(t,r,n){var i=t.length;if(i!==r[n])throw new u.a(i,r[n]);if(n")}(e,t,0)}function f(e,t){if(!Object(i.y)(e)||!Object(n.i)(e))throw new TypeError("Index must be an integer (value: "+e+")");if(e<0||"number"==typeof t&&t<=e)throw new o.a(e,t)}function l(e,t,r){if(!Array.isArray(e)||!Array.isArray(t))throw new TypeError("Array expected");if(0===t.length)throw new Error("Resizing to scalar is not supported");return t.forEach(function(e){if(!Object(i.y)(e)||!Object(n.i)(e)||e<0)throw new TypeError("Invalid size, must contain positive integers (size: "+Object(a.d)(t)+")")}),function e(t,r,n,i){var a;var o;var s=t.length;var u=r[n];var c=Math.min(s,u);t.length=u;if(ni.length||n-a+1>i.length;)i.push(0);else for(var o=Math.abs(n-a)-(i.length-1),s=0;s=i)return p(e,t);var s=o.coefficients,u=o.exponent;s.lengtht&&5<=n.splice(t,n.length-t)[0]){var i=t-1;for(n[i]++;10===n[i];)n.pop(),0===i&&(n.unshift(0),r.exponent++,i++),n[--i]++}return r}function y(e){for(var t=[],r=0;r/g,">")}function p(e,t){if(!Object(n.I)(e))throw new TypeError("Unexpected type of argument in function compareText (expected: string or Array or Matrix, actual: "+Object(n.M)(e)+", index: 0)");if(!Object(n.I)(t))throw new TypeError("Unexpected type of argument in function compareText (expected: string or Array or Matrix, actual: "+Object(n.M)(t)+", index: 1)");return e===t?0:t=this.max?this.message="Index out of range ("+this.index+" > "+(this.max-1)+")":this.message="Index out of range ("+this.index+")",this.stack=(new Error).stack}r.d(t,"a",function(){return n}),(n.prototype=new RangeError).constructor=RangeError,n.prototype.name="IndexError",n.prototype.isIndexError=!0},function(r,i,e){var a; /** * @license Fraction.js v4.0.12 09/09/2015 * http://www.xarg.org/2014/03/rational-numbers-in-javascript/ * * Copyright (c) 2015, Robert Eisele (robert@xarg.org) * Dual licensed under the MIT or GPL Version 2 licenses. **/ !function(){"use strict";var d={s:1,n:0,d:1};function e(t){function e(){var e=Error.apply(this,arguments);e.name=this.name=t,this.stack=e.stack,this.message=e.message}function r(){}return r.prototype=Error.prototype,e.prototype=new r,e}var y=f.DivisionByZero=e("DivisionByZero"),t=f.InvalidParameter=e("InvalidParameter");function g(e,t){return isNaN(e=parseInt(e,10))&&b(),e*t}function b(){throw new t}var n=function(e,t){var r,n=0,i=1,a=1,o=0,s=0,u=0,c=1,f=1,l=0,p=1,m=1,h=1;if(null!=e)if(void 0!==t)a=(n=e)*(i=t);else switch(typeof e){case"object":"d"in e&&"n"in e?(n=e.n,i=e.d,"s"in e&&(n*=e.s)):0 in e?(n=e[0],1 in e&&(i=e[1])):b(),a=n*i;break;case"number":if(e<0&&(e=-(a=e)),e%1==0)n=e;else if(0>=1)1&t&&(n=n*e%r);return n}(10,r,t),a=0;a<300;a++){if(n===i)return a;n=10*n%t,i=10*i%t}return 0}function c(e,t){if(!e)return t;if(!t)return e;for(;;){if(!(e%=t))return t;if(!(t%=e))return e}}function f(e,t){if(!(this instanceof f))return new f(e,t);n(e,t),e=f.REDUCE?c(d.d,d.n):1,this.s=d.s,this.n=d.n/e,this.d=d.d/e}f.REDUCE=1,f.prototype={s:1,n:0,d:1,abs:function(){return new f(this.n,this.d)},neg:function(){return new f(-this.s*this.n,this.d)},add:function(e,t){return n(e,t),new f(this.s*this.n*d.d+d.s*this.d*d.n,this.d*d.d)},sub:function(e,t){return n(e,t),new f(this.s*this.n*d.d-d.s*this.d*d.n,this.d*d.d)},mul:function(e,t){return n(e,t),new f(this.s*d.s*this.n*d.n,this.d*d.d)},div:function(e,t){return n(e,t),new f(this.s*d.s*this.n*d.d,this.d*d.n)},clone:function(){return new f(this)},mod:function(e,t){return isNaN(this.n)||isNaN(this.d)?new f(NaN):void 0===e?new f(this.s*this.n%this.d,1):(n(e,t),0===d.n&&0===this.d&&f(0,0),new f(this.s*(d.d*this.n)%(d.n*this.d),d.d*this.d))},gcd:function(e,t){return n(e,t),new f(c(d.n,this.n)*c(d.d,this.d),d.d*this.d)},lcm:function(e,t){return n(e,t),0===d.n&&0===this.n?new f:new f(d.n*this.n,c(d.n,this.n)*c(d.d,this.d))},ceil:function(e){return e=Math.pow(10,e||0),isNaN(this.n)||isNaN(this.d)?new f(NaN):new f(Math.ceil(e*this.s*this.n/this.d),e)},floor:function(e){return e=Math.pow(10,e||0),isNaN(this.n)||isNaN(this.d)?new f(NaN):new f(Math.floor(e*this.s*this.n/this.d),e)},round:function(e){return e=Math.pow(10,e||0),isNaN(this.n)||isNaN(this.d)?new f(NaN):new f(Math.round(e*this.s*this.n/this.d),e)},inverse:function(){return new f(this.s*this.d,this.n)},pow:function(e){return e<0?new f(Math.pow(this.s*this.d,-e),Math.pow(this.n,-e)):new f(Math.pow(this.s*this.n,e),Math.pow(this.d,e))},equals:function(e,t){return n(e,t),this.s*this.n*d.d==d.s*d.n*this.d},compare:function(e,t){n(e,t);var r=this.s*this.n*d.d-d.s*d.n*this.d;return(03?e.slice(3):"any";var i=n.split("|").map(O).filter(M).filter(N);var a=v(r,i);var o=i.map(function(e){var t=u(e);return{name:e,typeIndex:c(t),test:t.test,conversion:null,conversionIndex:-1}});var s=a.map(function(e){var t=u(e.from);return{name:e.from,typeIndex:c(t),test:t.test,conversion:e,conversionIndex:r.indexOf(e)}});return{types:o.concat(s),restParam:t}}function H(e,t,i){var r=[];if(e.trim()!==""){r=e.split(",").map(O).map(function(e,t,r){var n=o(e,i);if(n.restParam&&t!==r.length-1){throw new SyntaxError('Unexpected rest parameter "'+e+'": '+"only allowed for the last parameter")}return n})}if(r.some(j)){return null}return{params:r,fn:t}}function $(e){var t=S(e);return t?t.restParam:false}function s(e){return e.types.some(function(e){return e.conversion!=null})}function G(e){if(!e||e.types.length===0){return k}else if(e.types.length===1){return u(e.types[0].name).test}else if(e.types.length===2){var r=u(e.types[0].name).test;var n=u(e.types[1].name).test;return function e(t){return r(t)||n(t)}}else{var i=e.types.map(function(e){return u(e.name).test});return function e(t){for(var r=0;r=a+1}}else{if(e.length===0){return function e(t){return t.length===0}}else if(e.length===1){r=G(e[0]);return function e(t){return r(t[0])&&t.length===1}}else if(e.length===2){r=G(e[0]);i=G(e[1]);return function e(t){return r(t[0])&&i(t[1])&&t.length===2}}else{n=e.map(G);return function e(t){for(var r=0;r0){var c=p(r[s]);n=new TypeError("Unexpected type of argument in function "+a+" (expected: "+i.join(" or ")+", actual: "+c+", index: "+s+")");n.data={category:"wrongType",fn:a,index:s,actual:c,expected:i};return n}}else{o=u}}var f=o.map(function(e){return $(e.params)?Infinity:e.params.length});if(r.lengthl){n=new TypeError("Too many arguments in function "+a+" (expected: "+l+", actual: "+r.length+")");n.data={category:"tooManyArgs",fn:a,index:r.length,expectedLength:l};return n}n=new TypeError('Arguments of type "'+r.join(", ")+'" do not match any of the defined signatures of function '+a+".");n.data={category:"mismatch",actual:r.map(p)};return n}function y(e){var t=999;for(var r=0;r=o:c?o>=s:o===s}function w(r,t){if(Object.keys(t).length===0){throw new SyntaxError("No signatures provided")}var n=[];Object.keys(t).map(function(e){return H(e,t[e],F.conversions)}).filter(K).forEach(function(t){var e=ee(n,function(e){return Q(e,t)});if(e){throw new TypeError('Conflicting signatures "'+L(e.params)+'" and "'+L(t.params)+'".')}n.push(t)});var i=te(n,function(t){var e=t?X(t.params,false):[];return e.map(function(e){return{params:e,fn:t.fn}})}).filter(K);i.sort(J);var e=i[0]&&i[0].params.length<=2&&!$(i[0].params);var a=i[1]&&i[1].params.length<=2&&!$(i[1].params);var o=i[2]&&i[2].params.length<=2&&!$(i[2].params);var s=i[3]&&i[3].params.length<=2&&!$(i[3].params);var u=i[4]&&i[4].params.length<=2&&!$(i[4].params);var c=i[5]&&i[5].params.length<=2&&!$(i[5].params);var f=e&&a&&o&&s&&u&&c;var l=i.map(function(e){return Z(e.params)});var p=e?G(i[0].params[0]):re;var m=a?G(i[1].params[0]):re;var h=o?G(i[2].params[0]):re;var d=s?G(i[3].params[0]):re;var y=u?G(i[4].params[0]):re;var g=c?G(i[5].params[0]):re;var b=e?G(i[0].params[1]):re;var v=a?G(i[1].params[1]):re;var x=o?G(i[2].params[1]):re;var w=s?G(i[3].params[1]):re;var N=u?G(i[4].params[1]):re;var O=c?G(i[5].params[1]):re;var M=i.map(function(e){return W(e.params,e.fn)});var j=e?M[0]:ne;var E=a?M[1]:ne;var S=o?M[2]:ne;var A=s?M[3]:ne;var C=u?M[4]:ne;var _=c?M[5]:ne;var T=e?i[0].params.length:-1;var q=a?i[1].params.length:-1;var I=o?i[2].params.length:-1;var k=s?i[3].params.length:-1;var B=u?i[4].params.length:-1;var z=c?i[5].params.length:-1;var D=f?6:0;var R=i.length;var P=function e(){"use strict";for(var t=D;t>>=1;return(e+r)/t}},m.exports.resetGlobal=function(){Math.random=t},l(Math.random(),i)}).call(this,t(19))},function(t,De,Re){var Pe;!function(){"use strict";var l,_,o,s=9e15,d=1e9,y="0123456789abcdef",n="2.3025850929940456840179914546843642076011014886287729760333279009675726096773524802359972050895982983419677840422862486334095254650828067566662873690987816894829072083255546808437998948262331985283935053089653777326288461633662222876982198867465436674744042432743651550489343149393914796194044002221051017141748003688084012647080685567743216228355220114804663715659121373450747856947683463616792101806445070648000277502684916746550586856935673420670581136429224554405758925724208241314695689016758940256776311356919292033376587141660230105703089634572075440370847469940168269282808481184289314848524948644871927809676271275775397027668605952496716674183485704422507197965004714951050492214776567636938662976979522110718264549734772662425709429322582798502585509785265383207606726317164309505995087807523710333101197857547331541421808427543863591778117054309827482385045648019095610299291824318237525357709750539565187697510374970888692180205189339507238539205144634197265287286965110862571492198849978748873771345686209167058",i="3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679821480865132823066470938446095505822317253594081284811174502841027019385211055596446229489549303819644288109756659334461284756482337867831652712019091456485669234603486104543266482133936072602491412737245870066063155881748815209209628292540917153643678925903600113305305488204665213841469519415116094330572703657595919530921861173819326117931051185480744623799627495673518857527248912279381830119491298336733624406566430860213949463952247371907021798609437027705392171762931767523846748184676694051320005681271452635608277857713427577896091736371787214684409012249534301465495853710507922796892589235420199561121290219608640344181598136297747713099605187072113499999983729780499510597317328160963185950244594553469083026425223082533446850352619311881710100031378387528865875332083814206171776691473035982534904287554687311595628638823537875937519577818577805321712268066130019278766111959092164201989380952572010654858632789",u={precision:20,rounding:4,modulo:1,toExpNeg:-7,toExpPos:21,minE:-s,maxE:s,crypto:!1},v=!0,c="[DecimalError] ",g=c+"Invalid argument: ",a=c+"Precision limit exceeded",f=c+"crypto unavailable",T=Math.floor,b=Math.pow,p=/^0b([01]+(\.[01]*)?|\.[01]+)(p[+-]?\d+)?$/i,m=/^0x([0-9a-f]+(\.[0-9a-f]*)?|\.[0-9a-f]+)(p[+-]?\d+)?$/i,h=/^0o([0-7]+(\.[0-7]*)?|\.[0-7]+)(p[+-]?\d+)?$/i,x=/^(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i,q=1e7,I=7,w=n.length-1,N=i.length-1,O={name:"[object Decimal]"};function M(e){var t,r,n,i=e.length-1,a="",o=e[0];if(0r-1&&(void 0===a[n+1]&&(a[n+1]=0),a[n+1]+=a[n]/r|0,a[n]%=r)}return a.reverse()}O.absoluteValue=O.abs=function(){var e=new this.constructor(this);return e.s<0&&(e.s=1),D(e)},O.ceil=function(){return D(new this.constructor(this),this.e+1,2)},O.comparedTo=O.cmp=function(e){var t,r,n,i,a=this,o=a.d,s=(e=new a.constructor(e)).d,u=a.s,c=e.s;if(!o||!s)return u&&c?u!==c?u:o===s?0:!o^u<0?1:-1:NaN;if(!o[0]||!s[0])return o[0]?u:s[0]?-c:0;if(u!==c)return u;if(a.e!==e.e)return a.e>e.e^u<0?1:-1;for(t=0,r=(n=o.length)<(i=s.length)?n:i;ts[t]^u<0?1:-1;return n===i?0:ithis.d.length-2},O.isNaN=function(){return!this.s},O.isNegative=O.isNeg=function(){return this.s<0},O.isPositive=O.isPos=function(){return 0t&&(t=this.e+1)):t=NaN,t},O.round=function(){var e=this.constructor;return D(new e(this),this.e+1,e.rounding)},O.sine=O.sin=function(){var e,t,r=this,n=r.constructor;return r.isFinite()?r.isZero()?new n(r):(e=n.precision,t=n.rounding,n.precision=e+Math.max(r.e,r.sd())+I,n.rounding=1,r=function(e,t){var r,n=t.d.length;if(n<3)return W(e,2,t,t);r=16<(r=1.4*Math.sqrt(n))?16:0|r,t=t.times(1/Y(5,r)),t=W(e,2,t,t);for(var i,a=new e(5),o=new e(16),s=new e(20);r--;)i=t.times(t),t=t.times(a.plus(i.times(o.times(i).minus(s))));return t}(n,X(n,r)),n.precision=e,n.rounding=t,D(2=e.d.length-1&&(r=c<0?-c:c)<=9007199254740991)return i=H(u,s,r,n),e.s<0?new u(1).div(i):D(i,n,a);if((o=s.s)<0){if(tu.maxE+1||t=n.toExpPos):(j(e,1,d),void 0===t?t=n.rounding:j(t,0,8),C(r=D(new n(r),e,t),e<=r.e||r.e<=n.toExpNeg,e));return r.isNeg()&&!r.isZero()?"-"+i:i},O.toSignificantDigits=O.toSD=function(e,t){var r=this.constructor;return void 0===e?(e=r.precision,t=r.rounding):(j(e,1,d),void 0===t?t=r.rounding:j(t,0,8)),D(new r(this),e,t)},O.toString=function(){var e=this,t=e.constructor,r=C(e,e.e<=t.toExpNeg||e.e>=t.toExpPos);return e.isNeg()&&!e.isZero()?"-"+r:r},O.truncated=O.trunc=function(){return D(new this.constructor(this),this.e+1,1)},O.valueOf=O.toJSON=function(){var e=this,t=e.constructor,r=C(e,e.e<=t.toExpNeg||e.e>=t.toExpPos);return e.isNeg()?"-"+r:r};var A=function(e,t,r,n,i,a){var o,s,u,c,f,l,p,m,h,d,y,g,b,v,x,w,N,O,M,j,E=e.constructor,S=e.s==t.s?1:-1,A=e.d,C=t.d;if(!(A&&A[0]&&C&&C[0]))return new E(e.s&&t.s&&(A?!C||A[0]!=C[0]:C)?A&&0==A[0]||!C?0*S:S/0:NaN);for(s=a?(f=1,e.e-t.e):(a=q,f=I,T(e.e/f)-T(t.e/f)),M=C.length,N=A.length,d=(h=new E(S)).d=[],u=0;C[u]==(A[u]||0);u++);if(C[u]>(A[u]||0)&&s--,null==r?(v=r=E.precision,n=E.rounding):v=i?r+(e.e-t.e)+1:r,v<0)d.push(1),l=!0;else{if(v=v/f+2|0,u=0,1==M){for(C=C[c=0],v++;(u=a/2&&++O;c=0,(o=B(C,y,M,g))<0?(b=y[0],M!=g&&(b=b*a+(y[1]||0)),1<(c=b/O|0)?(a<=c&&(c=a-1),1==(o=B(p=k(C,c,a),y,m=p.length,g=y.length))&&(c--,z(p,Mt[i]?1:-1;break}return a}function z(e,t,r,n){for(var i=0;r--;)e[r]-=i,i=e[r]m.maxE?(e.d=null,e.e=NaN):e.ee.constructor.maxE?(e.d=null,e.e=NaN):e.ei-1;)l[r]=0,r||(++a,l.unshift(1));for(u=l.length;!l[u-1];--u);for(o=0,f="";ou)for(a-=u;a--;)f+="0";else at&&(e.length=t,1)}function K(e){return new this(e).abs()}function ee(e){return new this(e).acos()}function te(e){return new this(e).acosh()}function re(e,t){return new this(e).plus(t)}function ne(e){return new this(e).asin()}function ie(e){return new this(e).asinh()}function ae(e){return new this(e).atan()}function oe(e){return new this(e).atanh()}function se(e,t){e=new this(e),t=new this(t);var r,n=this.precision,i=this.rounding,a=n+4;return e.s&&t.s?e.d||t.d?!t.d||e.isZero()?(r=t.s<0?U(this,n,i):new this(0)).s=e.s:!e.d||t.isZero()?(r=U(this,a,1).times(.5)).s=e.s:r=t.s<0?(this.precision=a,this.rounding=1,r=this.atan(A(e,t,a,1)),t=U(this,a,1),this.precision=n,this.rounding=i,e.s<0?r.minus(t):r.plus(t)):this.atan(A(e,t,a,1)):(r=U(this,a,1).times(0a.maxE?(i.e=NaN,i.d=null):e.e=e.length&&o.slice(0,e.length)===e&&(s+=u[c[t]],o=o.slice(e.length,o.length),r=!0)}),r||(s+=o.slice(0,1),o=o.slice(1,o.length))}();return s}},function(e,t){function r(){}r.prototype={on:function(e,t,r){var n=this.e||(this.e={});return(n[e]||(n[e]=[])).push({fn:t,ctx:r}),this},once:function(e,t,r){var n=this;function i(){n.off(e,i),t.apply(r,arguments)}return i._=t,this.on(e,i,r)},emit:function(e){for(var t=[].slice.call(arguments,1),r=((this.e||(this.e={}))[e]||[]).slice(),n=0,i=r.length;n15 significant digits to BigNumber (value: "+e+"). Use function bignumber(x) to convert to BigNumber.");return new r(e)}},{from:"number",to:"Complex",convert:function(e){return n||f(e),new n(e,0)}},{from:"number",to:"string",convert:function(e){return e+""}},{from:"BigNumber",to:"Complex",convert:function(e){return n||f(e),new n(e.toNumber(),0)}},{from:"Fraction",to:"BigNumber",convert:function(){throw new TypeError("Cannot implicitly convert a Fraction to BigNumber or vice versa. Use function bignumber(x) to convert to BigNumber or fraction(x) to convert to Fraction.")}},{from:"Fraction",to:"Complex",convert:function(e){return n||f(e),new n(e.valueOf(),0)}},{from:"number",to:"Fraction",convert:function(e){i||l(e);var t=new i(e);if(t.valueOf()!==e)throw new TypeError("Cannot implicitly convert a number to a Fraction when there will be a loss of precision (value: "+e+"). Use function fraction(x) to convert to Fraction.");return t}},{from:"string",to:"number",convert:function(e){var t=Number(e);if(isNaN(t))throw new Error('Cannot convert "'+e+'" to a number');return t}},{from:"string",to:"BigNumber",convert:function(t){r||c(t);try{return new r(t)}catch(e){throw new Error('Cannot convert "'+t+'" to BigNumber')}}},{from:"string",to:"Fraction",convert:function(t){i||l(t);try{return new i(t)}catch(e){throw new Error('Cannot convert "'+t+'" to Fraction')}}},{from:"string",to:"Complex",convert:function(t){n||f(t);try{return new n(t)}catch(e){throw new Error('Cannot convert "'+t+'" to Complex')}}},{from:"boolean",to:"number",convert:function(e){return+e}},{from:"boolean",to:"BigNumber",convert:function(e){return r||c(e),new r(+e)}},{from:"boolean",to:"Fraction",convert:function(e){return i||l(e),new i(+e)}},{from:"boolean",to:"string",convert:function(e){return String(e)}},{from:"Array",to:"Matrix",convert:function(e){return t||function(){throw new Error("Cannot convert array into a Matrix: no class 'DenseMatrix' provided")}(),new t(e)}},{from:"Matrix",to:"Array",convert:function(e){return e.valueOf()}}],a});function c(e){throw new Error("Cannot convert value ".concat(e," into a BigNumber: no class 'BigNumber' provided"))}function f(e){throw new Error("Cannot convert value ".concat(e," into a Complex number: no class 'Complex' provided"))}function l(e){throw new Error("Cannot convert value ".concat(e," into a Fraction, no class 'Fraction' provided."))}var p=[],m=Object(s.a)("ResultSet",p,function(){function t(e){if(!(this instanceof t))throw new SyntaxError("Constructor must be called with the new operator");this.entries=e||[]}return t.prototype.type="ResultSet",t.prototype.isResultSet=!0,t.prototype.valueOf=function(){return this.entries},t.prototype.toString=function(){return"["+this.entries.join(", ")+"]"},t.prototype.toJSON=function(){return{mathjs:"ResultSet",entries:this.entries}},t.fromJSON=function(e){return new t(e.entries)},t},{isClass:!0}),h=r(15),d=r.n(h),y=["?on","config"],g=Object(s.a)("BigNumber",y,function(e){var t=e.on,r=e.config,n=d.a.clone({precision:r.precision,modulo:9});return n.prototype.type="BigNumber",n.prototype.isBigNumber=!0,n.prototype.toJSON=function(){return{mathjs:"BigNumber",value:this.toString()}},n.fromJSON=function(e){return new n(e.value)},t&&t("config",function(e,t){e.precision!==t.precision&&n.config({precision:e.precision})}),n},{isClass:!0}),b=r(8),v=r.n(b);function x(e){return(x="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}var w=[],N=Object(s.a)("Complex",w,function(){return v.a.prototype.type="Complex",v.a.prototype.isComplex=!0,v.a.prototype.toJSON=function(){return{mathjs:"Complex",re:this.re,im:this.im}},v.a.prototype.toPolar=function(){return{r:this.abs(),phi:this.arg()}},v.a.prototype.format=function(e){var t,r=this.im,n=this.re,i=Object(E.h)(this.re,e),a=Object(E.h)(this.im,e),o=Object(ie.y)(e)?e:e?e.precision:null;return null!==o&&(t=Math.pow(10,-o),Math.abs(n/r)t.re?1:e.ret.im?1:e.imn[a]&&(n[a]=t[a],i=!0);i&&s(e,n,r)}return(m.prototype=new t).createDenseMatrix=function(e,t){return new m(e,t)},m.prototype.type="DenseMatrix",m.prototype.isDenseMatrix=!0,m.prototype.getDataType=function(){return Object(q.h)(this._data,ie.M)},m.prototype.storage=function(){return"dense"},m.prototype.datatype=function(){return this._datatype},m.prototype.create=function(e,t){return new m(e,t)},m.prototype.subset=function(e,t,r){switch(arguments.length){case 1:return function(e,t){if(!Object(ie.t)(t))throw new TypeError("Invalid index");{if(t.isScalar())return e.get(t.min());var r=t.size();if(r.length!==e._size.length)throw new z.a(r.length,e._size.length);for(var n=t.min(),i=t.max(),a=0,o=e._size.length;a");var c=t.max().map(function(e){return e+1});l(e,c,n);var f=a.length;!function r(n,i,a,o,s){var e=s===o-1;var t=i.dimension(s);e?t.forEach(function(e,t){Object(q.q)(e),n[e]=a[t[0]]}):t.forEach(function(e,t){Object(q.q)(e),r(n[e],i,a[t[0]],o,s+1)})}(e._data,t,r,f,0)}return e}(this,e,t,r);default:throw new SyntaxError("Wrong number of arguments")}},m.prototype.get=function(e){if(!Object(ie.b)(e))throw new TypeError("Array expected");if(e.length!==this._size.length)throw new z.a(e.length,this._size.length);for(var t=0;t=n.length)throw new R.a(t,n.length);return Object(ie.v)(e)?e.create(L(e.valueOf(),t,r)):L(e,t,r)}function L(e,t,r){var n,i,a,o;if(t<=0){if(Array.isArray(e[0])){for(o=function(e){var t,r,n=e.length,i=e[0].length,a=[];for(r=0;r");for(var c=t.min()[0],f=t.min()[1],l=i[0],p=i[1],m=0;m "+(this._values?Object(V.d)(this._values[u],e):"X")}return i},j.prototype.toString=function(){return Object(V.d)(this.toArray())},j.prototype.toJSON=function(){return{mathjs:"SparseMatrix",values:this._values,index:this._index,ptr:this._ptr,size:this._size,datatype:this._datatype}},j.prototype.diagonal=function(e){if(e){if(Object(ie.e)(e)&&(e=e.toNumber()),!Object(ie.y)(e)||!Object(E.i)(e))throw new TypeError("The parameter k must be an integer number")}else e=0;var t=0Math.pow(2,32)-1)throw new SyntaxError('String "'.concat(e,'" is out of range'));2147483648&t&&(t=-1*~(t-1))}return t},BigNumber:function(e){return e.toNumber()},Fraction:function(e){return e.valueOf()},Unit:function(){throw new Error("Second argument with valueless unit expected")},null:function(){return 0},"Unit, string | Unit":function(e,t){return e.toNumber(t)},"Array | Matrix":function(e){return oe(e,this)}});return t.fromJSON=function(e){return parseFloat(e.value)},t}),Ae=["typed"],Ce=Object(s.a)("string",Ae,function(e){return(0,e.typed)("string",{"":function(){return""},number:E.h,null:function(){return"null"},boolean:function(e){return e+""},string:function(e){return e},"Array | Matrix":function(e){return oe(e,this)},any:function(e){return String(e)}})}),_e="boolean",Te=["typed"],qe=Object(s.a)(_e,Te,function(e){return(0,e.typed)(_e,{"":function(){return!1},boolean:function(e){return e},number:function(e){return!!e},null:function(){return!1},BigNumber:function(e){return!e.isZero()},string:function(e){var t=e.toLowerCase();if("true"===t)return!0;if("false"===t)return!1;var r=Number(e);if(""!==e&&!isNaN(r))return!!r;throw new Error('Cannot convert "'+e+'" to a boolean')},"Array | Matrix":function(e){return oe(e,this)}})}),Ie=["typed","BigNumber"],ke=Object(s.a)("bignumber",Ie,function(e){var t=e.typed,r=e.BigNumber;return t("bignumber",{"":function(){return new r(0)},number:function(e){return new r(e+"")},string:function(e){return new r(e)},BigNumber:function(e){return e},Fraction:function(e){return new r(e.n).div(e.d).times(e.s)},null:function(){return new r(0)},"Array | Matrix":function(e){return oe(e,this)}})}),Be=["typed","Complex"],ze=Object(s.a)("complex",Be,function(e){var t=e.typed,r=e.Complex;return t("complex",{"":function(){return r.ZERO},number:function(e){return new r(e,0)},"number, number":function(e,t){return new r(e,t)},"BigNumber, BigNumber":function(e,t){return new r(e.toNumber(),t.toNumber())},Fraction:function(e){return new r(e.valueOf(),0)},Complex:function(e){return e.clone()},string:function(e){return r(e)},null:function(){return r(0)},Object:function(e){if("re"in e&&"im"in e)return new r(e.re,e.im);if("r"in e&&"phi"in e||"abs"in e&&"arg"in e)return new r(e);throw new Error("Expected object with properties (re and im) or (r and phi) or (abs and arg)")},"Array | Matrix":function(e){return oe(e,this)}})}),De=["typed","Fraction"],Re=Object(s.a)("fraction",De,function(e){var t=e.typed,r=e.Fraction;return t("fraction",{number:function(e){if(!isFinite(e)||isNaN(e))throw new Error(e+" cannot be represented as a fraction");return new r(e)},string:function(e){return new r(e)},"number, number":function(e,t){return new r(e,t)},null:function(){return new r(0)},BigNumber:function(e){return new r(e.toString())},Fraction:function(e){return e},Object:function(e){return new r(e)},"Array | Matrix":function(e){return oe(e,this)}})}),Pe=["typed","Matrix","DenseMatrix","SparseMatrix"],Ue=Object(s.a)("matrix",Pe,function(e){var t=e.typed,n=(e.Matrix,e.DenseMatrix),i=e.SparseMatrix;return t("matrix",{"":function(){return r([])},string:function(e){return r([],e)},"string, string":function(e,t){return r([],e,t)},Array:function(e){return r(e)},Matrix:function(e){return r(e,e.storage())},"Array | Matrix, string":r,"Array | Matrix, string, string":r});function r(e,t,r){if("dense"===t||"default"===t||void 0===t)return new n(e,r);if("sparse"===t)return new i(e,r);throw new TypeError("Unknown matrix type "+JSON.stringify(t)+".")}}),Fe="splitUnit",Le=["typed"],He=Object(s.a)(Fe,Le,function(e){return(0,e.typed)(Fe,{"Unit, Array":function(e,t){return e.splitUnit(t)}})}),$e="number",Ge="number, number";function Ze(e){return Math.abs(e)}function Ve(e,t){return e+t}function Je(e,t){return e*t}function We(e){return-e}function Ye(e){return e}function Xe(e){return Object(E.d)(e)}function Qe(e){return Math.ceil(e)}function Ke(e){return e*e*e}function et(e){return Math.exp(e)}function tt(e){return Object(E.g)(e)}function rt(e,t){if(!Object(E.i)(e)||!Object(E.i)(t))throw new Error("Parameters in function gcd must be integer numbers");for(var r;0!==t;)r=e%t,e=t,t=r;return e<0?-e:e}function nt(e,t){if(!Object(E.i)(e)||!Object(E.i)(t))throw new Error("Parameters in function lcm must be integer numbers");if(0===e||0===t)return 0;for(var r,n=e*t;0!==t;)t=e%(r=t),e=r;return Math.abs(n/e)}function it(e){return Math.log(e)}function at(e){return Object(E.j)(e)}function ot(e){return Object(E.l)(e)}function st(e,t){if(0=n.length)throw new R.a(t,n.length);return Object(ie.v)(e)?e.create(Mt(e.valueOf(),t,r)):Mt(e,t,r)}})});function Mt(e,t,r){var n,i,a;if(t<=0){if(Array.isArray(e[0])){for(a=function(e){var t,r,n=e.length,i=e[0].length,a=[];for(r=0;rc)for(s-=c;s--;)u+="0";else s>1,f[m]&=1)}return f.reverse()}function Yr(e,t){if(e.isFinite()&&!e.isInteger()||t.isFinite()&&!t.isInteger())throw new Error("Integers expected in function bitXor");var r=e.constructor;if(e.isNaN()||t.isNaN())return new r(NaN);if(e.isZero())return t;if(t.isZero())return e;if(e.eq(t))return new r(0);var n=new r(-1);return e.eq(n)?Zr(t):t.eq(n)?Zr(e):e.isFinite()&&t.isFinite()?Jr(e,t,function(e,t){return e^t}):e.isFinite()||t.isFinite()?new r(e.isNegative()===t.isNegative()?1/0:-1/0):n}function Xr(e,t){if(e.isFinite()&&!e.isInteger()||t.isFinite()&&!t.isInteger())throw new Error("Integers expected in function leftShift");var r=e.constructor;return e.isNaN()||t.isNaN()||t.isNegative()&&!t.isZero()?new r(NaN):e.isZero()||t.isZero()?e:e.isFinite()||t.isFinite()?t.lt(55)?e.times(Math.pow(2,t.toNumber())+""):e.times(new r(2).pow(t)):new r(NaN)}function Qr(e,t){if(e.isFinite()&&!e.isInteger()||t.isFinite()&&!t.isInteger())throw new Error("Integers expected in function rightArithShift");var r=e.constructor;return e.isNaN()||t.isNaN()||t.isNegative()&&!t.isZero()?new r(NaN):e.isZero()||t.isZero()?e:t.isFinite()?t.lt(55)?e.div(Math.pow(2,t.toNumber())+"").floor():e.div(new r(2).pow(t)).floor():e.isNegative()?new r(-1):e.isFinite()?new r(0):new r(NaN)}var Kr="number, number";function en(e,t){if(!Object(E.i)(e)||!Object(E.i)(t))throw new Error("Integers expected in function bitAnd");return e&t}function tn(e){if(!Object(E.i)(e))throw new Error("Integer expected in function bitNot");return~e}function rn(e,t){if(!Object(E.i)(e)||!Object(E.i)(t))throw new Error("Integers expected in function bitOr");return e|t}function nn(e,t){if(!Object(E.i)(e)||!Object(E.i)(t))throw new Error("Integers expected in function bitXor");return e^t}function an(e,t){if(!Object(E.i)(e)||!Object(E.i)(t))throw new Error("Integers expected in function leftShift");return e<>t}function sn(e,t){if(!Object(E.i)(e)||!Object(E.i)(t))throw new Error("Integers expected in function rightLogShift");return e>>>t}en.signature=Kr,tn.signature="number",sn.signature=on.signature=an.signature=nn.signature=rn.signature=Kr;var un=["typed","matrix","equalScalar"],cn=Object(s.a)("bitAnd",un,function(e){var t=e.typed,r=e.matrix,n=e.equalScalar,i=tr({typed:t,equalScalar:n}),a=nr({typed:t,equalScalar:n}),o=ar({typed:t,equalScalar:n}),s=Wt({typed:t}),u=Xt({typed:t});return t("bitAnd",{"number, number":en,"BigNumber, BigNumber":Gr,"SparseMatrix, SparseMatrix":function(e,t){return a(e,t,this,!1)},"SparseMatrix, DenseMatrix":function(e,t){return i(t,e,this,!0)},"DenseMatrix, SparseMatrix":function(e,t){return i(e,t,this,!1)},"DenseMatrix, DenseMatrix":function(e,t){return s(e,t,this)},"Array, Array":function(e,t){return this(r(e),r(t)).valueOf()},"Array, Matrix":function(e,t){return this(r(e),t)},"Matrix, Array":function(e,t){return this(e,r(t))},"SparseMatrix, any":function(e,t){return o(e,t,this,!1)},"DenseMatrix, any":function(e,t){return u(e,t,this,!1)},"any, SparseMatrix":function(e,t){return o(t,e,this,!0)},"any, DenseMatrix":function(e,t){return u(t,e,this,!0)},"Array, any":function(e,t){return u(r(e),t,this,!1).valueOf()},"any, Array":function(e,t){return u(r(t),e,this,!0).valueOf()}})}),fn=["typed"],ln=Object(s.a)("bitNot",fn,function(e){return(0,e.typed)("bitNot",{number:tn,BigNumber:Zr,"Array | Matrix":function(e){return oe(e,this)}})}),pn=["typed","matrix","equalScalar","DenseMatrix"],mn=Object(s.a)("bitOr",pn,function(e){var t=e.typed,r=e.matrix,n=e.equalScalar,i=e.DenseMatrix,a=Ht({typed:t}),o=Gt({typed:t,equalScalar:n}),s=Vt({typed:t,DenseMatrix:i}),u=Wt({typed:t}),c=Xt({typed:t});return t("bitOr",{"number, number":rn,"BigNumber, BigNumber":Vr,"SparseMatrix, SparseMatrix":function(e,t){return o(e,t,this)},"SparseMatrix, DenseMatrix":function(e,t){return a(t,e,this,!0)},"DenseMatrix, SparseMatrix":function(e,t){return a(e,t,this,!1)},"DenseMatrix, DenseMatrix":function(e,t){return u(e,t,this)},"Array, Array":function(e,t){return this(r(e),r(t)).valueOf()},"Array, Matrix":function(e,t){return this(r(e),t)},"Matrix, Array":function(e,t){return this(e,r(t))},"SparseMatrix, any":function(e,t){return s(e,t,this,!1)},"DenseMatrix, any":function(e,t){return c(e,t,this,!1)},"any, SparseMatrix":function(e,t){return s(t,e,this,!0)},"any, DenseMatrix":function(e,t){return c(t,e,this,!0)},"Array, any":function(e,t){return c(r(e),t,this,!1).valueOf()},"any, Array":function(e,t){return c(r(t),e,this,!0).valueOf()}})}),hn=["typed","DenseMatrix"],dn=Object(s.a)("algorithm07",hn,function(e){var N=e.typed,O=e.DenseMatrix;return function(e,t,r){var n=e._size,i=e._datatype,a=t._size,o=t._datatype;if(n.length!==a.length)throw new z.a(n.length,a.length);if(n[0]!==a[0]||n[1]!==a[1])throw new RangeError("Dimension mismatch. Matrix A ("+n+") must match Matrix B ("+a+")");var s,u=n[0],c=n[1],f=0,l=r;"string"==typeof i&&i===o&&(s=i,f=N.convert(0,s),l=N.find(r,[s,s]));for(var p=[],m=0;mn)return e.substring(0,n);if(e.lengtha)for(var u=a-1,c=o.length;un&&(n=t[a],r=[a])}return r}});function ma(e,t,r){var n;return-1!==String(e).indexOf("Unexpected type")?(n=2Math.pow(2,31)-1||e<-Math.pow(2,31))throw new Error("Value must be in range [-2^31, 2^31-1]");if(!Object(E.i)(e))throw new Error("Value must be an integer");return e<0&&(e+=Math.pow(2,32)),"".concat(r).concat(e.toString(t))})})})}var xa=va("bin",2),wa=va("oct",8),Na=va("hex",16),Oa=["typed"],Ma=Object(s.a)("print",Oa,function(e){return(0,e.typed)("print",{"string, Object | Array":ja,"string, Object | Array, number | Object":ja})});function ja(e,a,o){return e.replace(/\$([\w.]+)/g,function(e,t){for(var r=t.split("."),n=a[r.shift()];r.length&&void 0!==n;)var i=r.shift(),n=i?n[i]:n+".";return void 0!==n?Object(ie.I)(n)?n:Object(V.d)(n,o):e})}var Ea=["typed","matrix"],Sa=Object(s.a)("to",Ea,function(e){var t=e.typed,r=e.matrix,n=Wt({typed:t}),i=Xt({typed:t});return t("to",{"Unit, Unit | string":function(e,t){return e.to(t)},"Matrix, Matrix":function(e,t){return n(e,t,this)},"Array, Array":function(e,t){return this(r(e),r(t)).valueOf()},"Array, Matrix":function(e,t){return this(r(e),t)},"Matrix, Array":function(e,t){return this(e,r(t))},"Matrix, any":function(e,t){return i(e,t,this,!1)},"any, Matrix":function(e,t){return i(t,e,this,!0)},"Array, any":function(e,t){return i(r(e),t,this,!1).valueOf()},"any, Array":function(e,t){return i(r(t),e,this,!0).valueOf()}})}),Aa="isPrime",Ca=["typed"],_a=Object(s.a)(Aa,Ca,function(e){return(0,e.typed)(Aa,{number:function(e){if(0*e!=0)return!1;if(e<=3)return 1>=1,i=o(i,i);return n}function l(e,t){return r(f(e.valueOf(),t))}});function Ra(t,e){var r,n=Object.keys(t);return Object.getOwnPropertySymbols&&(r=Object.getOwnPropertySymbols(t),e&&(r=r.filter(function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable})),n.push.apply(n,r)),n}function Pa(i){for(var e=1;ee.length)&&(t=e.length);for(var r=0,n=new Array(t);re.length)&&(t=e.length);for(var r=0,n=new Array(t);rt.re)return 1;if(e.ret.im)return 1;if(e.imr.length?1:t.length=e.length)throw new Error("k out of bounds");for(var n=0;n=e&&(c(s.value,0)||r(s.key,s.value,this)),(s=n.extractMinimum())&&a.push(s);for(var o=0;oe[n+1].length&&(t=e[n],e[n]=e[n+1],e[n+1]=t);return e}(r)}})}),Ac="setSize",Cc=["typed","compareNatural"],_c=Object(s.a)(Ac,Cc,function(e){var t=e.typed,a=e.compareNatural;return t(Ac,{"Array | Matrix":function(e){return Array.isArray(e)?Object(q.e)(e).length:Object(q.e)(e.toArray()).length},"Array | Matrix, boolean":function(e,t){if(!1===t||0===e.length)return Array.isArray(e)?Object(q.e)(e).length:Object(q.e)(e.toArray()).length;for(var r=Object(q.e)(Array.isArray(e)?e:e.toArray()).sort(a),n=1,i=1;i)'),t+this.index.toHTML(e)},n.prototype._toTex=function(e){var t=this.object.toTex(e);return i(this.object)&&(t="\\left(' + object + '\\right)"),t+this.index.toTex(e)},n.prototype.toJSON=function(){return{mathjs:"AccessorNode",object:this.object,index:this.index}},n.fromJSON=function(e){return new n(e.object,e.index)},n},{isClass:!0,isNode:!0}),af=["Node"],of=Object(s.a)("ArrayNode",af,function(e){var t=e.Node;function n(e){if(!(this instanceof n))throw new SyntaxError("Constructor must be called with the new operator");if(this.items=e||[],!Array.isArray(this.items)||!this.items.every(ie.w))throw new TypeError("Array containing Nodes expected")}return(n.prototype=new t).type="ArrayNode",n.prototype.isArrayNode=!0,n.prototype._compile=function(t,r){var e=Object(q.k)(this.items,function(e){return e._compile(t,r)});if("Array"===t.config.matrix)return function(t,r,n){return Object(q.k)(e,function(e){return e(t,r,n)})};var i=t.matrix;return function(t,r,n){return i(Object(q.k)(e,function(e){return e(t,r,n)}))}},n.prototype.forEach=function(e){for(var t=0;t['+this.items.map(function(e){return e.toHTML(t)}).join(',')+']'},n.prototype._toTex=function(t){var r="\\begin{bmatrix}";return this.items.forEach(function(e){e.items?r+=e.items.map(function(e){return e.toTex(t)}).join("&"):r+=e.toTex(t),r+="\\\\"}),r+="\\end{bmatrix}"},n},{isClass:!0,isNode:!0});function sf(e){return(sf="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}var uf=[{AssignmentNode:{},FunctionAssignmentNode:{}},{ConditionalNode:{latexLeftParens:!1,latexRightParens:!1,latexParens:!1}},{"OperatorNode:or":{associativity:"left",associativeWith:[]}},{"OperatorNode:xor":{associativity:"left",associativeWith:[]}},{"OperatorNode:and":{associativity:"left",associativeWith:[]}},{"OperatorNode:bitOr":{associativity:"left",associativeWith:[]}},{"OperatorNode:bitXor":{associativity:"left",associativeWith:[]}},{"OperatorNode:bitAnd":{associativity:"left",associativeWith:[]}},{"OperatorNode:equal":{associativity:"left",associativeWith:[]},"OperatorNode:unequal":{associativity:"left",associativeWith:[]},"OperatorNode:smaller":{associativity:"left",associativeWith:[]},"OperatorNode:larger":{associativity:"left",associativeWith:[]},"OperatorNode:smallerEq":{associativity:"left",associativeWith:[]},"OperatorNode:largerEq":{associativity:"left",associativeWith:[]},RelationalNode:{associativity:"left",associativeWith:[]}},{"OperatorNode:leftShift":{associativity:"left",associativeWith:[]},"OperatorNode:rightArithShift":{associativity:"left",associativeWith:[]},"OperatorNode:rightLogShift":{associativity:"left",associativeWith:[]}},{"OperatorNode:to":{associativity:"left",associativeWith:[]}},{RangeNode:{}},{"OperatorNode:add":{associativity:"left",associativeWith:["OperatorNode:add","OperatorNode:subtract"]},"OperatorNode:subtract":{associativity:"left",associativeWith:[]}},{"OperatorNode:multiply":{associativity:"left",associativeWith:["OperatorNode:multiply","OperatorNode:divide","Operator:dotMultiply","Operator:dotDivide"]},"OperatorNode:divide":{associativity:"left",associativeWith:[],latexLeftParens:!1,latexRightParens:!1,latexParens:!1},"OperatorNode:dotMultiply":{associativity:"left",associativeWith:["OperatorNode:multiply","OperatorNode:divide","OperatorNode:dotMultiply","OperatorNode:doDivide"]},"OperatorNode:dotDivide":{associativity:"left",associativeWith:[]},"OperatorNode:mod":{associativity:"left",associativeWith:[]}},{"OperatorNode:unaryPlus":{associativity:"right"},"OperatorNode:unaryMinus":{associativity:"right"},"OperatorNode:bitNot":{associativity:"right"},"OperatorNode:not":{associativity:"right"}},{"OperatorNode:pow":{associativity:"right",associativeWith:[],latexRightParens:!1},"OperatorNode:dotPow":{associativity:"right",associativeWith:[]}},{"OperatorNode:factorial":{associativity:"left"}},{"OperatorNode:transpose":{associativity:"left"}}];function cf(e,t){var r=e;"keep"!==t&&(r=e.getContent());for(var n=r.getIdentifier(),i=0;i)'),t+r+'='+n},s.prototype._toTex=function(e){var t=this.object.toTex(e),r=this.index?this.index.toTex(e):"",n=this.value.toTex(e);return u(this,e&&e.parenthesis)&&(n="\\left(".concat(n,"\\right)")),t+r+":="+n},s},{isClass:!0,isNode:!0}),hf=["ResultSet","Node"],df=Object(s.a)("BlockNode",hf,function(e){var o=e.ResultSet,t=e.Node;function a(e){if(!(this instanceof a))throw new SyntaxError("Constructor must be called with the new operator");if(!Array.isArray(e))throw new Error("Array expected");this.blocks=e.map(function(e){var t=e&&e.node,r=!e||void 0===e.visible||e.visible;if(!Object(ie.w)(t))throw new TypeError('Property "node" must be a Node');if("boolean"!=typeof r)throw new TypeError('Property "visible" must be a boolean');return{node:t,visible:r}})}return(a.prototype=new t).type="BlockNode",a.prototype.isBlockNode=!0,a.prototype._compile=function(t,r){var e=Object(q.k)(this.blocks,function(e){return{evaluate:e.node._compile(t,r),visible:e.visible}});return function(r,n,i){var a=[];return Object(q.f)(e,function(e){var t=e.evaluate(r,n,i);e.visible&&a.push(t)}),new o(a)}},a.prototype.forEach=function(e){for(var t=0;t;')}).join('
')},a.prototype._toTex=function(t){return this.blocks.map(function(e){return e.node.toTex(t)+(e.visible?"":";")}).join("\\;\\;\n")},a},{isClass:!0,isNode:!0}),yf=["Node"],gf=Object(s.a)("ConditionalNode",yf,function(e){var t=e.Node;function n(e,t,r){if(!(this instanceof n))throw new SyntaxError("Constructor must be called with the new operator");if(!Object(ie.w)(e))throw new TypeError("Parameter condition must be a Node");if(!Object(ie.w)(t))throw new TypeError("Parameter trueExpr must be a Node");if(!Object(ie.w)(r))throw new TypeError("Parameter falseExpr must be a Node");this.condition=e,this.trueExpr=t,this.falseExpr=r}return(n.prototype=new t).type="ConditionalNode",n.prototype.isConditionalNode=!0,n.prototype._compile=function(e,t){var n=this.condition._compile(e,t),i=this.trueExpr._compile(e,t),a=this.falseExpr._compile(e,t);return function(e,t,r){return(function(e){if("number"==typeof e||"boolean"==typeof e||"string"==typeof e)return!!e;if(e){if(Object(ie.e)(e))return!e.isZero();if(Object(ie.j)(e))return!(!e.re&&!e.im);if(Object(ie.L)(e))return!!e.value}if(null!=e)throw new TypeError('Unsupported type of condition "'+Object(ie.M)(e)+'"');return!1}(n(e,t,r))?i:a)(e,t,r)}},n.prototype.forEach=function(e){e(this.condition,"condition",this),e(this.trueExpr,"trueExpr",this),e(this.falseExpr,"falseExpr",this)},n.prototype.map=function(e){return new n(this._ifNode(e(this.condition,"condition",this)),this._ifNode(e(this.trueExpr,"trueExpr",this)),this._ifNode(e(this.falseExpr,"falseExpr",this)))},n.prototype.clone=function(){return new n(this.condition,this.trueExpr,this.falseExpr)},n.prototype._toString=function(e){var t=e&&e.parenthesis?e.parenthesis:"keep",r=cf(this,t),n=this.condition.toString(e),i=cf(this.condition,t);("all"===t||"OperatorNode"===this.condition.type||null!==i&&i<=r)&&(n="("+n+")");var a=this.trueExpr.toString(e),o=cf(this.trueExpr,t);("all"===t||"OperatorNode"===this.trueExpr.type||null!==o&&o<=r)&&(a="("+a+")");var s=this.falseExpr.toString(e),u=cf(this.falseExpr,t);return("all"===t||"OperatorNode"===this.falseExpr.type||null!==u&&u<=r)&&(s="("+s+")"),n+" ? "+a+" : "+s},n.prototype.toJSON=function(){return{mathjs:"ConditionalNode",condition:this.condition,trueExpr:this.trueExpr,falseExpr:this.falseExpr}},n.fromJSON=function(e){return new n(e.condition,e.trueExpr,e.falseExpr)},n.prototype.toHTML=function(e){var t=e&&e.parenthesis?e.parenthesis:"keep",r=cf(this,t),n=this.condition.toHTML(e),i=cf(this.condition,t);("all"===t||"OperatorNode"===this.condition.type||null!==i&&i<=r)&&(n='('+n+')');var a=this.trueExpr.toHTML(e),o=cf(this.trueExpr,t);("all"===t||"OperatorNode"===this.trueExpr.type||null!==o&&o<=r)&&(a='('+a+')');var s=this.falseExpr.toHTML(e),u=cf(this.falseExpr,t);return("all"===t||"OperatorNode"===this.falseExpr.type||null!==u&&u<=r)&&(s='('+s+')'),n+'?'+a+':'+s},n.prototype._toTex=function(e){return"\\begin{cases} {"+this.trueExpr.toTex(e)+"}, &\\quad{\\text{if }\\;"+this.condition.toTex(e)+"}\\\\{"+this.falseExpr.toTex(e)+"}, &\\quad{\\text{otherwise}}\\end{cases}"},n},{isClass:!0,isNode:!0}),bf=r(16),vf=r.n(bf),xf={Alpha:"A",alpha:"\\alpha",Beta:"B",beta:"\\beta",Gamma:"\\Gamma",gamma:"\\gamma",Delta:"\\Delta",delta:"\\delta",Epsilon:"E",epsilon:"\\epsilon",varepsilon:"\\varepsilon",Zeta:"Z",zeta:"\\zeta",Eta:"H",eta:"\\eta",Theta:"\\Theta",theta:"\\theta",vartheta:"\\vartheta",Iota:"I",iota:"\\iota",Kappa:"K",kappa:"\\kappa",varkappa:"\\varkappa",Lambda:"\\Lambda",lambda:"\\lambda",Mu:"M",mu:"\\mu",Nu:"N",nu:"\\nu",Xi:"\\Xi",xi:"\\xi",Omicron:"O",omicron:"o",Pi:"\\Pi",pi:"\\pi",varpi:"\\varpi",Rho:"P",rho:"\\rho",varrho:"\\varrho",Sigma:"\\Sigma",sigma:"\\sigma",varsigma:"\\varsigma",Tau:"T",tau:"\\tau",Upsilon:"\\Upsilon",upsilon:"\\upsilon",Phi:"\\Phi",phi:"\\phi",varphi:"\\varphi",Chi:"X",chi:"\\chi",Psi:"\\Psi",psi:"\\psi",Omega:"\\Omega",omega:"\\omega",true:"\\mathrm{True}",false:"\\mathrm{False}",i:"i",inf:"\\infty",Inf:"\\infty",infinity:"\\infty",Infinity:"\\infty",oo:"\\infty",lim:"\\lim",undefined:"\\mathbf{?}"},wf={transpose:"^\\top",ctranspose:"^H",factorial:"!",pow:"^",dotPow:".^\\wedge",unaryPlus:"+",unaryMinus:"-",bitNot:"\\~",not:"\\neg",multiply:"\\cdot",divide:"\\frac",dotMultiply:".\\cdot",dotDivide:".:",mod:"\\mod",add:"+",subtract:"-",to:"\\rightarrow",leftShift:"<<",rightArithShift:">>",rightLogShift:">>>",equal:"=",unequal:"\\neq",smaller:"<",larger:">",smallerEq:"\\leq",largerEq:"\\geq",bitAnd:"\\&",bitXor:"\\underline{|}",bitOr:"|",and:"\\wedge",xor:"\\veebar",or:"\\vee"},Nf={abs:{1:"\\left|${args[0]}\\right|"},add:{2:"\\left(${args[0]}".concat(wf.add,"${args[1]}\\right)")},cbrt:{1:"\\sqrt[3]{${args[0]}}"},ceil:{1:"\\left\\lceil${args[0]}\\right\\rceil"},cube:{1:"\\left(${args[0]}\\right)^3"},divide:{2:"\\frac{${args[0]}}{${args[1]}}"},dotDivide:{2:"\\left(${args[0]}".concat(wf.dotDivide,"${args[1]}\\right)")},dotMultiply:{2:"\\left(${args[0]}".concat(wf.dotMultiply,"${args[1]}\\right)")},dotPow:{2:"\\left(${args[0]}".concat(wf.dotPow,"${args[1]}\\right)")},exp:{1:"\\exp\\left(${args[0]}\\right)"},expm1:"\\left(e".concat(wf.pow,"{${args[0]}}-1\\right)"),fix:{1:"\\mathrm{${name}}\\left(${args[0]}\\right)"},floor:{1:"\\left\\lfloor${args[0]}\\right\\rfloor"},gcd:"\\gcd\\left(${args}\\right)",hypot:"\\hypot\\left(${args}\\right)",log:{1:"\\ln\\left(${args[0]}\\right)",2:"\\log_{${args[1]}}\\left(${args[0]}\\right)"},log10:{1:"\\log_{10}\\left(${args[0]}\\right)"},log1p:{1:"\\ln\\left(${args[0]}+1\\right)",2:"\\log_{${args[1]}}\\left(${args[0]}+1\\right)"},log2:"\\log_{2}\\left(${args[0]}\\right)",mod:{2:"\\left(${args[0]}".concat(wf.mod,"${args[1]}\\right)")},multiply:{2:"\\left(${args[0]}".concat(wf.multiply,"${args[1]}\\right)")},norm:{1:"\\left\\|${args[0]}\\right\\|",2:void 0},nthRoot:{2:"\\sqrt[${args[1]}]{${args[0]}}"},nthRoots:{2:"\\{y : $y^{args[1]} = {${args[0]}}\\}"},pow:{2:"\\left(${args[0]}\\right)".concat(wf.pow,"{${args[1]}}")},round:{1:"\\left\\lfloor${args[0]}\\right\\rceil",2:void 0},sign:{1:"\\mathrm{${name}}\\left(${args[0]}\\right)"},sqrt:{1:"\\sqrt{${args[0]}}"},square:{1:"\\left(${args[0]}\\right)^2"},subtract:{2:"\\left(${args[0]}".concat(wf.subtract,"${args[1]}\\right)")},unaryMinus:{1:"".concat(wf.unaryMinus,"\\left(${args[0]}\\right)")},unaryPlus:{1:"".concat(wf.unaryPlus,"\\left(${args[0]}\\right)")},bitAnd:{2:"\\left(${args[0]}".concat(wf.bitAnd,"${args[1]}\\right)")},bitNot:{1:wf.bitNot+"\\left(${args[0]}\\right)"},bitOr:{2:"\\left(${args[0]}".concat(wf.bitOr,"${args[1]}\\right)")},bitXor:{2:"\\left(${args[0]}".concat(wf.bitXor,"${args[1]}\\right)")},leftShift:{2:"\\left(${args[0]}".concat(wf.leftShift,"${args[1]}\\right)")},rightArithShift:{2:"\\left(${args[0]}".concat(wf.rightArithShift,"${args[1]}\\right)")},rightLogShift:{2:"\\left(${args[0]}".concat(wf.rightLogShift,"${args[1]}\\right)")},bellNumbers:{1:"\\mathrm{B}_{${args[0]}}"},catalan:{1:"\\mathrm{C}_{${args[0]}}"},stirlingS2:{2:"\\mathrm{S}\\left(${args}\\right)"},arg:{1:"\\arg\\left(${args[0]}\\right)"},conj:{1:"\\left(${args[0]}\\right)^*"},im:{1:"\\Im\\left\\lbrace${args[0]}\\right\\rbrace"},re:{1:"\\Re\\left\\lbrace${args[0]}\\right\\rbrace"},and:{2:"\\left(${args[0]}".concat(wf.and,"${args[1]}\\right)")},not:{1:wf.not+"\\left(${args[0]}\\right)"},or:{2:"\\left(${args[0]}".concat(wf.or,"${args[1]}\\right)")},xor:{2:"\\left(${args[0]}".concat(wf.xor,"${args[1]}\\right)")},cross:{2:"\\left(${args[0]}\\right)\\times\\left(${args[1]}\\right)"},ctranspose:{1:"\\left(${args[0]}\\right)".concat(wf.ctranspose)},det:{1:"\\det\\left(${args[0]}\\right)"},dot:{2:"\\left(${args[0]}\\cdot${args[1]}\\right)"},expm:{1:"\\exp\\left(${args[0]}\\right)"},inv:{1:"\\left(${args[0]}\\right)^{-1}"},sqrtm:{1:"{${args[0]}}".concat(wf.pow,"{\\frac{1}{2}}")},trace:{1:"\\mathrm{tr}\\left(${args[0]}\\right)"},transpose:{1:"\\left(${args[0]}\\right)".concat(wf.transpose)},combinations:{2:"\\binom{${args[0]}}{${args[1]}}"},combinationsWithRep:{2:"\\left(\\!\\!{\\binom{${args[0]}}{${args[1]}}}\\!\\!\\right)"},factorial:{1:"\\left(${args[0]}\\right)".concat(wf.factorial)},gamma:{1:"\\Gamma\\left(${args[0]}\\right)"},equal:{2:"\\left(${args[0]}".concat(wf.equal,"${args[1]}\\right)")},larger:{2:"\\left(${args[0]}".concat(wf.larger,"${args[1]}\\right)")},largerEq:{2:"\\left(${args[0]}".concat(wf.largerEq,"${args[1]}\\right)")},smaller:{2:"\\left(${args[0]}".concat(wf.smaller,"${args[1]}\\right)")},smallerEq:{2:"\\left(${args[0]}".concat(wf.smallerEq,"${args[1]}\\right)")},unequal:{2:"\\left(${args[0]}".concat(wf.unequal,"${args[1]}\\right)")},erf:{1:"erf\\left(${args[0]}\\right)"},max:"\\max\\left(${args}\\right)",min:"\\min\\left(${args}\\right)",variance:"\\mathrm{Var}\\left(${args}\\right)",acos:{1:"\\cos^{-1}\\left(${args[0]}\\right)"},acosh:{1:"\\cosh^{-1}\\left(${args[0]}\\right)"},acot:{1:"\\cot^{-1}\\left(${args[0]}\\right)"},acoth:{1:"\\coth^{-1}\\left(${args[0]}\\right)"},acsc:{1:"\\csc^{-1}\\left(${args[0]}\\right)"},acsch:{1:"\\mathrm{csch}^{-1}\\left(${args[0]}\\right)"},asec:{1:"\\sec^{-1}\\left(${args[0]}\\right)"},asech:{1:"\\mathrm{sech}^{-1}\\left(${args[0]}\\right)"},asin:{1:"\\sin^{-1}\\left(${args[0]}\\right)"},asinh:{1:"\\sinh^{-1}\\left(${args[0]}\\right)"},atan:{1:"\\tan^{-1}\\left(${args[0]}\\right)"},atan2:{2:"\\mathrm{atan2}\\left(${args}\\right)"},atanh:{1:"\\tanh^{-1}\\left(${args[0]}\\right)"},cos:{1:"\\cos\\left(${args[0]}\\right)"},cosh:{1:"\\cosh\\left(${args[0]}\\right)"},cot:{1:"\\cot\\left(${args[0]}\\right)"},coth:{1:"\\coth\\left(${args[0]}\\right)"},csc:{1:"\\csc\\left(${args[0]}\\right)"},csch:{1:"\\mathrm{csch}\\left(${args[0]}\\right)"},sec:{1:"\\sec\\left(${args[0]}\\right)"},sech:{1:"\\mathrm{sech}\\left(${args[0]}\\right)"},sin:{1:"\\sin\\left(${args[0]}\\right)"},sinh:{1:"\\sinh\\left(${args[0]}\\right)"},tan:{1:"\\tan\\left(${args[0]}\\right)"},tanh:{1:"\\tanh\\left(${args[0]}\\right)"},to:{2:"\\left(${args[0]}".concat(wf.to,"${args[1]}\\right)")},numeric:function(e){return e.args[0].toTex()},number:{0:"0",1:"\\left(${args[0]}\\right)",2:"\\left(\\left(${args[0]}\\right)${args[1]}\\right)"},string:{0:'\\mathtt{""}',1:"\\mathrm{string}\\left(${args[0]}\\right)"},bignumber:{0:"0",1:"\\left(${args[0]}\\right)"},complex:{0:"0",1:"\\left(${args[0]}\\right)",2:"\\left(\\left(${args[0]}\\right)+".concat(xf.i,"\\cdot\\left(${args[1]}\\right)\\right)")},matrix:{0:"\\begin{bmatrix}\\end{bmatrix}",1:"\\left(${args[0]}\\right)",2:"\\left(${args[0]}\\right)"},sparse:{0:"\\begin{bsparse}\\end{bsparse}",1:"\\left(${args[0]}\\right)"},unit:{1:"\\left(${args[0]}\\right)",2:"\\left(\\left(${args[0]}\\right)${args[1]}\\right)"}},Of={deg:"^\\circ"};function Mf(e){return vf()(e,{preserveFormatting:!0})}function jf(e,t){return(t=void 0!==t&&t)?Object(ae.f)(Of,e)?Of[e]:"\\mathrm{"+Mf(e)+"}":Object(ae.f)(xf,e)?xf[e]:Mf(e)}var Ef=["Node"],Sf=Object(s.a)("ConstantNode",Ef,function(e){var t=e.Node;function r(e){if(!(this instanceof r))throw new SyntaxError("Constructor must be called with the new operator");this.value=e}return(r.prototype=new t).type="ConstantNode",r.prototype.isConstantNode=!0,r.prototype._compile=function(e,t){var r=this.value;return function(){return r}},r.prototype.forEach=function(e){},r.prototype.map=function(e){return this.clone()},r.prototype.clone=function(){return new r(this.value)},r.prototype._toString=function(e){return Object(V.d)(this.value,e)},r.prototype.toHTML=function(e){var t=this._toString(e);switch(Object(ie.M)(this.value)){case"number":case"BigNumber":case"Fraction":return''+t+"";case"string":return''+t+"";case"boolean":return''+t+"";case"null":return''+t+"";case"undefined":return''+t+"";default:return''+t+""}},r.prototype.toJSON=function(){return{mathjs:"ConstantNode",value:this.value}},r.fromJSON=function(e){return new r(e.value)},r.prototype._toTex=function(e){var t=this._toString(e);switch(Object(ie.M)(this.value)){case"string":return"\\mathtt{"+Mf(t)+"}";case"number":case"BigNumber":if(!isFinite(this.value))return this.value.valueOf()<0?"-\\infty":"\\infty";var r=t.toLowerCase().indexOf("e");return-1!==r?t.substring(0,r)+"\\cdot10^{"+t.substring(r+1)+"}":t;case"Fraction":return this.value.toLatex();default:return t}},r},{isClass:!0,isNode:!0}),Af=["typed","Node"],Cf=Object(s.a)("FunctionAssignmentNode",Af,function(e){var f=e.typed,t=e.Node;function n(e,t,r){if(!(this instanceof n))throw new SyntaxError("Constructor must be called with the new operator");if("string"!=typeof e)throw new TypeError('String expected for parameter "name"');if(!Array.isArray(t))throw new TypeError('Array containing strings or objects expected for parameter "params"');if(!Object(ie.w)(r))throw new TypeError('Node expected for parameter "expr"');if(e in Wc)throw new Error('Illegal function name, "'+e+'" is a reserved keyword');this.name=e,this.params=t.map(function(e){return e&&e.name||e}),this.types=t.map(function(e){return e&&e.type||"any"}),this.expr=r}function a(e,t){var r=cf(e,t),n=cf(e.expr,t);return"all"===t||null!==n&&n<=r}return(n.prototype=new t).type="FunctionAssignmentNode",n.prototype.isFunctionAssignmentNode=!0,n.prototype._compile=function(e,t){var r=Object.create(t);Object(q.f)(this.params,function(e){r[e]=!0});var a=this.expr._compile(e,r),o=this.name,s=this.params,u=Object(q.j)(this.types,","),c=o+"("+Object(q.j)(this.params,", ")+")";return function(r,n,i){var e={};e[u]=function(){for(var e=Object.create(n),t=0;t'+Object(V.c)(this.params[n])+"");var i=this.expr.toHTML(e);return a(this,t)&&(i='('+i+')'),''+Object(V.c)(this.name)+'('+r.join(',')+')='+i},n.prototype._toTex=function(e){var t=e&&e.parenthesis?e.parenthesis:"keep",r=this.expr.toTex(e);return a(this,t)&&(r="\\left(".concat(r,"\\right)")),"\\mathrm{"+this.name+"}\\left("+this.params.map(jf).join(",")+"\\right):="+r},n},{isClass:!0,isNode:!0});function _f(e){return function(e){if(Array.isArray(e))return Tf(e)}(e)||function(e){if("undefined"!=typeof Symbol&&Symbol.iterator in Object(e))return Array.from(e)}(e)||function(e,t){if(!e)return;if("string"==typeof e)return Tf(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);"Object"===r&&e.constructor&&(r=e.constructor.name);if("Map"===r||"Set"===r)return Array.from(e);if("Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return Tf(e,t)}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function Tf(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r.'+Object(V.c)(this.getObjectProperty())+"":'['+t.join(',')+']'},i.prototype._toTex=function(t){var e=this.dimensions.map(function(e){return e.toTex(t)});return this.dotNotation?"."+this.getObjectProperty():"_{"+e.join(",")+"}"},i},{isClass:!0,isNode:!0});function kf(e){return(kf="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}var Bf=["Node"],zf=Object(s.a)("ObjectNode",Bf,function(e){var t=e.Node;function n(t){if(!(this instanceof n))throw new SyntaxError("Constructor must be called with the new operator");if(this.properties=t||{},t&&("object"!==kf(t)||!Object.keys(t).every(function(e){return Object(ie.w)(t[e])})))throw new TypeError("Object containing Nodes expected")}return(n.prototype=new t).type="ObjectNode",n.prototype.isObjectNode=!0,n.prototype._compile=function(e,t){var r,a={};for(r in this.properties)if(Object(ae.f)(this.properties,r)){var n=Object(V.e)(r),i=JSON.parse(n);if(!Ui(this.properties,i))throw new Error('No access to property "'+i+'"');a[i]=this.properties[r]._compile(e,t)}return function(e,t,r){var n,i={};for(n in a)Object(ae.f)(a,n)&&(i[n]=a[n](e,t,r));return i}},n.prototype.forEach=function(e){for(var t in this.properties)Object(ae.f)(this.properties,t)&&e(this.properties[t],"properties["+Object(V.e)(t)+"]",this)},n.prototype.map=function(e){var t,r={};for(t in this.properties)Object(ae.f)(this.properties,t)&&(r[t]=this._ifNode(e(this.properties[t],"properties["+Object(V.e)(t)+"]",this)));return new n(r)},n.prototype.clone=function(){var e,t={};for(e in this.properties)Object(ae.f)(this.properties,e)&&(t[e]=this.properties[e]);return new n(t)},n.prototype._toString=function(e){var t,r=[];for(t in this.properties)Object(ae.f)(this.properties,t)&&r.push(Object(V.e)(t)+": "+this.properties[t].toString(e));return"{"+r.join(", ")+"}"},n.prototype.toJSON=function(){return{mathjs:"ObjectNode",properties:this.properties}},n.fromJSON=function(e){return new n(e.properties)},n.prototype.toHTML=function(e){var t,r=[];for(t in this.properties)Object(ae.f)(this.properties,t)&&r.push(''+Object(V.c)(t)+':'+this.properties[t].toHTML(e));return'{'+r.join(',')+'}'},n.prototype._toTex=function(e){var t,r=[];for(t in this.properties)Object(ae.f)(this.properties,t)&&r.push("\\mathbf{"+t+":} & "+this.properties[t].toTex(e)+"\\\\");return"\\left\\{\\begin{array}{ll}".concat(r.join("\n"),"\\end{array}\\right\\}")},n},{isClass:!0,isNode:!0}),Df=["Node"],Rf=Object(s.a)("OperatorNode",Df,function(e){var t=e.Node;function i(e,t,r,n){if(!(this instanceof i))throw new SyntaxError("Constructor must be called with the new operator");if("string"!=typeof e)throw new TypeError('string expected for parameter "op"');if("string"!=typeof t)throw new TypeError('string expected for parameter "fn"');if(!Array.isArray(r)||!r.every(ie.w))throw new TypeError('Array containing Nodes expected for parameter "args"');this.implicit=!0===n,this.op=e,this.fn=t,this.args=r||[]}function m(i,a,e,t,r){var n,o=cf(i,a),s=ff(i,a);if("all"===a||2)'),"right"===a?''+Object(V.c)(this.op)+""+o:o+''+Object(V.c)(this.op)+""}if(2===n.length){var s=n[0].toHTML(r),u=n[1].toHTML(r);return i[0]&&(s='('+s+')'),i[1]&&(u='('+u+')'),this.implicit&&"OperatorNode:multiply"===this.getIdentifier()&&"hide"===t?s+''+u:s+''+Object(V.c)(this.op)+""+u}var c=n.map(function(e,t){return e=e.toHTML(r),i[t]&&(e='('+e+')'),e});return 2'):c.join(''+Object(V.c)(this.op)+""):''+Object(V.c)(this.fn)+'('+c.join(',')+')'},i.prototype._toTex=function(r){var e=r&&r.parenthesis?r.parenthesis:"keep",t=r&&r.implicit?r.implicit:"hide",n=this.args,i=m(this,e,t,n,!0),a=void 0===(a=wf[this.fn])?this.op:a;if(1===n.length){var o=ff(this,e),s=n[0].toTex(r);return i[0]&&(s="\\left(".concat(s,"\\right)")),"right"===o?a+s:s+a}if(2===n.length){var u=n[0],c=u.toTex(r);i[0]&&(c="\\left(".concat(c,"\\right)"));var f,l=n[1].toTex(r);switch(i[1]&&(l="\\left(".concat(l,"\\right)")),f="keep"===e?u.getIdentifier():u.getContent().getIdentifier(),this.getIdentifier()){case"OperatorNode:divide":return a+"{"+c+"}{"+l+"}";case"OperatorNode:pow":switch(c="{"+c+"}",l="{"+l+"}",f){case"ConditionalNode":case"OperatorNode:divide":c="\\left(".concat(c,"\\right)")}break;case"OperatorNode:multiply":if(this.implicit&&"hide"===t)return c+"~"+l}return c+a+l}if(2('+this.content.toHTML(e)+')':this.content.toHTML(e)},r.prototype._toTex=function(e){return!e||e&&!e.parenthesis||e&&"keep"===e.parenthesis?"\\left(".concat(this.content.toTex(e),"\\right)"):this.content.toTex(e)},r},{isClass:!0,isNode:!0}),Ff=["Node"],Lf=Object(s.a)("RangeNode",Ff,function(e){var t=e.Node;function n(e,t,r){if(!(this instanceof n))throw new SyntaxError("Constructor must be called with the new operator");if(!Object(ie.w)(e))throw new TypeError("Node expected");if(!Object(ie.w)(t))throw new TypeError("Node expected");if(r&&!Object(ie.w)(r))throw new TypeError("Node expected");if(3('+i+')'),t=i,this.step&&(r=this.step.toHTML(e),n.step&&(r='('+r+')'),t+=':'+r);var a=this.end.toHTML(e);return n.end&&(a='('+a+')'),t+=':'+a},n.prototype._toTex=function(e){var t,r=o(this,e&&e.parenthesis?e.parenthesis:"keep"),n=this.start.toTex(e);r.start&&(n="\\left(".concat(n,"\\right)")),this.step&&(t=this.step.toTex(e),r.step&&(t="\\left(".concat(t,"\\right)")),n+=":"+t);var i=this.end.toTex(e);return r.end&&(i="\\left(".concat(i,"\\right)")),n+=":"+i},n},{isClass:!0,isNode:!0}),Hf=["Node"],$f=Object(s.a)("RelationalNode",Hf,function(e){var t=e.Node;function i(e,t){if(!(this instanceof i))throw new SyntaxError("Constructor must be called with the new operator");if(!Array.isArray(e))throw new TypeError("Parameter conditionals must be an array");if(!Array.isArray(t))throw new TypeError("Parameter params must be an array");if(e.length!==t.length-1)throw new TypeError("Parameter params must contain exactly one more element than parameter conditionals");this.conditionals=e,this.params=t}return(i.prototype=new t).type="RelationalNode",i.prototype.isRelationalNode=!0,i.prototype._compile=function(o,t){var s=this,u=this.params.map(function(e){return e._compile(o,t)});return function(e,t,r){for(var n=u[0](e,t,r),i=0;i",smallerEq:"<=",largerEq:">="},r=e[0],o=0;o('+e.toHTML(n)+')':e.toHTML(n)}),t={equal:"==",unequal:"!=",smaller:"<",larger:">",smallerEq:"<=",largerEq:">="},r=e[0],o=0;o'+Object(V.c)(t[this.conditionals[o]])+""+e[o+1];return r},i.prototype._toTex=function(n){for(var i=n&&n.parenthesis?n.parenthesis:"keep",a=cf(this,i),e=this.params.map(function(e,t){var r=cf(e,i);return"all"===i||null!==r&&r<=a?"\\left("+e.toTex(n)+"\right)":e.toTex(n)}),t=e[0],r=0;r'+t+"":"i"===t?''+t+"":"Infinity"===t?''+t+"":"NaN"===t?''+t+"":"null"===t?''+t+"":"undefined"===t?''+t+"":''+t+""},r.prototype.toJSON=function(){return{mathjs:"SymbolNode",name:this.name}},r.fromJSON=function(e){return new r(e.name)},r.prototype._toTex=function(e){var t=!1;void 0===n[this.name]&&s(this.name)&&(t=!0);var r=jf(this.name,t);return"\\"===r[0]?r:" "+r},r},{isClass:!0,isNode:!0});function Vf(e){return(Vf="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function Jf(){return(Jf=Object.assign||function(e){for(var t=1;t'+Object(V.c)(this.fn)+'('+e.join(',')+')'};var o=d.prototype.toTex;return d.prototype.toTex=function(e){var t;return e&&"object"===Vf(e.handler)&&Object(ae.f)(e.handler,this.name)&&(t=e.handler[this.name](this,e)),void 0!==t?t:o.call(this,e)},d.prototype._toTex=function(t){var e,r,n=this.args.map(function(e){return e.toTex(t)});switch(Nf[this.name]&&(e=Nf[this.name]),!i[this.name]||"function"!=typeof i[this.name].toTex&&"object"!==Vf(i[this.name].toTex)&&"string"!=typeof i[this.name].toTex||(e=i[this.name].toTex),Vf(e)){case"function":r=e(this,t);break;case"string":r=a(e,this,t);break;case"object":switch(Vf(e[n.length])){case"function":r=e[n.length](this,t);break;case"string":r=a(e[n.length],this,t)}}return void 0!==r?r:a("\\mathrm{${name}}\\left(${args}\\right)",this,t)},d.prototype.getIdentifier=function(){return this.type+":"+this.name},d},{isClass:!0,isNode:!0});function Xf(){return(Xf=Object.assign||function(e){for(var t=1;t":!0,"<=":!0,">=":!0,"<<":!0,">>":!0,">>>":!0},O={mod:!0,to:!0,in:!0,and:!0,xor:!0,or:!0,not:!0},M={true:!0,false:!1,null:null,undefined:void 0},j=["NaN","Infinity"];function E(e,t){return e.expression.substr(e.index,t)}function S(e){return E(e,1)}function A(e){e.index++}function C(e){return e.expression.charAt(e.index-1)}function _(e){return e.expression.charAt(e.index+1)}function T(e){for(e.tokenType=w.NULL,e.token="",e.comment="";x.isWhitespace(S(e),e.nestingLevel);)A(e);if("#"===S(e))for(;"\n"!==S(e)&&""!==S(e);)e.comment+=S(e),A(e);if(""!==S(e)){if("\n"===S(e)&&!e.nestingLevel)return e.tokenType=w.DELIMITER,e.token=S(e),void A(e);var t=S(e),r=E(e,2),n=E(e,3);if(3===n.length&&N[n])return e.tokenType=w.DELIMITER,e.token=n,A(e),A(e),void A(e);if(2===r.length&&N[r])return e.tokenType=w.DELIMITER,e.token=r,A(e),void A(e);if(N[t])return e.tokenType=w.DELIMITER,e.token=t,void A(e);if(x.isDigitDot(t)){e.tokenType=w.NUMBER;var i=E(e,2);if("0b"===i||"0o"===i||"0x"===i){for(e.token+=S(e),A(e),e.token+=S(e),A(e);x.isHexDigit(S(e));)e.token+=S(e),A(e);return}if("."===S(e))e.token+=S(e),A(e),x.isDigit(S(e))||(e.tokenType=w.DELIMITER);else{for(;x.isDigit(S(e));)e.token+=S(e),A(e);x.isDecimalMark(S(e),_(e))&&(e.token+=S(e),A(e))}for(;x.isDigit(S(e));)e.token+=S(e),A(e);if("E"===S(e)||"e"===S(e))if(x.isDigit(_(e))||"-"===_(e)||"+"===_(e)){if(e.token+=S(e),A(e),"+"!==S(e)&&"-"!==S(e)||(e.token+=S(e),A(e)),!x.isDigit(S(e)))throw re(e,'Digit expected, got "'+S(e)+'"');for(;x.isDigit(S(e));)e.token+=S(e),A(e);if(x.isDecimalMark(S(e),_(e)))throw re(e,'Digit expected, got "'+S(e)+'"')}else if("."===_(e))throw A(e),re(e,'Digit expected, got "'+S(e)+'"')}else{if(!x.isAlpha(S(e),C(e),_(e))){for(e.tokenType=w.UNKNOWN;""!==S(e);)e.token+=S(e),A(e);throw re(e,'Syntax error in part "'+e.token+'"')}for(;x.isAlpha(S(e),C(e),_(e))||x.isDigit(S(e));)e.token+=S(e),A(e);Object(ae.f)(O,e.token)?e.tokenType=w.DELIMITER:e.tokenType=w.SYMBOL}}else e.tokenType=w.DELIMITER}function q(e){for(;T(e),"\n"===e.token;);}function I(e){e.nestingLevel++}function k(e){e.nestingLevel--}function B(e,t){var r={extraNodes:{},expression:"",comment:"",index:0,token:"",tokenType:w.NULL,nestingLevel:0,conditionalLevel:null};Xf(r,{expression:e,extraNodes:t}),T(r);var n=function(e){var t,r,n=[];""!==e.token&&"\n"!==e.token&&";"!==e.token&&((t=z(e)).comment=e.comment);for(;"\n"===e.token||";"===e.token;)0===n.length&&t&&(r=";"!==e.token,n.push({node:t,visible:r})),T(e),"\n"!==e.token&&";"!==e.token&&""!==e.token&&((t=z(e)).comment=e.comment,r=";"!==e.token,n.push({node:t,visible:r}));return 0":"larger","<=":"smallerEq",">=":"largerEq"};Object(ae.f)(n,e.token);){var i={name:e.token,fn:n[e.token]};r.push(i),q(e),t.push(H(e))}return 1===t.length?t[0]:2===t.length?new y(r[0].name,r[0].fn,t):new b(r.map(function(e){return e.fn}),t)}function H(e){for(var t,r,n,i=$(e),a={"<<":"leftShift",">>":"rightArithShift",">>>":"rightLogShift"};Object(ae.f)(a,e.token);)r=a[t=e.token],q(e),n=[i,$(e)],i=new y(t,r,n);return i}function $(e){for(var t,r,n,i=G(e),a={to:"to",in:"to"};Object(ae.f)(a,e.token);)r=a[t=e.token],q(e),i="in"===t&&""===e.token?new y("*","multiply",[i,new v("in")],!0):(n=[i,G(e)],new y(t,r,n));return i}function G(e){var t=[],r=":"===e.token?new l(1):Z(e);if(":"===e.token&&e.conditionalLevel!==e.nestingLevel){for(t.push(r);":"===e.token&&t.length<3;)q(e),")"===e.token||"]"===e.token||","===e.token||""===e.token?t.push(new v("end")):t.push(Z(e));r=3===t.length?new n(t[0],t[2],t[1]):new n(t[0],t[1])}return r}function Z(e){for(var t,r,n,i=V(e),a={"+":"add","-":"subtract"};Object(ae.f)(a,e.token);)r=a[t=e.token],q(e),n=[i,V(e)],i=new y(t,r,n);return i}function V(e){for(var t,r,n,i=J(e),a={"*":"multiply",".*":"dotMultiply","/":"divide","./":"dotDivide","%":"mod",mod:"mod"};Object(ae.f)(a,e.token);)r=a[t=e.token],q(e),n=J(e),i=new y(t,r,[i,n]);return i}function J(e){for(var t=W(e),r=t;e.tokenType===w.SYMBOL||"in"===e.token&&Object(ie.l)(t)||!(e.tokenType!==w.NUMBER||Object(ie.l)(r)||Object(ie.B)(r)&&"!"!==r.op)||"("===e.token;)r=W(e),t=new y("*","multiply",[t,r],!0);return t}function W(e){for(var t=Y(e),r=t,n=[];"/"===e.token&&Object(ie.l)(r);){if(n.push(Xf({},e)),q(e),e.tokenType!==w.NUMBER){Xf(e,n.pop());break}if(n.push(Xf({},e)),q(e),e.tokenType!==w.SYMBOL&&"("!==e.token){n.pop(),Xf(e,n.pop());break}Xf(e,n.pop()),n.pop(),r=Y(e),t=new y("/","divide",[t,r])}return t}function Y(e){var t,r,n,i={"-":"unaryMinus","+":"unaryPlus","~":"bitNot",not:"not"};return Object(ae.f)(i,e.token)?(n=i[e.token],t=e.token,q(e),r=[Y(e)],new y(t,n,r)):function(e){var t,r,n,i;t=function(e){var t,r,n;t=function(e){var t=[];if(e.tokenType===w.SYMBOL&&Object(ae.f)(e.extraNodes,e.token)){var r=e.extraNodes[e.token];if(T(e),"("===e.token){if(t=[],I(e),T(e),")"!==e.token)for(t.push(z(e));","===e.token;)T(e),t.push(z(e));if(")"!==e.token)throw re(e,"Parenthesis ) expected");k(e),T(e)}return new r(t)}return function(e){var t,r;if(e.tokenType===w.SYMBOL||e.tokenType===w.DELIMITER&&e.token in O)return r=e.token,T(e),t=Object(ae.f)(M,r)?new l(M[r]):-1!==j.indexOf(r)?new l(s(r,"number")):new v(r),t=X(e,t);return function(e){var t,r;return'"'!==e.token?function(e){var t,r;return"'"!==e.token?function(e){var t,r,n,i;if("["!==e.token)return function(e){if("{"!==e.token)return function(e){var t;return e.tokenType!==w.NUMBER?function(e){var t;if("("!==e.token)return function(e){throw""===e.token?re(e,"Unexpected end of expression"):re(e,"Value expected")}(e);if(I(e),T(e),t=z(e),")"===e.token)return k(e),T(e),t=new g(t),t=X(e,t);throw re(e,"Parenthesis ) expected")}(e):(t=e.token,T(e),new l(s(t,u.number)))}(e);var t;I(e);var r={};do{if(T(e),"}"!==e.token){if('"'===e.token)t=Q(e);else if("'"===e.token)t=K(e);else{if(!(e.tokenType===w.SYMBOL||e.tokenType===w.DELIMITER&&e.token in O))throw re(e,"Symbol or string expected as object key");t=e.token,T(e)}if(":"!==e.token)throw re(e,"Colon : expected after object key");T(e),r[t]=z(e)}}while(","===e.token);if("}"!==e.token)throw re(e,"Comma , or bracket } expected after object value");k(e),T(e);var n=new d(r);return n=X(e,n)}(e);if(I(e),T(e),"]"!==e.token){var a=ee(e);if(";"===e.token){for(n=1,r=[a];";"===e.token;)T(e),r[n]=ee(e),n++;if("]"!==e.token)throw re(e,"End of matrix ] expected");k(e),T(e),i=r[0].items.length;for(var o=1;oi))for(var l=s[c+1];f=I?M[_+U]-=m:0!==M[_+U]&&(M[_+U]=M[C+U]+G);for(h=L;h1, -1 when x<0, and 0 when x=0.",examples:["sign(3.5)","sign(-4.2)","sign(0)"],seealso:["abs"]},sqrt:{name:"sqrt",category:"Arithmetic",syntax:["sqrt(x)"],description:"Compute the square root value. If x = y * y, then y is the square root of x.",examples:["sqrt(25)","5 * 5","sqrt(-1)"],seealso:["square","sqrtm","multiply","nthRoot","nthRoots","pow"]},sqrtm:{name:"sqrtm",category:"Arithmetic",syntax:["sqrtm(x)"],description:"Calculate the principal square root of a square matrix. The principal square root matrix `X` of another matrix `A` is such that `X * X = A`.",examples:["sqrtm([[1, 2], [3, 4]])"],seealso:["sqrt","abs","square","multiply"]},square:{name:"square",category:"Arithmetic",syntax:["square(x)"],description:"Compute the square of a value. The square of x is x * x.",examples:["square(3)","sqrt(9)","3^2","3 * 3"],seealso:["multiply","pow","sqrt","cube"]},subtract:{name:"subtract",category:"Operators",syntax:["x - y","subtract(x, y)"],description:"subtract two values.",examples:["a = 5.3 - 2","a + 2","2/3 - 1/6","2 * 3 - 3","2.1 km - 500m"],seealso:["add"]},unaryMinus:{name:"unaryMinus",category:"Operators",syntax:["-x","unaryMinus(x)"],description:"Inverse the sign of a value. Converts booleans and strings to numbers.",examples:["-4.5","-(-5.6)",'-"22"'],seealso:["add","subtract","unaryPlus"]},unaryPlus:{name:"unaryPlus",category:"Operators",syntax:["+x","unaryPlus(x)"],description:"Converts booleans and strings to numbers.",examples:["+true",'+"2"'],seealso:["add","subtract","unaryMinus"]},xgcd:{name:"xgcd",category:"Arithmetic",syntax:["xgcd(a, b)"],description:"Calculate the extended greatest common divisor for two values. The result is an array [d, x, y] with 3 entries, where d is the greatest common divisor, and d = x * a + y * b.",examples:["xgcd(8, 12)","gcd(8, 12)","xgcd(36163, 21199)"],seealso:["gcd","lcm"]},bitAnd:{name:"bitAnd",category:"Bitwise",syntax:["x & y","bitAnd(x, y)"],description:"Bitwise AND operation. Performs the logical AND operation on each pair of the corresponding bits of the two given values by multiplying them. If both bits in the compared position are 1, the bit in the resulting binary representation is 1, otherwise, the result is 0",examples:["5 & 3","bitAnd(53, 131)","[1, 12, 31] & 42"],seealso:["bitNot","bitOr","bitXor","leftShift","rightArithShift","rightLogShift"]},bitNot:{name:"bitNot",category:"Bitwise",syntax:["~x","bitNot(x)"],description:"Bitwise NOT operation. Performs a logical negation on each bit of the given value. Bits that are 0 become 1, and those that are 1 become 0.",examples:["~1","~2","bitNot([2, -3, 4])"],seealso:["bitAnd","bitOr","bitXor","leftShift","rightArithShift","rightLogShift"]},bitOr:{name:"bitOr",category:"Bitwise",syntax:["x | y","bitOr(x, y)"],description:"Bitwise OR operation. Performs the logical inclusive OR operation on each pair of corresponding bits of the two given values. The result in each position is 1 if the first bit is 1 or the second bit is 1 or both bits are 1, otherwise, the result is 0.",examples:["5 | 3","bitOr([1, 2, 3], 4)"],seealso:["bitAnd","bitNot","bitXor","leftShift","rightArithShift","rightLogShift"]},bitXor:{name:"bitXor",category:"Bitwise",syntax:["bitXor(x, y)"],description:"Bitwise XOR operation, exclusive OR. Performs the logical exclusive OR operation on each pair of corresponding bits of the two given values. The result in each position is 1 if only the first bit is 1 or only the second bit is 1, but will be 0 if both are 0 or both are 1.",examples:["bitOr(1, 2)","bitXor([2, 3, 4], 4)"],seealso:["bitAnd","bitNot","bitOr","leftShift","rightArithShift","rightLogShift"]},leftShift:{name:"leftShift",category:"Bitwise",syntax:["x << y","leftShift(x, y)"],description:"Bitwise left logical shift of a value x by y number of bits.",examples:["4 << 1","8 >> 1"],seealso:["bitAnd","bitNot","bitOr","bitXor","rightArithShift","rightLogShift"]},rightArithShift:{name:"rightArithShift",category:"Bitwise",syntax:["x >> y","rightArithShift(x, y)"],description:"Bitwise right arithmetic shift of a value x by y number of bits.",examples:["8 >> 1","4 << 1","-12 >> 2"],seealso:["bitAnd","bitNot","bitOr","bitXor","leftShift","rightLogShift"]},rightLogShift:{name:"rightLogShift",category:"Bitwise",syntax:["x >>> y","rightLogShift(x, y)"],description:"Bitwise right logical shift of a value x by y number of bits.",examples:["8 >>> 1","4 << 1","-12 >>> 2"],seealso:["bitAnd","bitNot","bitOr","bitXor","leftShift","rightArithShift"]},bellNumbers:{name:"bellNumbers",category:"Combinatorics",syntax:["bellNumbers(n)"],description:"The Bell Numbers count the number of partitions of a set. A partition is a pairwise disjoint subset of S whose union is S. `bellNumbers` only takes integer arguments. The following condition must be enforced: n >= 0.",examples:["bellNumbers(3)","bellNumbers(8)"],seealso:["stirlingS2"]},catalan:{name:"catalan",category:"Combinatorics",syntax:["catalan(n)"],description:"The Catalan Numbers enumerate combinatorial structures of many different types. catalan only takes integer arguments. The following condition must be enforced: n >= 0.",examples:["catalan(3)","catalan(8)"],seealso:["bellNumbers"]},composition:{name:"composition",category:"Combinatorics",syntax:["composition(n, k)"],description:"The composition counts of n into k parts. composition only takes integer arguments. The following condition must be enforced: k <= n.",examples:["composition(5, 3)"],seealso:["combinations"]},stirlingS2:{name:"stirlingS2",category:"Combinatorics",syntax:["stirlingS2(n, k)"],description:"he Stirling numbers of the second kind, counts the number of ways to partition a set of n labelled objects into k nonempty unlabelled subsets. `stirlingS2` only takes integer arguments. The following condition must be enforced: k <= n. If n = k or k = 1, then s(n,k) = 1.",examples:["stirlingS2(5, 3)"],seealso:["bellNumbers"]},config:{name:"config",category:"Core",syntax:["config()","config(options)"],description:"Get configuration or change configuration.",examples:["config()","1/3 + 1/4",'config({number: "Fraction"})',"1/3 + 1/4"],seealso:[]},import:{name:"import",category:"Core",syntax:["import(functions)","import(functions, options)"],description:"Import functions or constants from an object.",examples:["import({myFn: f(x)=x^2, myConstant: 32 })","myFn(2)","myConstant"],seealso:[]},typed:{name:"typed",category:"Core",syntax:["typed(signatures)","typed(name, signatures)"],description:"Create a typed function.",examples:['double = typed({ "number, number": f(x)=x+x })',"double(2)",'double("hello")'],seealso:[]},arg:{name:"arg",category:"Complex",syntax:["arg(x)"],description:"Compute the argument of a complex value. If x = a+bi, the argument is computed as atan2(b, a).",examples:["arg(2 + 2i)","atan2(3, 2)","arg(2 + 3i)"],seealso:["re","im","conj","abs"]},conj:{name:"conj",category:"Complex",syntax:["conj(x)"],description:"Compute the complex conjugate of a complex value. If x = a+bi, the complex conjugate is a-bi.",examples:["conj(2 + 3i)","conj(2 - 3i)","conj(-5.2i)"],seealso:["re","im","abs","arg"]},re:{name:"re",category:"Complex",syntax:["re(x)"],description:"Get the real part of a complex number.",examples:["re(2 + 3i)","im(2 + 3i)","re(-5.2i)","re(2.4)"],seealso:["im","conj","abs","arg"]},im:{name:"im",category:"Complex",syntax:["im(x)"],description:"Get the imaginary part of a complex number.",examples:["im(2 + 3i)","re(2 + 3i)","im(-5.2i)","im(2.4)"],seealso:["re","conj","abs","arg"]},evaluate:{name:"evaluate",category:"Expression",syntax:["evaluate(expression)","evaluate([expr1, expr2, expr3, ...])"],description:"Evaluate an expression or an array with expressions.",examples:['evaluate("2 + 3")','evaluate("sqrt(" + 4 + ")")'],seealso:[]},help:{name:"help",category:"Expression",syntax:["help(object)","help(string)"],description:"Display documentation on a function or data type.",examples:["help(sqrt)",'help("complex")'],seealso:[]},distance:{name:"distance",category:"Geometry",syntax:["distance([x1, y1], [x2, y2])","distance([[x1, y1], [x2, y2]])"],description:"Calculates the Euclidean distance between two points.",examples:["distance([0,0], [4,4])","distance([[0,0], [4,4]])"],seealso:[]},intersect:{name:"intersect",category:"Geometry",syntax:["intersect(expr1, expr2, expr3, expr4)","intersect(expr1, expr2, expr3)"],description:"Computes the intersection point of lines and/or planes.",examples:["intersect([0, 0], [10, 10], [10, 0], [0, 10])","intersect([1, 0, 1], [4, -2, 2], [1, 1, 1, 6])"],seealso:[]},and:{name:"and",category:"Logical",syntax:["x and y","and(x, y)"],description:"Logical and. Test whether two values are both defined with a nonzero/nonempty value.",examples:["true and false","true and true","2 and 4"],seealso:["not","or","xor"]},not:{name:"not",category:"Logical",syntax:["not x","not(x)"],description:"Logical not. Flips the boolean value of given argument.",examples:["not true","not false","not 2","not 0"],seealso:["and","or","xor"]},or:{name:"or",category:"Logical",syntax:["x or y","or(x, y)"],description:"Logical or. Test if at least one value is defined with a nonzero/nonempty value.",examples:["true or false","false or false","0 or 4"],seealso:["not","and","xor"]},xor:{name:"xor",category:"Logical",syntax:["x xor y","xor(x, y)"],description:"Logical exclusive or, xor. Test whether one and only one value is defined with a nonzero/nonempty value.",examples:["true xor false","false xor false","true xor true","0 xor 4"],seealso:["not","and","or"]},concat:{name:"concat",category:"Matrix",syntax:["concat(A, B, C, ...)","concat(A, B, C, ..., dim)"],description:"Concatenate matrices. By default, the matrices are concatenated by the last dimension. The dimension on which to concatenate can be provided as last argument.",examples:["A = [1, 2; 5, 6]","B = [3, 4; 7, 8]","concat(A, B)","concat(A, B, 1)","concat(A, B, 2)"],seealso:["det","diag","identity","inv","ones","range","size","squeeze","subset","trace","transpose","zeros"]},cross:{name:"cross",category:"Matrix",syntax:["cross(A, B)"],description:"Calculate the cross product for two vectors in three dimensional space.",examples:["cross([1, 1, 0], [0, 1, 1])","cross([3, -3, 1], [4, 9, 2])","cross([2, 3, 4], [5, 6, 7])"],seealso:["multiply","dot"]},column:{name:"column",category:"Matrix",syntax:["column(x, index)"],description:"Return a column from a matrix or array.",examples:["A = [[1, 2], [3, 4]]","column(A, 1)","column(A, 2)"],seealso:["row"]},ctranspose:{name:"ctranspose",category:"Matrix",syntax:["x'","ctranspose(x)"],description:"Complex Conjugate and Transpose a matrix",examples:["a = [1, 2, 3; 4, 5, 6]","a'","ctranspose(a)"],seealso:["concat","det","diag","identity","inv","ones","range","size","squeeze","subset","trace","zeros"]},det:{name:"det",category:"Matrix",syntax:["det(x)"],description:"Calculate the determinant of a matrix",examples:["det([1, 2; 3, 4])","det([-2, 2, 3; -1, 1, 3; 2, 0, -1])"],seealso:["concat","diag","identity","inv","ones","range","size","squeeze","subset","trace","transpose","zeros"]},diag:{name:"diag",category:"Matrix",syntax:["diag(x)","diag(x, k)"],description:"Create a diagonal matrix or retrieve the diagonal of a matrix. When x is a vector, a matrix with the vector values on the diagonal will be returned. When x is a matrix, a vector with the diagonal values of the matrix is returned. When k is provided, the k-th diagonal will be filled in or retrieved, if k is positive, the values are placed on the super diagonal. When k is negative, the values are placed on the sub diagonal.",examples:["diag(1:3)","diag(1:3, 1)","a = [1, 2, 3; 4, 5, 6; 7, 8, 9]","diag(a)"],seealso:["concat","det","identity","inv","ones","range","size","squeeze","subset","trace","transpose","zeros"]},diff:{name:"diff",category:"Matrix",syntax:["diff(arr)","diff(arr, dim)"],description:["Create a new matrix or array with the difference of the passed matrix or array.","Dim parameter is optional and used to indicant the dimension of the array/matrix to apply the difference","If no dimension parameter is passed it is assumed as dimension 0","Dimension is zero-based in javascript and one-based in the parser","Arrays must be 'rectangular' meaning arrays like [1, 2]","If something is passed as a matrix it will be returned as a matrix but other than that all matrices are converted to arrays"],examples:["diff([1, 2, 4, 7, 0])","diff([1, 2, 4, 7, 0], 0)","diff(matrix([1, 2, 4, 7, 0]))","diff([[1, 2], [3, 4]])","diff([[1, 2], [3, 4]], 0)","diff([[1, 2], [3, 4]], 1)","diff([[1, 2], [3, 4]], bignumber(1))","diff(matrix([[1, 2], [3, 4]]), 1)","diff([[1, 2], matrix([3, 4])], 1)"],seealso:["subtract","partitionSelect"]},dot:{name:"dot",category:"Matrix",syntax:["dot(A, B)","A * B"],description:"Calculate the dot product of two vectors. The dot product of A = [a1, a2, a3, ..., an] and B = [b1, b2, b3, ..., bn] is defined as dot(A, B) = a1 * b1 + a2 * b2 + a3 * b3 + ... + an * bn",examples:["dot([2, 4, 1], [2, 2, 3])","[2, 4, 1] * [2, 2, 3]"],seealso:["multiply","cross"]},getMatrixDataType:{name:"getMatrixDataType",category:"Matrix",syntax:["getMatrixDataType(x)"],description:'Find the data type of all elements in a matrix or array, for example "number" if all items are a number and "Complex" if all values are complex numbers. If a matrix contains more than one data type, it will return "mixed".',examples:["getMatrixDataType([1, 2, 3])","getMatrixDataType([[5 cm], [2 inch]])",'getMatrixDataType([1, "text"])',"getMatrixDataType([1, bignumber(4)])"],seealso:["matrix","sparse","typeOf"]},identity:{name:"identity",category:"Matrix",syntax:["identity(n)","identity(m, n)","identity([m, n])"],description:"Returns the identity matrix with size m-by-n. The matrix has ones on the diagonal and zeros elsewhere.",examples:["identity(3)","identity(3, 5)","a = [1, 2, 3; 4, 5, 6]","identity(size(a))"],seealso:["concat","det","diag","inv","ones","range","size","squeeze","subset","trace","transpose","zeros"]},filter:{name:"filter",category:"Matrix",syntax:["filter(x, test)"],description:"Filter items in a matrix.",examples:["isPositive(x) = x > 0","filter([6, -2, -1, 4, 3], isPositive)","filter([6, -2, 0, 1, 0], x != 0)"],seealso:["sort","map","forEach"]},flatten:{name:"flatten",category:"Matrix",syntax:["flatten(x)"],description:"Flatten a multi dimensional matrix into a single dimensional matrix.",examples:["a = [1, 2, 3; 4, 5, 6]","size(a)","b = flatten(a)","size(b)"],seealso:["concat","resize","size","squeeze"]},forEach:{name:"forEach",category:"Matrix",syntax:["forEach(x, callback)"],description:"Iterates over all elements of a matrix/array, and executes the given callback function.",examples:["forEach([1, 2, 3], function(val) { console.log(val) })"],seealso:["map","sort","filter"]},inv:{name:"inv",category:"Matrix",syntax:["inv(x)"],description:"Calculate the inverse of a matrix",examples:["inv([1, 2; 3, 4])","inv(4)","1 / 4"],seealso:["concat","det","diag","identity","ones","range","size","squeeze","subset","trace","transpose","zeros"]},eigs:{name:"eigs",category:"Matrix",syntax:["eigs(x)"],description:"Calculate the eigenvalues and eigenvectors of a real symmetric matrix",examples:["eigs([[5, 2.3], [2.3, 1]])"],seealso:["inv"]},kron:{name:"kron",category:"Matrix",syntax:["kron(x, y)"],description:"Calculates the kronecker product of 2 matrices or vectors.",examples:["kron([[1, 0], [0, 1]], [[1, 2], [3, 4]])","kron([1,1], [2,3,4])"],seealso:["multiply","dot","cross"]},map:{name:"map",category:"Matrix",syntax:["map(x, callback)"],description:"Create a new matrix or array with the results of the callback function executed on each entry of the matrix/array.",examples:["map([1, 2, 3], square)"],seealso:["filter","forEach"]},ones:{name:"ones",category:"Matrix",syntax:["ones(m)","ones(m, n)","ones(m, n, p, ...)","ones([m])","ones([m, n])","ones([m, n, p, ...])"],description:"Create a matrix containing ones.",examples:["ones(3)","ones(3, 5)","ones([2,3]) * 4.5","a = [1, 2, 3; 4, 5, 6]","ones(size(a))"],seealso:["concat","det","diag","identity","inv","range","size","squeeze","subset","trace","transpose","zeros"]},partitionSelect:{name:"partitionSelect",category:"Matrix",syntax:["partitionSelect(x, k)","partitionSelect(x, k, compare)"],description:"Partition-based selection of an array or 1D matrix. Will find the kth smallest value, and mutates the input array. Uses Quickselect.",examples:["partitionSelect([5, 10, 1], 2)",'partitionSelect(["C", "B", "A", "D"], 1)'],seealso:["sort"]},range:{name:"range",category:"Type",syntax:["start:end","start:step:end","range(start, end)","range(start, end, step)","range(string)"],description:"Create a range. Lower bound of the range is included, upper bound is excluded.",examples:["1:5","3:-1:-3","range(3, 7)","range(0, 12, 2)",'range("4:10")',"a = [1, 2, 3, 4; 5, 6, 7, 8]","a[1:2, 1:2]"],seealso:["concat","det","diag","identity","inv","ones","size","squeeze","subset","trace","transpose","zeros"]},resize:{name:"resize",category:"Matrix",syntax:["resize(x, size)","resize(x, size, defaultValue)"],description:"Resize a matrix.",examples:["resize([1,2,3,4,5], [3])","resize([1,2,3], [5])","resize([1,2,3], [5], -1)","resize(2, [2, 3])",'resize("hello", [8], "!")'],seealso:["size","subset","squeeze","reshape"]},reshape:{name:"reshape",category:"Matrix",syntax:["reshape(x, sizes)"],description:"Reshape a multi dimensional array to fit the specified dimensions.",examples:["reshape([1, 2, 3, 4, 5, 6], [2, 3])","reshape([[1, 2], [3, 4]], [1, 4])","reshape([[1, 2], [3, 4]], [4])"],seealso:["size","squeeze","resize"]},row:{name:"row",category:"Matrix",syntax:["row(x, index)"],description:"Return a row from a matrix or array.",examples:["A = [[1, 2], [3, 4]]","row(A, 1)","row(A, 2)"],seealso:["column"]},size:{name:"size",category:"Matrix",syntax:["size(x)"],description:"Calculate the size of a matrix.",examples:["size(2.3)",'size("hello world")',"a = [1, 2; 3, 4; 5, 6]","size(a)","size(1:6)"],seealso:["concat","det","diag","identity","inv","ones","range","squeeze","subset","trace","transpose","zeros"]},sort:{name:"sort",category:"Matrix",syntax:["sort(x)","sort(x, compare)"],description:'Sort the items in a matrix. Compare can be a string "asc", "desc", "natural", or a custom sort function.',examples:["sort([5, 10, 1])",'sort(["C", "B", "A", "D"])',"sortByLength(a, b) = size(a)[1] - size(b)[1]",'sort(["Langdon", "Tom", "Sara"], sortByLength)','sort(["10", "1", "2"], "natural")'],seealso:["map","filter","forEach"]},squeeze:{name:"squeeze",category:"Matrix",syntax:["squeeze(x)"],description:"Remove inner and outer singleton dimensions from a matrix.",examples:["a = zeros(3,2,1)","size(squeeze(a))","b = zeros(1,1,3)","size(squeeze(b))"],seealso:["concat","det","diag","identity","inv","ones","range","size","subset","trace","transpose","zeros"]},subset:{name:"subset",category:"Matrix",syntax:["value(index)","value(index) = replacement","subset(value, [index])","subset(value, [index], replacement)"],description:"Get or set a subset of a matrix or string. Indexes are one-based. Both the ranges lower-bound and upper-bound are included.",examples:["d = [1, 2; 3, 4]","e = []","e[1, 1:2] = [5, 6]","e[2, :] = [7, 8]","f = d * e","f[2, 1]","f[:, 1]"],seealso:["concat","det","diag","identity","inv","ones","range","size","squeeze","trace","transpose","zeros"]},trace:{name:"trace",category:"Matrix",syntax:["trace(A)"],description:"Calculate the trace of a matrix: the sum of the elements on the main diagonal of a square matrix.",examples:["A = [1, 2, 3; -1, 2, 3; 2, 0, 3]","trace(A)"],seealso:["concat","det","diag","identity","inv","ones","range","size","squeeze","subset","transpose","zeros"]},transpose:{name:"transpose",category:"Matrix",syntax:["x'","transpose(x)"],description:"Transpose a matrix",examples:["a = [1, 2, 3; 4, 5, 6]","a'","transpose(a)"],seealso:["concat","det","diag","identity","inv","ones","range","size","squeeze","subset","trace","zeros"]},zeros:{name:"zeros",category:"Matrix",syntax:["zeros(m)","zeros(m, n)","zeros(m, n, p, ...)","zeros([m])","zeros([m, n])","zeros([m, n, p, ...])"],description:"Create a matrix containing zeros.",examples:["zeros(3)","zeros(3, 5)","a = [1, 2, 3; 4, 5, 6]","zeros(size(a))"],seealso:["concat","det","diag","identity","inv","ones","range","size","squeeze","subset","trace","transpose"]},combinations:{name:"combinations",category:"Probability",syntax:["combinations(n, k)"],description:"Compute the number of combinations of n items taken k at a time",examples:["combinations(7, 5)"],seealso:["combinationsWithRep","permutations","factorial"]},combinationsWithRep:{name:"combinationsWithRep",category:"Probability",syntax:["combinationsWithRep(n, k)"],description:"Compute the number of combinations of n items taken k at a time with replacements.",examples:["combinationsWithRep(7, 5)"],seealso:["combinations","permutations","factorial"]},factorial:{name:"factorial",category:"Probability",syntax:["n!","factorial(n)"],description:"Compute the factorial of a value",examples:["5!","5 * 4 * 3 * 2 * 1","3!"],seealso:["combinations","combinationsWithRep","permutations","gamma"]},gamma:{name:"gamma",category:"Probability",syntax:["gamma(n)"],description:"Compute the gamma function. For small values, the Lanczos approximation is used, and for large values the extended Stirling approximation.",examples:["gamma(4)","3!","gamma(1/2)","sqrt(pi)"],seealso:["factorial"]},kldivergence:{name:"kldivergence",category:"Probability",syntax:["kldivergence(x, y)"],description:"Calculate the Kullback-Leibler (KL) divergence between two distributions.",examples:["kldivergence([0.7,0.5,0.4], [0.2,0.9,0.5])"],seealso:[]},multinomial:{name:"multinomial",category:"Probability",syntax:["multinomial(A)"],description:"Multinomial Coefficients compute the number of ways of picking a1, a2, ..., ai unordered outcomes from `n` possibilities. multinomial takes one array of integers as an argument. The following condition must be enforced: every ai > 0.",examples:["multinomial([1, 2, 1])"],seealso:["combinations","factorial"]},permutations:{name:"permutations",category:"Probability",syntax:["permutations(n)","permutations(n, k)"],description:"Compute the number of permutations of n items taken k at a time",examples:["permutations(5)","permutations(5, 3)"],seealso:["combinations","combinationsWithRep","factorial"]},pickRandom:{name:"pickRandom",category:"Probability",syntax:["pickRandom(array)","pickRandom(array, number)","pickRandom(array, weights)","pickRandom(array, number, weights)","pickRandom(array, weights, number)"],description:"Pick a random entry from a given array.",examples:["pickRandom(0:10)","pickRandom([1, 3, 1, 6])","pickRandom([1, 3, 1, 6], 2)","pickRandom([1, 3, 1, 6], [2, 3, 2, 1])","pickRandom([1, 3, 1, 6], 2, [2, 3, 2, 1])","pickRandom([1, 3, 1, 6], [2, 3, 2, 1], 2)"],seealso:["random","randomInt"]},random:{name:"random",category:"Probability",syntax:["random()","random(max)","random(min, max)","random(size)","random(size, max)","random(size, min, max)"],description:"Return a random number.",examples:["random()","random(10, 20)","random([2, 3])"],seealso:["pickRandom","randomInt"]},randomInt:{name:"randomInt",category:"Probability",syntax:["randomInt(max)","randomInt(min, max)","randomInt(size)","randomInt(size, max)","randomInt(size, min, max)"],description:"Return a random integer number",examples:["randomInt(10, 20)","randomInt([2, 3], 10)"],seealso:["pickRandom","random"]},compare:{name:"compare",category:"Relational",syntax:["compare(x, y)"],description:"Compare two values. Returns 1 when x > y, -1 when x < y, and 0 when x == y.",examples:["compare(2, 3)","compare(3, 2)","compare(2, 2)","compare(5cm, 40mm)","compare(2, [1, 2, 3])"],seealso:["equal","unequal","smaller","smallerEq","largerEq","compareNatural","compareText"]},compareNatural:{name:"compareNatural",category:"Relational",syntax:["compareNatural(x, y)"],description:"Compare two values of any type in a deterministic, natural way. Returns 1 when x > y, -1 when x < y, and 0 when x == y.",examples:["compareNatural(2, 3)","compareNatural(3, 2)","compareNatural(2, 2)","compareNatural(5cm, 40mm)",'compareNatural("2", "10")',"compareNatural(2 + 3i, 2 + 4i)","compareNatural([1, 2, 4], [1, 2, 3])","compareNatural([1, 5], [1, 2, 3])","compareNatural([1, 2], [1, 2])","compareNatural({a: 2}, {a: 4})"],seealso:["equal","unequal","smaller","smallerEq","largerEq","compare","compareText"]},compareText:{name:"compareText",category:"Relational",syntax:["compareText(x, y)"],description:"Compare two strings lexically. Comparison is case sensitive. Returns 1 when x > y, -1 when x < y, and 0 when x == y.",examples:['compareText("B", "A")','compareText("A", "B")','compareText("A", "A")','compareText("2", "10")','compare("2", "10")',"compare(2, 10)",'compareNatural("2", "10")','compareText("B", ["A", "B", "C"])'],seealso:["compare","compareNatural"]},deepEqual:{name:"deepEqual",category:"Relational",syntax:["deepEqual(x, y)"],description:"Check equality of two matrices element wise. Returns true if the size of both matrices is equal and when and each of the elements are equal.",examples:["deepEqual([1,3,4], [1,3,4])","deepEqual([1,3,4], [1,3])"],seealso:["equal","unequal","smaller","larger","smallerEq","largerEq","compare"]},equal:{name:"equal",category:"Relational",syntax:["x == y","equal(x, y)"],description:"Check equality of two values. Returns true if the values are equal, and false if not.",examples:["2+2 == 3","2+2 == 4","a = 3.2","b = 6-2.8","a == b","50cm == 0.5m"],seealso:["unequal","smaller","larger","smallerEq","largerEq","compare","deepEqual","equalText"]},equalText:{name:"equalText",category:"Relational",syntax:["equalText(x, y)"],description:"Check equality of two strings. Comparison is case sensitive. Returns true if the values are equal, and false if not.",examples:['equalText("Hello", "Hello")','equalText("a", "A")','equal("2e3", "2000")','equalText("2e3", "2000")','equalText("B", ["A", "B", "C"])'],seealso:["compare","compareNatural","compareText","equal"]},larger:{name:"larger",category:"Relational",syntax:["x > y","larger(x, y)"],description:"Check if value x is larger than y. Returns true if x is larger than y, and false if not.",examples:["2 > 3","5 > 2*2","a = 3.3","b = 6-2.8","(a > b)","(b < a)","5 cm > 2 inch"],seealso:["equal","unequal","smaller","smallerEq","largerEq","compare"]},largerEq:{name:"largerEq",category:"Relational",syntax:["x >= y","largerEq(x, y)"],description:"Check if value x is larger or equal to y. Returns true if x is larger or equal to y, and false if not.",examples:["2 >= 1+1","2 > 1+1","a = 3.2","b = 6-2.8","(a >= b)"],seealso:["equal","unequal","smallerEq","smaller","compare"]},smaller:{name:"smaller",category:"Relational",syntax:["x < y","smaller(x, y)"],description:"Check if value x is smaller than value y. Returns true if x is smaller than y, and false if not.",examples:["2 < 3","5 < 2*2","a = 3.3","b = 6-2.8","(a < b)","5 cm < 2 inch"],seealso:["equal","unequal","larger","smallerEq","largerEq","compare"]},smallerEq:{name:"smallerEq",category:"Relational",syntax:["x <= y","smallerEq(x, y)"],description:"Check if value x is smaller or equal to value y. Returns true if x is smaller than y, and false if not.",examples:["2 <= 1+1","2 < 1+1","a = 3.2","b = 6-2.8","(a <= b)"],seealso:["equal","unequal","larger","smaller","largerEq","compare"]},unequal:{name:"unequal",category:"Relational",syntax:["x != y","unequal(x, y)"],description:"Check unequality of two values. Returns true if the values are unequal, and false if they are equal.",examples:["2+2 != 3","2+2 != 4","a = 3.2","b = 6-2.8","a != b","50cm != 0.5m","5 cm != 2 inch"],seealso:["equal","smaller","larger","smallerEq","largerEq","compare","deepEqual"]},setCartesian:{name:"setCartesian",category:"Set",syntax:["setCartesian(set1, set2)"],description:"Create the cartesian product of two (multi)sets. Multi-dimension arrays will be converted to single-dimension arrays before the operation.",examples:["setCartesian([1, 2], [3, 4])"],seealso:["setUnion","setIntersect","setDifference","setPowerset"]},setDifference:{name:"setDifference",category:"Set",syntax:["setDifference(set1, set2)"],description:"Create the difference of two (multi)sets: every element of set1, that is not the element of set2. Multi-dimension arrays will be converted to single-dimension arrays before the operation.",examples:["setDifference([1, 2, 3, 4], [3, 4, 5, 6])","setDifference([[1, 2], [3, 4]], [[3, 4], [5, 6]])"],seealso:["setUnion","setIntersect","setSymDifference"]},setDistinct:{name:"setDistinct",category:"Set",syntax:["setDistinct(set)"],description:"Collect the distinct elements of a multiset. A multi-dimension array will be converted to a single-dimension array before the operation.",examples:["setDistinct([1, 1, 1, 2, 2, 3])"],seealso:["setMultiplicity"]},setIntersect:{name:"setIntersect",category:"Set",syntax:["setIntersect(set1, set2)"],description:"Create the intersection of two (multi)sets. Multi-dimension arrays will be converted to single-dimension arrays before the operation.",examples:["setIntersect([1, 2, 3, 4], [3, 4, 5, 6])","setIntersect([[1, 2], [3, 4]], [[3, 4], [5, 6]])"],seealso:["setUnion","setDifference"]},setIsSubset:{name:"setIsSubset",category:"Set",syntax:["setIsSubset(set1, set2)"],description:"Check whether a (multi)set is a subset of another (multi)set: every element of set1 is the element of set2. Multi-dimension arrays will be converted to single-dimension arrays before the operation.",examples:["setIsSubset([1, 2], [3, 4, 5, 6])","setIsSubset([3, 4], [3, 4, 5, 6])"],seealso:["setUnion","setIntersect","setDifference"]},setMultiplicity:{name:"setMultiplicity",category:"Set",syntax:["setMultiplicity(element, set)"],description:"Count the multiplicity of an element in a multiset. A multi-dimension array will be converted to a single-dimension array before the operation.",examples:["setMultiplicity(1, [1, 2, 2, 4])","setMultiplicity(2, [1, 2, 2, 4])"],seealso:["setDistinct","setSize"]},setPowerset:{name:"setPowerset",category:"Set",syntax:["setPowerset(set)"],description:"Create the powerset of a (multi)set: the powerset contains very possible subsets of a (multi)set. A multi-dimension array will be converted to a single-dimension array before the operation.",examples:["setPowerset([1, 2, 3])"],seealso:["setCartesian"]},setSize:{name:"setSize",category:"Set",syntax:["setSize(set)","setSize(set, unique)"],description:'Count the number of elements of a (multi)set. When the second parameter "unique" is true, count only the unique values. A multi-dimension array will be converted to a single-dimension array before the operation.',examples:["setSize([1, 2, 2, 4])","setSize([1, 2, 2, 4], true)"],seealso:["setUnion","setIntersect","setDifference"]},setSymDifference:{name:"setSymDifference",category:"Set",syntax:["setSymDifference(set1, set2)"],description:"Create the symmetric difference of two (multi)sets. Multi-dimension arrays will be converted to single-dimension arrays before the operation.",examples:["setSymDifference([1, 2, 3, 4], [3, 4, 5, 6])","setSymDifference([[1, 2], [3, 4]], [[3, 4], [5, 6]])"],seealso:["setUnion","setIntersect","setDifference"]},setUnion:{name:"setUnion",category:"Set",syntax:["setUnion(set1, set2)"],description:"Create the union of two (multi)sets. Multi-dimension arrays will be converted to single-dimension arrays before the operation.",examples:["setUnion([1, 2, 3, 4], [3, 4, 5, 6])","setUnion([[1, 2], [3, 4]], [[3, 4], [5, 6]])"],seealso:["setIntersect","setDifference"]},erf:{name:"erf",category:"Special",syntax:["erf(x)"],description:"Compute the erf function of a value using a rational Chebyshev approximations for different intervals of x",examples:["erf(0.2)","erf(-0.5)","erf(4)"],seealso:[]},mad:{name:"mad",category:"Statistics",syntax:["mad(a, b, c, ...)","mad(A)"],description:"Compute the median absolute deviation of a matrix or a list with values. The median absolute deviation is defined as the median of the absolute deviations from the median.",examples:["mad(10, 20, 30)","mad([1, 2, 3])"],seealso:["mean","median","std","abs"]},max:{name:"max",category:"Statistics",syntax:["max(a, b, c, ...)","max(A)","max(A, dim)"],description:"Compute the maximum value of a list of values.",examples:["max(2, 3, 4, 1)","max([2, 3, 4, 1])","max([2, 5; 4, 3])","max([2, 5; 4, 3], 1)","max([2, 5; 4, 3], 2)","max(2.7, 7.1, -4.5, 2.0, 4.1)","min(2.7, 7.1, -4.5, 2.0, 4.1)"],seealso:["mean","median","min","prod","std","sum","variance"]},mean:{name:"mean",category:"Statistics",syntax:["mean(a, b, c, ...)","mean(A)","mean(A, dim)"],description:"Compute the arithmetic mean of a list of values.",examples:["mean(2, 3, 4, 1)","mean([2, 3, 4, 1])","mean([2, 5; 4, 3])","mean([2, 5; 4, 3], 1)","mean([2, 5; 4, 3], 2)","mean([1.0, 2.7, 3.2, 4.0])"],seealso:["max","median","min","prod","std","sum","variance"]},median:{name:"median",category:"Statistics",syntax:["median(a, b, c, ...)","median(A)"],description:"Compute the median of all values. The values are sorted and the middle value is returned. In case of an even number of values, the average of the two middle values is returned.",examples:["median(5, 2, 7)","median([3, -1, 5, 7])"],seealso:["max","mean","min","prod","std","sum","variance","quantileSeq"]},min:{name:"min",category:"Statistics",syntax:["min(a, b, c, ...)","min(A)","min(A, dim)"],description:"Compute the minimum value of a list of values.",examples:["min(2, 3, 4, 1)","min([2, 3, 4, 1])","min([2, 5; 4, 3])","min([2, 5; 4, 3], 1)","min([2, 5; 4, 3], 2)","min(2.7, 7.1, -4.5, 2.0, 4.1)","max(2.7, 7.1, -4.5, 2.0, 4.1)"],seealso:["max","mean","median","prod","std","sum","variance"]},mode:{name:"mode",category:"Statistics",syntax:["mode(a, b, c, ...)","mode(A)","mode(A, a, b, B, c, ...)"],description:"Computes the mode of all values as an array. In case mode being more than one, multiple values are returned in an array.",examples:["mode(2, 1, 4, 3, 1)","mode([1, 2.7, 3.2, 4, 2.7])","mode(1, 4, 6, 1, 6)"],seealso:["max","mean","min","median","prod","std","sum","variance"]},prod:{name:"prod",category:"Statistics",syntax:["prod(a, b, c, ...)","prod(A)"],description:"Compute the product of all values.",examples:["prod(2, 3, 4)","prod([2, 3, 4])","prod([2, 5; 4, 3])"],seealso:["max","mean","min","median","min","std","sum","variance"]},quantileSeq:{name:"quantileSeq",category:"Statistics",syntax:["quantileSeq(A, prob[, sorted])","quantileSeq(A, [prob1, prob2, ...][, sorted])","quantileSeq(A, N[, sorted])"],description:"Compute the prob order quantile of a matrix or a list with values. The sequence is sorted and the middle value is returned. Supported types of sequence values are: Number, BigNumber, Unit Supported types of probablity are: Number, BigNumber. \n\nIn case of a (multi dimensional) array or matrix, the prob order quantile of all elements will be calculated.",examples:["quantileSeq([3, -1, 5, 7], 0.5)","quantileSeq([3, -1, 5, 7], [1/3, 2/3])","quantileSeq([3, -1, 5, 7], 2)","quantileSeq([-1, 3, 5, 7], 0.5, true)"],seealso:["mean","median","min","max","prod","std","sum","variance"]},std:{name:"std",category:"Statistics",syntax:["std(a, b, c, ...)","std(A)","std(A, normalization)"],description:'Compute the standard deviation of all values, defined as std(A) = sqrt(variance(A)). Optional parameter normalization can be "unbiased" (default), "uncorrected", or "biased".',examples:["std(2, 4, 6)","std([2, 4, 6, 8])",'std([2, 4, 6, 8], "uncorrected")','std([2, 4, 6, 8], "biased")',"std([1, 2, 3; 4, 5, 6])"],seealso:["max","mean","min","median","prod","sum","variance"]},sum:{name:"sum",category:"Statistics",syntax:["sum(a, b, c, ...)","sum(A)"],description:"Compute the sum of all values.",examples:["sum(2, 3, 4, 1)","sum([2, 3, 4, 1])","sum([2, 5; 4, 3])"],seealso:["max","mean","median","min","prod","std","sum","variance"]},variance:{name:"variance",category:"Statistics",syntax:["variance(a, b, c, ...)","variance(A)","variance(A, normalization)"],description:'Compute the variance of all values. Optional parameter normalization can be "unbiased" (default), "uncorrected", or "biased".',examples:["variance(2, 4, 6)","variance([2, 4, 6, 8])",'variance([2, 4, 6, 8], "uncorrected")','variance([2, 4, 6, 8], "biased")',"variance([1, 2, 3; 4, 5, 6])"],seealso:["max","mean","min","median","min","prod","std","sum"]},acos:{name:"acos",category:"Trigonometry",syntax:["acos(x)"],description:"Compute the inverse cosine of a value in radians.",examples:["acos(0.5)","acos(cos(2.3))"],seealso:["cos","atan","asin"]},acosh:{name:"acosh",category:"Trigonometry",syntax:["acosh(x)"],description:"Calculate the hyperbolic arccos of a value, defined as `acosh(x) = ln(sqrt(x^2 - 1) + x)`.",examples:["acosh(1.5)"],seealso:["cosh","asinh","atanh"]},acot:{name:"acot",category:"Trigonometry",syntax:["acot(x)"],description:"Calculate the inverse cotangent of a value.",examples:["acot(0.5)","acot(cot(0.5))","acot(2)"],seealso:["cot","atan"]},acoth:{name:"acoth",category:"Trigonometry",syntax:["acoth(x)"],description:"Calculate the hyperbolic arccotangent of a value, defined as `acoth(x) = (ln((x+1)/x) + ln(x/(x-1))) / 2`.",examples:["acoth(2)","acoth(0.5)"],seealso:["acsch","asech"]},acsc:{name:"acsc",category:"Trigonometry",syntax:["acsc(x)"],description:"Calculate the inverse cotangent of a value.",examples:["acsc(2)","acsc(csc(0.5))","acsc(0.5)"],seealso:["csc","asin","asec"]},acsch:{name:"acsch",category:"Trigonometry",syntax:["acsch(x)"],description:"Calculate the hyperbolic arccosecant of a value, defined as `acsch(x) = ln(1/x + sqrt(1/x^2 + 1))`.",examples:["acsch(0.5)"],seealso:["asech","acoth"]},asec:{name:"asec",category:"Trigonometry",syntax:["asec(x)"],description:"Calculate the inverse secant of a value.",examples:["asec(0.5)","asec(sec(0.5))","asec(2)"],seealso:["acos","acot","acsc"]},asech:{name:"asech",category:"Trigonometry",syntax:["asech(x)"],description:"Calculate the inverse secant of a value.",examples:["asech(0.5)"],seealso:["acsch","acoth"]},asin:{name:"asin",category:"Trigonometry",syntax:["asin(x)"],description:"Compute the inverse sine of a value in radians.",examples:["asin(0.5)","asin(sin(0.5))"],seealso:["sin","acos","atan"]},asinh:{name:"asinh",category:"Trigonometry",syntax:["asinh(x)"],description:"Calculate the hyperbolic arcsine of a value, defined as `asinh(x) = ln(x + sqrt(x^2 + 1))`.",examples:["asinh(0.5)"],seealso:["acosh","atanh"]},atan:{name:"atan",category:"Trigonometry",syntax:["atan(x)"],description:"Compute the inverse tangent of a value in radians.",examples:["atan(0.5)","atan(tan(0.5))"],seealso:["tan","acos","asin"]},atanh:{name:"atanh",category:"Trigonometry",syntax:["atanh(x)"],description:"Calculate the hyperbolic arctangent of a value, defined as `atanh(x) = ln((1 + x)/(1 - x)) / 2`.",examples:["atanh(0.5)"],seealso:["acosh","asinh"]},atan2:{name:"atan2",category:"Trigonometry",syntax:["atan2(y, x)"],description:"Computes the principal value of the arc tangent of y/x in radians.",examples:["atan2(2, 2) / pi","angle = 60 deg in rad","x = cos(angle)","y = sin(angle)","atan2(y, x)"],seealso:["sin","cos","tan"]},cos:{name:"cos",category:"Trigonometry",syntax:["cos(x)"],description:"Compute the cosine of x in radians.",examples:["cos(2)","cos(pi / 4) ^ 2","cos(180 deg)","cos(60 deg)","sin(0.2)^2 + cos(0.2)^2"],seealso:["acos","sin","tan"]},cosh:{name:"cosh",category:"Trigonometry",syntax:["cosh(x)"],description:"Compute the hyperbolic cosine of x in radians.",examples:["cosh(0.5)"],seealso:["sinh","tanh","coth"]},cot:{name:"cot",category:"Trigonometry",syntax:["cot(x)"],description:"Compute the cotangent of x in radians. Defined as 1/tan(x)",examples:["cot(2)","1 / tan(2)"],seealso:["sec","csc","tan"]},coth:{name:"coth",category:"Trigonometry",syntax:["coth(x)"],description:"Compute the hyperbolic cotangent of x in radians.",examples:["coth(2)","1 / tanh(2)"],seealso:["sech","csch","tanh"]},csc:{name:"csc",category:"Trigonometry",syntax:["csc(x)"],description:"Compute the cosecant of x in radians. Defined as 1/sin(x)",examples:["csc(2)","1 / sin(2)"],seealso:["sec","cot","sin"]},csch:{name:"csch",category:"Trigonometry",syntax:["csch(x)"],description:"Compute the hyperbolic cosecant of x in radians. Defined as 1/sinh(x)",examples:["csch(2)","1 / sinh(2)"],seealso:["sech","coth","sinh"]},sec:{name:"sec",category:"Trigonometry",syntax:["sec(x)"],description:"Compute the secant of x in radians. Defined as 1/cos(x)",examples:["sec(2)","1 / cos(2)"],seealso:["cot","csc","cos"]},sech:{name:"sech",category:"Trigonometry",syntax:["sech(x)"],description:"Compute the hyperbolic secant of x in radians. Defined as 1/cosh(x)",examples:["sech(2)","1 / cosh(2)"],seealso:["coth","csch","cosh"]},sin:{name:"sin",category:"Trigonometry",syntax:["sin(x)"],description:"Compute the sine of x in radians.",examples:["sin(2)","sin(pi / 4) ^ 2","sin(90 deg)","sin(30 deg)","sin(0.2)^2 + cos(0.2)^2"],seealso:["asin","cos","tan"]},sinh:{name:"sinh",category:"Trigonometry",syntax:["sinh(x)"],description:"Compute the hyperbolic sine of x in radians.",examples:["sinh(0.5)"],seealso:["cosh","tanh"]},tan:{name:"tan",category:"Trigonometry",syntax:["tan(x)"],description:"Compute the tangent of x in radians.",examples:["tan(0.5)","sin(0.5) / cos(0.5)","tan(pi / 4)","tan(45 deg)"],seealso:["atan","sin","cos"]},tanh:{name:"tanh",category:"Trigonometry",syntax:["tanh(x)"],description:"Compute the hyperbolic tangent of x in radians.",examples:["tanh(0.5)","sinh(0.5) / cosh(0.5)"],seealso:["sinh","cosh"]},to:{name:"to",category:"Units",syntax:["x to unit","to(x, unit)"],description:"Change the unit of a value.",examples:["5 inch to cm","3.2kg to g","16 bytes in bits"],seealso:[]},clone:{name:"clone",category:"Utils",syntax:["clone(x)"],description:"Clone a variable. Creates a copy of primitive variables,and a deep copy of matrices",examples:["clone(3.5)","clone(2 - 4i)","clone(45 deg)","clone([1, 2; 3, 4])",'clone("hello world")'],seealso:[]},format:{name:"format",category:"Utils",syntax:["format(value)","format(value, precision)"],description:"Format a value of any type as string.",examples:["format(2.3)","format(3 - 4i)","format([])","format(pi, 3)"],seealso:["print"]},bin:{name:"bin",category:"Utils",syntax:["bin(value)"],description:"Format a number as binary",examples:["bin(2)"],seealso:["oct","hex"]},oct:{name:"oct",category:"Utils",syntax:["oct(value)"],description:"Format a number as octal",examples:["oct(56)"],seealso:["bin","hex"]},hex:{name:"hex",category:"Utils",syntax:["hex(value)"],description:"Format a number as hexadecimal",examples:["hex(240)"],seealso:["bin","oct"]},isNaN:{name:"isNaN",category:"Utils",syntax:["isNaN(x)"],description:"Test whether a value is NaN (not a number)",examples:["isNaN(2)","isNaN(0 / 0)","isNaN(NaN)","isNaN(Infinity)"],seealso:["isNegative","isNumeric","isPositive","isZero"]},isInteger:{name:"isInteger",category:"Utils",syntax:["isInteger(x)"],description:"Test whether a value is an integer number.",examples:["isInteger(2)","isInteger(3.5)","isInteger([3, 0.5, -2])"],seealso:["isNegative","isNumeric","isPositive","isZero"]},isNegative:{name:"isNegative",category:"Utils",syntax:["isNegative(x)"],description:"Test whether a value is negative: smaller than zero.",examples:["isNegative(2)","isNegative(0)","isNegative(-4)","isNegative([3, 0.5, -2])"],seealso:["isInteger","isNumeric","isPositive","isZero"]},isNumeric:{name:"isNumeric",category:"Utils",syntax:["isNumeric(x)"],description:"Test whether a value is a numeric value. Returns true when the input is a number, BigNumber, Fraction, or boolean.",examples:["isNumeric(2)",'isNumeric("2")','hasNumericValue("2")',"isNumeric(0)","isNumeric(bignumber(500))","isNumeric(fraction(0.125))","isNumeric(2 + 3i)",'isNumeric([2.3, "foo", false])'],seealso:["isInteger","isZero","isNegative","isPositive","isNaN","hasNumericValue"]},hasNumericValue:{name:"hasNumericValue",category:"Utils",syntax:["hasNumericValue(x)"],description:"Test whether a value is an numeric value. In case of a string, true is returned if the string contains a numeric value.",examples:["hasNumericValue(2)",'hasNumericValue("2")','isNumeric("2")',"hasNumericValue(0)","hasNumericValue(bignumber(500))","hasNumericValue(fraction(0.125))","hasNumericValue(2 + 3i)",'hasNumericValue([2.3, "foo", false])'],seealso:["isInteger","isZero","isNegative","isPositive","isNaN","isNumeric"]},isPositive:{name:"isPositive",category:"Utils",syntax:["isPositive(x)"],description:"Test whether a value is positive: larger than zero.",examples:["isPositive(2)","isPositive(0)","isPositive(-4)","isPositive([3, 0.5, -2])"],seealso:["isInteger","isNumeric","isNegative","isZero"]},isPrime:{name:"isPrime",category:"Utils",syntax:["isPrime(x)"],description:"Test whether a value is prime: has no divisors other than itself and one.",examples:["isPrime(3)","isPrime(-2)","isPrime([2, 17, 100])"],seealso:["isInteger","isNumeric","isNegative","isZero"]},isZero:{name:"isZero",category:"Utils",syntax:["isZero(x)"],description:"Test whether a value is zero.",examples:["isZero(2)","isZero(0)","isZero(-4)","isZero([3, 0, -2, 0])"],seealso:["isInteger","isNumeric","isNegative","isPositive"]},typeOf:{name:"typeOf",category:"Utils",syntax:["typeOf(x)"],description:"Get the type of a variable.",examples:["typeOf(3.5)","typeOf(2 - 4i)","typeOf(45 deg)",'typeOf("hello world")'],seealso:["getMatrixDataType"]},numeric:{name:"numeric",category:"Utils",syntax:["numeric(x)"],description:"Convert a numeric input to a specific numeric type: number, BigNumber, or Fraction.",examples:['numeric("4")','numeric("4", "number")','numeric("4", "BigNumber")','numeric("4", "Fraction)','numeric(4, "Fraction")','numeric(fraction(2, 5), "number)'],seealso:["number","fraction","bignumber","string","format"]}},Ll=["typed","mathWithTransform","Help"],Hl=Object(s.a)("help",Ll,function(e){var t=e.typed,a=e.mathWithTransform,o=e.Help;return t("help",{any:function(e){var t,r=e;if("string"!=typeof e)for(t in a)if(Object(ae.f)(a,t)&&e===a[t]){r=t;break}var n=Ri(Fl,r);if(n)return new o(n);var i="function"==typeof r?r.name:r;throw new Error('No documentation found on "'+i+'"')}})}),$l=["typed","Chain"],Gl=Object(s.a)("chain",$l,function(e){var t=e.typed,r=e.Chain;return t("chain",{"":function(){return new r},any:function(e){return new r(e)}})}),Zl=["typed","matrix","subtract","multiply","unaryMinus","lup"],Vl=Object(s.a)("det",Zl,function(e){var t=e.typed,i=e.matrix,f=e.subtract,l=e.multiply,p=e.unaryMinus,m=e.lup;return t("det",{any:function(e){return Object(ae.a)(e)},"Array | Matrix":function(e){var t=Object(ie.v)(e)?e.size():Array.isArray(e)?(e=i(e)).size():[];switch(t.length){case 0:return Object(ae.a)(e);case 1:if(1===t[0])return Object(ae.a)(e.valueOf()[0]);throw new RangeError("Matrix must be square (size: "+Object(V.d)(t)+")");case 2:var r=t[0],n=t[1];if(r===n)return function(e,t){{if(1===t)return Object(ae.a)(e[0][0]);if(2===t)return f(l(e[0][0],e[1][1]),l(e[1][0],e[0][1]));for(var r=m(e),n=r.U[0][0],i=1;il&&(l=M(u[m][f]),p=m),m++;if(0===l)throw Error("Cannot calculate inverse, determinant is zero");(m=p)!==f&&(o=u[f],u[f]=u[m],u[m]=o,o=c[f],c[f]=c[m],c[m]=o);var h=u[f],d=c[f];for(m=0;m=l(n);){var s=o[0][0],u=o[0][1];t=function(e,t,r){var n=g(t,e);return l(n)<=d.epsilon?w(-1).acos().div(4):x(.5,p(N(2,r,m(n))))}(e[s][s],e[u][u],e[s][u]),e=function(e,t,r,n){for(var i=e.length,a=w(b(t)),o=w(v(t)),s=x(a,a),u=x(o,o),c=E(i,w(0)),f=E(i,w(0)),l=N(w(2),a,o,e[r][n]),p=y(g(x(s,e[r][r]),l),x(u,e[n][n])),m=O(x(u,e[r][r]),l,x(s,e[n][n])),h=0;h=Math.abs(n);){var l=f[0][0],p=f[0][1];o=e[l][l],s=e[p][p],u=e[l][p],c=void 0,c=s-o,e=function(e,t,r,n){for(var i=e.length,a=Math.cos(t),o=Math.sin(t),s=a*a,u=o*o,c=E(i,0),f=E(i,0),l=s*e[r][r]-2*a*o*e[r][n]+u*e[n][n],p=u*e[r][r]+2*a*o*e[r][n]+s*e[n][n],m=0;m>1;return jp(e,r)*jp(1+r,t)}function Ep(e,t){if(!Object(E.i)(e)||e<0)throw new TypeError("Positive integer value expected in function combinations");if(!Object(E.i)(t)||t<0)throw new TypeError("Positive integer value expected in function combinations");if(e");if(2!==u.length)throw SyntaxError("Could not parse rule: "+a);a={l:u[0],r:u[1]};case"object":o={l:_(c(a.l)),r:_(c(a.r))},a.context&&(o.evaluate=a.context),a.evaluate&&(o.evaluate=c(a.evaluate)),M(o.l)&&(n=A(o.l),i=new b("_p"+q++),o.expanded={},o.expanded.l=n([o.l.clone(),i]),j(o.expanded.l),E(o.expanded.l),o.expanded.r=n([o.r,i]));break;case"function":o=a;break;default:throw TypeError("Unsupported type of rule: "+s)}t.push(o)}return t}(t);for(var i=w(e,r),a={},o=(i=_(i)).toString({parenthesis:"all"});!a[o];){a[o]=!0;for(var s=q=0;swindow.__errors.push(e.message));window.addEventListener("unhandledrejection",e=>window.__errors.push(String(e.reason))); document.addEventListener("DOMContentLoaded",()=>{document.getElementById("inline-try-again")?.addEventListener("click",()=>onClickRunButton());}); FILE: source.html Source & license

SineRider · Curvebound

A silent static edition of Hack Club’s game about love and graphing. Write curves to guide the sled through goals. Desktop keyboard recommended.

Silent static edition; local assets; images optimized to WebP; tracking and external promotions removed. Original graphing, physics, puzzles, story and local progress retained.

Original source (625 stars observed) · AGPL-3.0 license · Complete modified application source

Dependencies: Lodash MIT, Math.js Apache-2.0, LZ-String MIT. Native plain-expression input replaces MathQuill/jQuery. Dependency license texts. Original notices preserved in files.

Return to game