diff --git a/package-lock.json b/package-lock.json index 9a51e7d09..eb9a97b57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "firebase": "^9", "firebase-functions": "^4.4.1", "graphemer": "^1.4.0", + "matter-js": "^0.19.0", "pitchy": "^4.0.7", "recoverable-random": "^1.0.3", "uuid": "^9" @@ -24,6 +25,7 @@ "@sveltejs/kit": "^1", "@testing-library/jest-dom": "^5", "@testing-library/svelte": "^4", + "@types/matter-js": "^0.19.0", "@types/uuid": "^9", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", @@ -3398,6 +3400,12 @@ "@types/mdurl": "*" } }, + "node_modules/@types/matter-js": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@types/matter-js/-/matter-js-0.19.0.tgz", + "integrity": "sha512-SqgYUc8j68n/R2p8rVgpxTVC6gwCby+93dd5eWqjQdpL3l3JUqxzhbEJH/X0NXv+pmoAeWheH1kPvFIgC904Bw==", + "dev": true + }, "node_modules/@types/mdurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", @@ -9485,6 +9493,11 @@ "node": ">= 12" } }, + "node_modules/matter-js": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/matter-js/-/matter-js-0.19.0.tgz", + "integrity": "sha512-v2huwvQGOHTGOkMqtHd2hercCG3f6QAObTisPPHg8TZqq2lz7eIY/5i/5YUV8Ibf3mEioFEmwibcPUF2/fnKKQ==" + }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", diff --git a/package.json b/package.json index ac1a5fd62..0c2ed5c58 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@sveltejs/kit": "^1", "@testing-library/jest-dom": "^5", "@testing-library/svelte": "^4", + "@types/matter-js": "^0.19.0", "@types/uuid": "^9", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", @@ -83,6 +84,7 @@ "firebase": "^9", "firebase-functions": "^4.4.1", "graphemer": "^1.4.0", + "matter-js": "^0.19.0", "pitchy": "^4.0.7", "recoverable-random": "^1.0.3", "uuid": "^9" diff --git a/src/components/concepts/ExampleUI.svelte b/src/components/concepts/ExampleUI.svelte index f8991c58d..dfad818b9 100644 --- a/src/components/concepts/ExampleUI.svelte +++ b/src/components/concepts/ExampleUI.svelte @@ -10,7 +10,7 @@ import type Value from '../../values/Value'; import CodeView from './CodeView.svelte'; import { DB, locales } from '../../db/Database'; - import Stage, { toStage } from '../../output/Stage'; + import Stage, { NameGenerator, toStage } from '../../output/Stage'; import OutputView from '../output/OutputView.svelte'; export let example: Example; @@ -44,7 +44,9 @@ function update() { if (evaluator) { value = evaluator.getLatestSourceValue(project.main); - stage = value ? toStage(project, value) : undefined; + stage = value + ? toStage(project, value, new NameGenerator()) + : undefined; } } diff --git a/src/components/output/GroupView.svelte b/src/components/output/GroupView.svelte index 8e08a3970..dd7778c75 100644 --- a/src/components/output/GroupView.svelte +++ b/src/components/output/GroupView.svelte @@ -15,16 +15,18 @@ import Group from '@output/Group'; import Evaluate from '@nodes/Evaluate'; import { getSelectedOutput } from '../project/Contexts'; - import type { Shape } from '../../output/Shapes'; import type Stage from '../../output/Stage'; import { locale, locales } from '../../db/Database'; + import type { Form } from '../../output/Form'; + import Shape from '../../output/Shape'; + import ShapeView from './ShapeView.svelte'; export let group: Group | Stage; export let place: Place; export let focus: Place; export let viewport: { width: number; height: number } | undefined = undefined; - export let clip: Shape | undefined = undefined; + export let clip: Form | undefined = undefined; export let interactive: boolean; export let parentAscent: number; export let context: RenderContext; @@ -108,6 +110,17 @@ {editing} {still} /> + {:else if child instanceof Shape} + {:else} diff --git a/src/components/output/OutputView.svelte b/src/components/output/OutputView.svelte index edc7ab19f..e75ffb671 100644 --- a/src/components/output/OutputView.svelte +++ b/src/components/output/OutputView.svelte @@ -95,11 +95,17 @@ let startGesturePlace: Place | undefined = undefined; $: exception = value instanceof ExceptionValue ? value : undefined; + + /** Everyt ime the value changes, try to parse a Stage from it. */ $: stageValue = value === undefined ? undefined : toStage(project, value); + + /** Keep track of whether the creator is typing, so we can blur output until the next change. */ $: typing = !mini && $evaluation?.playing === true && $keyboardEditIdle === IdleKind.Typing; + + /** Keep a background color up to date. */ $: background = value instanceof ExceptionValue ? 'var(--wordplay-error)' @@ -581,7 +587,7 @@ // Scale down the mouse delta and offset by the drag starting point. stage.setFocus( renderedDeltaX / scale + drag.startPlace.x, - -renderedDeltaY / scale + drag.startPlace.y, + renderedDeltaY / scale + drag.startPlace.y, drag.startPlace.z ); event.stopPropagation(); diff --git a/src/components/output/ShapeView.svelte b/src/components/output/ShapeView.svelte new file mode 100644 index 000000000..c7dceb72a --- /dev/null +++ b/src/components/output/ShapeView.svelte @@ -0,0 +1,88 @@ + + +{#if visible} +
+{/if} + + diff --git a/src/components/output/StageView.svelte b/src/components/output/StageView.svelte index 050b03a29..d63871833 100644 --- a/src/components/output/StageView.svelte +++ b/src/components/output/StageView.svelte @@ -34,7 +34,7 @@ getEvaluation, } from '../project/Contexts'; import type Evaluator from '@runtime/Evaluator'; - import type TypeOutput from '../../output/TypeOutput'; + import type Output from '../../output/Output'; import { animationFactor, locale, locales } from '../../db/Database'; import { describeEnteredOutput, @@ -91,10 +91,10 @@ } let exiting: OutputInfoSet = new Map(); - let entered: Map = new Map(); - let present: Map = new Map(); + let entered: Map = new Map(); + let present: Map = new Map(); let moved: Moved = new Map(); - let previouslyPresent: Map | undefined = undefined; + let previouslyPresent: Map | undefined = undefined; const announcer = getAnnounce(); @@ -127,7 +127,9 @@ /** A stage to manage entries, exits, animations. A new one each time the for each project. */ let scene: Scene; $: { + // Previous scene? Stop it. if (scene !== undefined) scene.stop(); + // Make a new one. scene = new Scene( evaluator, // When output exits, remove it from the map and triggering a render. @@ -154,7 +156,7 @@ $: editableStore.set(editable); setContext('project', project); - /** Whenever the verse, languages, fonts, or rendered focus changes, update the rendered scene accordingly. */ + /** Whenever the stage, languages, fonts, or rendered focus changes, update the rendered scene accordingly. */ $: { const results = scene.update( stage, @@ -256,7 +258,7 @@ fitFocus = createPlace( evaluator, -(contentBounds.left + contentBounds.width / 2), - -contentBounds.top + contentBounds.height / 2, + contentBounds.top - contentBounds.height / 2, z ); // If we're currently fitting to content, just make the adjusted focus the same in case the setting is inactive. @@ -458,4 +460,10 @@ background-color: var(--grid-color); opacity: 0.4; } + + .rectangle-barrier { + position: absolute; + background: var(--wordplay-inactive-color); + border-radius: calc(var(--wordplay-border-radius) * 2); + } diff --git a/src/components/palette/ContentEditor.svelte b/src/components/palette/ContentEditor.svelte index f23689cf2..c876b598d 100644 --- a/src/components/palette/ContentEditor.svelte +++ b/src/components/palette/ContentEditor.svelte @@ -32,6 +32,10 @@ value.is( project.shares.output.Group, project.getNodeContext(value) + ) || + value.is( + project.shares.output.Shape, + project.getNodeContext(value) )) ); @@ -92,7 +96,7 @@ project, list, list?.values.length ?? 1 - 1, - true + 'phrase' ) : undefined} >+{project.shares.output.Phrase.getNames()[0]}+{project.shares.output.Group.getNames()[0]} +
{:else} diff --git a/src/components/palette/MotionEditor.svelte b/src/components/palette/MotionEditor.svelte new file mode 100644 index 000000000..bb131de77 --- /dev/null +++ b/src/components/palette/MotionEditor.svelte @@ -0,0 +1,48 @@ + + +
+ {project.shares.input.Motion.names.getPreferredNameString([], true)} + {#if place instanceof Evaluate} +
+
+ {/if} + {#if velocity instanceof Evaluate} +
+
+ {/if} +
+ + diff --git a/src/components/palette/PaletteProperty.svelte b/src/components/palette/PaletteProperty.svelte index 24a19aa30..963d72933 100644 --- a/src/components/palette/PaletteProperty.svelte +++ b/src/components/palette/PaletteProperty.svelte @@ -24,6 +24,8 @@ import { DB, locale, locales } from '../../db/Database'; import { tick } from 'svelte'; import { DOCUMENTATION_SYMBOL, EDIT_SYMBOL } from '../../parser/Symbols'; + import MotionEditor from './MotionEditor.svelte'; + import PlacementEditor from './PlacementEditor.svelte'; export let project: Project; export let property: OutputProperty; @@ -136,11 +138,25 @@ {:else if property.type === 'content'} {:else if property.type === 'place'} - + {@const place = values.getEvaluationOf( + project, + project.shares.output.Place + )} + {@const motion = values.getEvaluationOf( + project, + project.shares.input.Motion + )} + {@const placement = values.getEvaluationOf( + project, + project.shares.input.Placement + )} + {#if place} + + {:else if motion} + + {:else if placement} + + {/if} {/if} diff --git a/src/components/palette/PlaceEditor.svelte b/src/components/palette/PlaceEditor.svelte index c960482dc..b13b94844 100644 --- a/src/components/palette/PlaceEditor.svelte +++ b/src/components/palette/PlaceEditor.svelte @@ -1,7 +1,7 @@
- {#each [getFirstName($locale.output.Place.x.names), getFirstName($locale.output.Place.y.names), getFirstName($locale.output.Place.z.names)] as dimension, index} + {project.shares.output.Place.names.getSymbolicName()}{#each [getFirstName($locale.output.Place.x.names), getFirstName($locale.output.Place.y.names), getFirstName($locale.output.Place.z.names)] as dimension, index} {@const given = place?.getMappingFor( dimension, project.getNodeContext(place) @@ -83,12 +85,67 @@
{/each} +{#if convertable} + + +{/if} diff --git a/src/components/palette/VelocityEditor.svelte b/src/components/palette/VelocityEditor.svelte new file mode 100644 index 000000000..1bf6af1e7 --- /dev/null +++ b/src/components/palette/VelocityEditor.svelte @@ -0,0 +1,110 @@ + + +
+ {project.shares.output.Velocity.names.getSymbolicName()}{#each project.shares.output.Velocity.inputs.map( (input) => input.getPreferredName(project.locales) ) as dimension, index} + {@const mapping = velocity?.getMappingFor( + dimension, + project.getNodeContext(velocity) + )} + {@const given = mapping?.given} + + {@const value = + given instanceof Expression ? getNumber(given) : undefined} +
+ {#if value !== undefined} + handleChange(dimension, index, value)} + bind:view={views[index]} + /> + {#if index < 2}m/s{:else}°/s{/if} + {:else} + {$locales.map( + (locale) => locale.ui.palette.labels.computed + )} + {/if} +
+ {/each} +
+ + diff --git a/src/components/palette/editOutput.ts b/src/components/palette/editOutput.ts index 1fea17703..a225d33fb 100644 --- a/src/components/palette/editOutput.ts +++ b/src/components/palette/editOutput.ts @@ -133,30 +133,38 @@ export function addContent( project: Project, list: ListLiteral, index: number, - phrase: boolean + kind: 'phrase' | 'group' | 'shape' ) { const GroupType = project.shares.output.Group; const RowType = project.shares.output.Row; - const newPhrase = createPlaceholderPhrase(database, project); reviseContent(database, project, list, [ ...list.values.slice(0, index + 1), - phrase - ? newPhrase + kind === 'phrase' + ? // Create a placeholder phrase + createPlaceholderPhrase(database, project) : // Create a group with a Row layout and a single phrase + kind === 'group' + ? Evaluate.make(GroupType.getReference(project.locales), [ + Evaluate.make(RowType.getReference(project.locales), []), + ListLiteral.make([ + createPlaceholderPhrase(database, project), + ]), + ]) + : // Create a placeholder shape Evaluate.make( - Reference.make( - GroupType.names.getPreferredNameString(project.locales) - ), + project.shares.output.Shape.getReference(project.locales), [ Evaluate.make( - Reference.make( - RowType.names.getPreferredNameString( - project.locales - ) + project.shares.output.Rectangle.getReference( + project.locales ), - [] + [ + NumberLiteral.make(-5, Unit.create(['m'])), + NumberLiteral.make(0, Unit.create(['m'])), + NumberLiteral.make(5, Unit.create(['m'])), + NumberLiteral.make(-1, Unit.create(['m'])), + ] ), - ListLiteral.make([newPhrase]), ] ), ...list.values.slice(index + 1), diff --git a/src/components/project/TileView.svelte b/src/components/project/TileView.svelte index 5925f22fe..ae7b8ce3c 100644 --- a/src/components/project/TileView.svelte +++ b/src/components/project/TileView.svelte @@ -226,7 +226,7 @@ height="13px" viewBox="0 0 14 14" width="14px" - style="stroke: var(--wordplay-foreground)" + style="stroke: var(--wordplay-foreground); pointer-events: none;" ><desc /><defs /><g fill-rule="evenodd" stroke-width="1" diff --git a/src/db/AnimationFactorSetting.ts b/src/db/AnimationFactorSetting.ts index d3405c2da..862078762 100644 --- a/src/db/AnimationFactorSetting.ts +++ b/src/db/AnimationFactorSetting.ts @@ -5,5 +5,5 @@ export const AnimationFactorSetting = new Setting<number>( false, 1, (value) => (typeof value === 'number' && value >= 1 ? value : undefined), - (current, value) => current == value + (current, value) => current === value ); diff --git a/src/edit/GroupProperties.ts b/src/edit/GroupProperties.ts index 5c77e883b..e73cc08f5 100644 --- a/src/edit/GroupProperties.ts +++ b/src/edit/GroupProperties.ts @@ -6,6 +6,7 @@ import ListLiteral from '../nodes/ListLiteral'; import Reference from '../nodes/Reference'; import OutputProperty from './OutputProperty'; import OutputPropertyOptions from './OutputPropertyOptions'; +import { getTypeOutputProperties } from './OutputProperties'; export default function getGroupProperties( project: Project, @@ -52,5 +53,6 @@ export default function getGroupProperties( (expr) => expr instanceof ListLiteral, () => ListLiteral.make([]) ), + ...getTypeOutputProperties(project, locale), ]; } diff --git a/src/edit/OutputExpression.ts b/src/edit/OutputExpression.ts index f58cd2798..0686789ff 100644 --- a/src/edit/OutputExpression.ts +++ b/src/edit/OutputExpression.ts @@ -11,7 +11,7 @@ import type OutputProperty from './OutputProperty'; import getStageProperties from './StageProperties'; import getGroupProperties from './GroupProperties'; import getPhraseProperties from './PhraseProperties'; -import getTypeOutputProperties from './TypeOutputProperties'; +import getShapeProperties from './getShapeProperties'; /** * Represents the value of a property. If given is true, it means its set explicitly. @@ -56,6 +56,7 @@ export default class OutputExpression { (fun === this.project.shares.output.Stage || fun === this.project.shares.output.Group || fun === this.project.shares.output.Phrase || + fun === this.project.shares.output.Shape || fun === this.project.shares.output.Pose || fun === this.project.shares.output.Sequence) ? fun @@ -76,20 +77,18 @@ export default class OutputExpression { // We handle pose types differently, so we return an empty list here. return type === this.project.shares.output.Pose ? [] - : // For all other types, we create a list of editable properties. + : // For all other types, we create a list of editable properties if we know how. [ // Add output type specific properties first ...(type === this.project.shares.output.Phrase - ? getPhraseProperties(locale) + ? getPhraseProperties(this.project, locale) : type === this.project.shares.output.Group ? getGroupProperties(this.project, locale) : type === this.project.shares.output.Stage ? getStageProperties(this.project, locale) + : type === this.project.shares.output.Shape + ? getShapeProperties(this.project, locale) : []), - ...getTypeOutputProperties( - this.project, - this.project.basis.locales[0] - ), ]; } diff --git a/src/edit/TypeOutputProperties.ts b/src/edit/OutputProperties.ts similarity index 88% rename from src/edit/TypeOutputProperties.ts rename to src/edit/OutputProperties.ts index b395f050c..eb480edda 100644 --- a/src/edit/TypeOutputProperties.ts +++ b/src/edit/OutputProperties.ts @@ -5,7 +5,7 @@ import NumberLiteral from '@nodes/NumberLiteral'; import TextLiteral from '@nodes/TextLiteral'; import Unit from '@nodes/Unit'; import { createPoseLiteral } from '@output/Pose'; -import { DefaultStyle } from '@output/TypeOutput'; +import { DefaultStyle } from '@output/Output'; import OutputProperty from './OutputProperty'; import OutputPropertyText from './OutputPropertyText'; import OutputPropertyOptions from './OutputPropertyOptions'; @@ -67,15 +67,15 @@ export function getStyleProperty(locale: Locale): OutputProperty { ); } -// All output has these properties. -export default function getTypeOutputProperties( +// All type output has these properties. +export function getTypeOutputProperties( project: Project, locale: Locale ): OutputProperty[] { return [ new OutputProperty( locale.output.Phrase.size, - new OutputPropertyRange(0.25, 32, 0.25, 'm'), + new OutputPropertyRange(0.25, 32, 0.25, 'm', 2), false, true, (expr) => expr instanceof NumberLiteral, @@ -104,7 +104,9 @@ export default function getTypeOutputProperties( false, (expr, context) => expr instanceof Evaluate && - expr.is(project.shares.output.Place, context), + (expr.is(project.shares.output.Place, context) || + expr.is(project.shares.input.Motion, context) || + expr.is(project.shares.input.Placement, context)), (locale) => Evaluate.make( Reference.make( @@ -120,6 +122,17 @@ export default function getTypeOutputProperties( ] ) ), + ...getOutputProperties(project, locale), + ]; +} + +/** All output has these properties */ +// All type output has these properties, in this order. +export function getOutputProperties( + project: Project, + locale: Locale +): OutputProperty[] { + return [ new OutputProperty( locale.output.Phrase.name, new OutputPropertyText(() => true), diff --git a/src/edit/OutputProperty.ts b/src/edit/OutputProperty.ts index ad8e2fe5c..8ca29f8a9 100644 --- a/src/edit/OutputProperty.ts +++ b/src/edit/OutputProperty.ts @@ -15,23 +15,15 @@ type OutputPropertyType = | 'pose' | 'poses' | 'content' - | 'place'; + | 'place' + | 'form'; /** Represents an editable property on the output expression, with some optional information about valid property values */ class OutputProperty { /** The internal name of the property, corresponding to bind input names of TypeOutput. */ readonly name: NameAndDoc; /** The type of input, roughly corresponding to widgets. */ - readonly type: - | OutputPropertyRange - | OutputPropertyOptions - | OutputPropertyText - | 'color' - | 'bool' - | 'pose' - | 'poses' - | 'content' - | 'place'; + readonly type: OutputPropertyType; /** True if the property is required */ readonly required: boolean; /** True if the property uses the nearest parent's property if unset */ diff --git a/src/edit/OutputPropertyValueSet.ts b/src/edit/OutputPropertyValueSet.ts index 0a85f4a33..d5fc60dbe 100644 --- a/src/edit/OutputPropertyValueSet.ts +++ b/src/edit/OutputPropertyValueSet.ts @@ -14,6 +14,8 @@ import type Bind from '../nodes/Bind'; import type { Database } from '../db/Database'; import MarkupValue from '@values/MarkupValue'; import type Locale from '../locale/Locale'; +import type StructureDefinition from '../nodes/StructureDefinition'; +import type StreamDefinition from '../nodes/StreamDefinition'; /** * Represents one or more equivalent inputs to an output expression. @@ -124,10 +126,13 @@ export default class OutputPropertyValueSet { return expr instanceof ListLiteral ? expr : undefined; } - getPlace(project: Project) { + getEvaluationOf( + project: Project, + definition: StructureDefinition | StreamDefinition + ) { const expr = this.getExpression(); return expr instanceof Evaluate && - expr.is(project.shares.output.Place, project.getNodeContext(expr)) + expr.is(definition, project.getNodeContext(expr)) ? expr : undefined; } diff --git a/src/edit/PhraseProperties.ts b/src/edit/PhraseProperties.ts index 47d95c405..6db6ed83e 100644 --- a/src/edit/PhraseProperties.ts +++ b/src/edit/PhraseProperties.ts @@ -8,8 +8,13 @@ import OutputPropertyRange from './OutputPropertyRange'; import NumberLiteral from '../nodes/NumberLiteral'; import Unit from '../nodes/Unit'; import OutputPropertyOptions from './OutputPropertyOptions'; +import { getTypeOutputProperties } from './OutputProperties'; +import type Project from '../models/Project'; -export default function getPhraseProperties(locale: Locale): OutputProperty[] { +export default function getPhraseProperties( + project: Project, + locale: Locale +): OutputProperty[] { return [ new OutputProperty( locale.output.Phrase.text, @@ -42,5 +47,6 @@ export default function getPhraseProperties(locale: Locale): OutputProperty[] { (expr) => expr instanceof TextLiteral, () => TextLiteral.make('|') ), + ...getTypeOutputProperties(project, locale), ]; } diff --git a/src/edit/SequenceProperties.ts b/src/edit/SequenceProperties.ts index 871d91770..3d4c4f208 100644 --- a/src/edit/SequenceProperties.ts +++ b/src/edit/SequenceProperties.ts @@ -6,7 +6,7 @@ import { createPoseLiteral } from '../output/Pose'; import type Locale from '../locale/Locale'; import OutputProperty from './OutputProperty'; import OutputPropertyRange from './OutputPropertyRange'; -import { getDurationProperty, getStyleProperty } from './TypeOutputProperties'; +import { getDurationProperty, getStyleProperty } from './OutputProperties'; import type Project from '../models/Project'; export default function getSequenceProperties( diff --git a/src/edit/ShapeProperties.ts b/src/edit/ShapeProperties.ts new file mode 100644 index 000000000..9a568411d --- /dev/null +++ b/src/edit/ShapeProperties.ts @@ -0,0 +1,22 @@ +import type Locale from '../locale/Locale'; +import type Project from '../models/Project'; +import ListLiteral from '../nodes/ListLiteral'; +import { getOutputProperties } from './OutputProperties'; +import OutputProperty from './OutputProperty'; + +export default function getShapeProperties( + project: Project, + locale: Locale +): OutputProperty[] { + return [ + new OutputProperty( + locale.output.Stage.content, + 'content', + true, + false, + (expr) => expr instanceof ListLiteral, + () => ListLiteral.make([]) + ), + ...getOutputProperties(project, locale), + ]; +} diff --git a/src/edit/StageProperties.ts b/src/edit/StageProperties.ts index c59c8f4da..d0d225603 100644 --- a/src/edit/StageProperties.ts +++ b/src/edit/StageProperties.ts @@ -1,7 +1,11 @@ import type Locale from '../locale/Locale'; import type Project from '../models/Project'; import ListLiteral from '../nodes/ListLiteral'; +import NumberLiteral from '../nodes/NumberLiteral'; +import Unit from '../nodes/Unit'; import OutputProperty from './OutputProperty'; +import OutputPropertyRange from './OutputPropertyRange'; +import { getTypeOutputProperties } from './OutputProperties'; export default function getStageProperties( project: Project, @@ -16,5 +20,14 @@ export default function getStageProperties( (expr) => expr instanceof ListLiteral, () => ListLiteral.make([]) ), + new OutputProperty( + locale.output.Stage.gravity, + new OutputPropertyRange(0, 20, 0.2, 'm/s^2', 1), + true, + false, + (expr) => expr instanceof NumberLiteral, + () => NumberLiteral.make('9.8', Unit.create(['m'], ['s', 's'])) + ), + ...getTypeOutputProperties(project, locale), ]; } diff --git a/src/edit/getShapeProperties.ts b/src/edit/getShapeProperties.ts new file mode 100644 index 000000000..0000a9d1e --- /dev/null +++ b/src/edit/getShapeProperties.ts @@ -0,0 +1,22 @@ +import type Locale from '../locale/Locale'; +import type Project from '../models/Project'; +import ListLiteral from '../nodes/ListLiteral'; +import { getOutputProperties } from './OutputProperties'; +import OutputProperty from './OutputProperty'; + +export default function getShapeProperties( + project: Project, + locale: Locale +): OutputProperty[] { + return [ + new OutputProperty( + locale.output.Shape.form, + 'form', + true, + false, + (expr) => expr instanceof ListLiteral, + () => ListLiteral.make([]) + ), + ...getOutputProperties(project, locale), + ]; +} diff --git a/src/examples/Cannon.wp b/src/examples/Cannon.wp deleted file mode 100644 index fc51550dc..000000000 --- a/src/examples/Cannon.wp +++ /dev/null @@ -1,23 +0,0 @@ -Cannon -=== start/en -•Question(id•"" vx•#m/s vy•#m/s va•#°/s) - -magnitude: 2 -pressed: ∆ Key() -count•#: 0 … ∆ Key() … count + 1 -questions•[Question]: [] … ∆ Key() … questions.add(Question(count→"" Random(magnitude magnitude · 2) · 1m/s Random(magnitude · 2 magnitude · 4) · 1m/s Random(0 360) · 1°/s)) - -Stage( - [ Phrase('👨‍👨‍👧‍👦' face: "Noto Emoji" place: 📍(0m 0m 0m)) ].append( - questions.translate(ƒ(q•Question index•#) ( - initialize: pressed & (index = questions.length()) - Motion( - Phrase('Q' name: q.id resting:Pose(opacity: initialize ? 0% 100%)) - startplace: Place(0m 0m 0m) - startvx: q.vx - startvy: q.vy - startvangle: q.va - ) - )) - ) -) \ No newline at end of file diff --git a/src/examples/Catch.wp b/src/examples/Catch.wp index a2a026b52..79c2db4ef 100644 --- a/src/examples/Catch.wp +++ b/src/examples/Catch.wp @@ -17,6 +17,6 @@ Stage([ name: 'mouse' ) ] - place: Place(0m 0m -2m) + place: Place(0m 0m -5m) frame: Rectangle(-3m 3m 3m -3m) ) \ No newline at end of file diff --git a/src/examples/Hira.wp b/src/examples/Hira.wp new file mode 100644 index 000000000..6718c2d8c --- /dev/null +++ b/src/examples/Hira.wp @@ -0,0 +1,25 @@ +Hiragana +=== start +count: 32 + +gravity•#m/s^2: 15m/s^2 … ∆ Button(⊤) … -gravity + +hiragana: "ぁあぃいぅうぇえぉおかがきぎくぐけげこごさざしじすずせぜそぞただちぢっつづてでとどなにぬねのはばぱひびぴふぶぷへべぺほぼぽまみむめもゃやゅゆょよらりるれろゎわゐゑをんゔゕゖ"→[''] + +letters•[Output]: count → [].translate( + ƒ(number index) + Phrase( + hiragana[number] + place: Motion(Place((index - (count ÷ 2))· 0.2m 10m) Velocity(angle: Random(-10 10)· 1°/s)) + matter: Matter(bounciness: 0.6) + ) + ) + + +Stage( + letters.append([ + Shape(Rectangle(-40m 0m 40m -2m)) + ]) + gravity: gravity + place: Place(0m 5m -20m) +) \ No newline at end of file diff --git a/src/examples/Layers.wp b/src/examples/Layers.wp new file mode 100644 index 000000000..b1279767a --- /dev/null +++ b/src/examples/Layers.wp @@ -0,0 +1,24 @@ +Layers +=== start +count: 10→[] + +ƒ balls(z•#m)•[Output] count.translate( + ƒ () + Phrase( + 'o' + place: Motion( + Place(Random(-10 10)· 1m Random(0 10)· 1m z) + Velocity(Random(-10 10)· 1m/s Random(-5 5)· 1m/s) + ) + matter: Matter(bounciness: 1.2 friction:0) + ) +) + +Stage( + balls(0m).append(balls(-10m).append(balls(10m))).append([ + Shape(Rectangle(-20m 0m 20m -1m 0m)) + Shape(Rectangle(-20m 0m 20m -1m 10m) rotation: 25°) + Shape(Rectangle(-20m 0m 20m -1m -10m) rotation: -10°) + ]) + place: Place(0m 10m -30m) +) \ No newline at end of file diff --git a/src/examples/Move.wp b/src/examples/Move.wp deleted file mode 100644 index a5f93477f..000000000 --- a/src/examples/Move.wp +++ /dev/null @@ -1,44 +0,0 @@ -Move -=== start -•Cat(position•#m direction•#) - -key: Key() -speed: 1m - -cat•Cat: Cat(5m 1) … ∆ key … - key = "ArrowLeft" ? Cat(cat.position - speed 1) - key = "ArrowRight" ? Cat(cat.position + speed -1) - cat - -ƒ close(ball•Place|ø x•#m) - ball•ø ? ⊥ - (ball.y < 1m) & - ( - ((ball.x - x).positive() < 1m) | - ((ball.x - (x + 2m)).positive() < 1m) - ) - -ball: Motion( - Phrase("o") - startplace: Place(0m 0m) - vx: ◆ ? 0m/s - close(ball.place cat.position) ? (Random(1 5) · -1m/s · cat.direction) - ø - vy: ◆ ? 0m/s - close(ball.place cat.position) ? (Random(2 10) · 1m/s) - ø - vangle: ◆ ? 0°/s - close(ball.place cat.position) ? (cat.direction · 120°/s) - ø -) - -Stage([ - Phrase( - "🐈" - face: "Noto Color Emoji" - name: "kitty" - size: 2m - place: Place(cat.position 0m) - resting: Pose(flipx: cat.direction < 0) - moving: Sequence(sway(5°) 0.25s "zippy") -) ball] place: Place(0m 0m -5m)) \ No newline at end of file diff --git a/src/examples/Physics.wp b/src/examples/Physics.wp deleted file mode 100644 index 1c13b90c5..000000000 --- a/src/examples/Physics.wp +++ /dev/null @@ -1,21 +0,0 @@ -Physics -=== start -count: 32 - -gravity•#m/s^2: 15m/s^2 … ∆ Button(⊤) … -gravity - -hiragana: "ぁあぃいぅうぇえぉおかがきぎくぐけげこごさざしじすずせぜそぞただちぢっつづてでとどなにぬねのはばぱひびぴふぶぷへべぺほぼぽまみむめもゃやゅゆょよらりるれろゎわゐゑをんゔゕゖ"→[''] - - -Stage(count → [].translate( - ƒ(number) Motion( - Phrase( - hiragana[number] - ) - startplace: Place(y: 10m) - startvx: Random(-5 5) · 1m/s - startvangle: Random(0 360) · 1°/s - bounciness: Random() - gravity: gravity - ) -)) \ No newline at end of file diff --git a/src/examples/Pounce.wp b/src/examples/Pounce.wp new file mode 100644 index 000000000..2826f2bfe --- /dev/null +++ b/src/examples/Pounce.wp @@ -0,0 +1,45 @@ +Pounce +=== start +•CatPosition(position•#m direction•#) + +key: Key() +speed: 1m + +kittyPlace: CatPosition(5m 1) … ∆ key … + ((key = "ArrowLeft") & (kittyPlace.position > -10m)) ? CatPosition(kittyPlace.position - speed 1) + ((key = "ArrowRight") & (kittyPlace.position < 8m)) ? CatPosition(kittyPlace.position + speed -1) + kittyPlace + +kitty: Phrase( + "🐈" + face: "Noto Color Emoji" + name: "kitty" + size: 2m + place: Place(kittyPlace.position 0m) + resting: Pose(flipx: kittyPlace.direction < 0) + moving: Sequence(sway(5°) 0.25s "zippy") + matter: Matter(1000kg) +) + +pounce: Collision('ball' 'kitty') + +ballPlace: Motion( + Place(0m 9m) + nextvelocity: + pounce•ø ? ø + Velocity((Random(5 15) · -1m/s · kittyPlace.direction) (Random(27 30) · 1m/s) (-kittyPlace.direction · Random(0 15) · 1°/s)) +) + +ball: Phrase("🧶" name: 'ball' place: ballPlace matter: Matter(1kg 0.5 0 1)) + +Stage( + [ + Shape(Rectangle(-10m 0m 10m -1m)) + Shape(Rectangle(-10m -1m -9m 10m)) + Shape(Rectangle(10m -1m 9m 10m)) + Shape(Rectangle(-10m 10m 10m 11m)) + ball + kitty + ] + place: Place(y: 5m z: -10m) +) \ No newline at end of file diff --git a/src/examples/Questions.wp b/src/examples/Questions.wp new file mode 100644 index 000000000..868c6968f --- /dev/null +++ b/src/examples/Questions.wp @@ -0,0 +1,22 @@ +Questions +=== start/en +•Question(id•"" vx•#m/s vy•#m/s va•#°/s) + +pressed: ∆ Key() +count•#: 0 … pressed … count + 1 +questions•[Question]: [] … pressed … questions.add(Question(count→"" Random(30 50) · 1m/s Random(30 50) · 1m/s Random(0 30) · 1°/s)) + +Stage( + [ Phrase('👨‍👨‍👧‍👦' place: Place(0m 0m) face: "Noto Emoji") Shape(Rectangle(-5m 0m 25m -1m))].append(questions.translate( + ƒ(q•Question index•#) ( + initialize: pressed & (index = questions.length()) + Phrase( + 'Q' + name: q.id + place: Motion(velocity: Velocity(q.vx q.vy q.va)) + matter: Matter() + resting: Pose(opacity: initialize ? 0% 100%) + ) + ) + )) +) \ No newline at end of file diff --git a/src/examples/examples.ts b/src/examples/examples.ts index 033d34aaf..2ec3585f0 100644 --- a/src/examples/examples.ts +++ b/src/examples/examples.ts @@ -1,17 +1,18 @@ import type { SerializedProject } from '../models/Project'; import WhatWord from './WhatWord.wp?raw'; import Kitties from './RainingKitties.wp?raw'; -import Move from './Move.wp?raw'; -import Physics from './Physics.wp?raw'; +import Pounce from './Pounce.wp?raw'; +import Hira from './Hira.wp?raw'; import Video from './Video.wp?raw'; import RainingLetters from './RainingLetters.wp?raw'; import Poem from './Poem.wp?raw'; -import Cannon from './Cannon.wp?raw'; +import Questions from './Questions.wp?raw'; import Maze from './Maze.wp?raw'; import Adventure from './Adventure.wp?raw'; import Letters from './Letters.wp?raw'; import Catch from './Catch.wp?raw'; import Headlines from './Headlines.wp?raw'; +import Layers from './Layers.wp?raw'; import { parseNames } from '../parser/parseBind'; import { toTokens } from '../parser/toTokens'; @@ -68,14 +69,15 @@ export const examples = wpToSerializedProjects([ Adventure, WhatWord, Kitties, - Move, - Physics, + Pounce, + Hira, Video, RainingLetters, Poem, - Cannon, + Questions, Letters, Maze, Catch, Headlines, + Layers, ]); diff --git a/src/input/Collision.ts b/src/input/Collision.ts new file mode 100644 index 000000000..7e6e686a5 --- /dev/null +++ b/src/input/Collision.ts @@ -0,0 +1,175 @@ +import type Evaluator from '@runtime/Evaluator'; +import StreamValue from '@values/StreamValue'; +import StreamDefinition from '@nodes/StreamDefinition'; +import { getDocLocales } from '@locale/getDocLocales'; +import { getNameLocales } from '@locale/getNameLocales'; +import Bind from '@nodes/Bind'; +import UnionType from '@nodes/UnionType'; +import NoneType from '@nodes/NoneType'; +import StreamType from '@nodes/StreamType'; +import createStreamEvaluator from './createStreamEvaluator'; +import type Locale from '../locale/Locale'; +import NoneLiteral from '../nodes/NoneLiteral'; +import type StructureValue from '../values/StructureValue'; +import TextType from '../nodes/TextType'; +import type StructureDefinition from '../nodes/StructureDefinition'; +import type Type from '../nodes/Type'; +import NoneValue from '../values/NoneValue'; +import TextValue from '../values/TextValue'; +import { createReboundStructure } from '../output/Rebound'; +import { PX_PER_METER } from '../output/outputToCSS'; + +export type ReboundEvent = + | { + subject: string; + object: string; + direction: { + x: number; + y: number; + }; + /** True if the collision is start, false if it just ended. */ + starting: boolean; + } + | undefined; + +export default class Collision extends StreamValue< + StructureValue | NoneValue, + ReboundEvent +> { + subject: string | undefined; + object: string | undefined; + + constructor( + evaluator: Evaluator, + subject: string | undefined, + object: string | undefined + ) { + super( + evaluator, + evaluator.project.shares.input.Button, + new NoneValue(evaluator.getMain()), + undefined + ); + + this.subject = subject; + this.object = object; + } + + update(subject: string | undefined, object: string | undefined) { + this.subject = subject; + this.object = object; + } + + react(rebound: ReboundEvent) { + // Does this rebound match the filter? + if (rebound === undefined) { + this.add(new NoneValue(this.creator), undefined); + } else if ( + // Everything + (this.subject === undefined && this.object === undefined) || + // A particular subject and anything + (this.subject !== undefined && + this.object === undefined && + (this.subject === rebound.subject || + this.subject === rebound.object)) || + // A particular object and any subject + (this.subject === undefined && + this.object !== undefined && + (this.object === rebound.subject || + this.object === rebound.object)) || + // A specific object and subject + (this.subject !== undefined && + this.object !== undefined && + ((this.subject === rebound.subject && + this.object === rebound.object) || + (this.subject === rebound.object && + this.object === rebound.subject))) + ) { + // Swap if the match is reversed + const swap = this.subject === rebound.object; + const subject = swap ? rebound.object : rebound.subject; + const object = swap ? rebound.subject : rebound.object; + const direction = { + x: ((swap ? -1 : 1) * rebound.direction.x) / PX_PER_METER, + y: ((swap ? -1 : 1) * rebound.direction.y) / PX_PER_METER, + }; + + // If starting, add a collision + if (rebound.starting) + this.add( + createReboundStructure( + this.evaluator, + this.evaluator.project.shares.input.Collision, + subject, + object, + direction + ), + rebound + ); + // If ending, immediately add none, after processing the collision. + else this.add(new NoneValue(this.creator), undefined); + } + } + + start() { + return; + } + stop() { + return; + } + + getType(): Type { + return StreamType.make( + this.evaluator.project.shares.output.Rebound.getTypeReference() + ); + } +} + +export function createCollisionDefinition( + locales: Locale[], + ReboundType: StructureDefinition +) { + const NameBind = Bind.make( + getDocLocales(locales, (locale) => locale.input.Collision.subject.doc), + getNameLocales( + locales, + (locale) => locale.input.Collision.subject.names + ), + UnionType.make(TextType.make(), NoneType.make()), + // Default to none + NoneLiteral.make() + ); + + const OtherBind = Bind.make( + getDocLocales(locales, (locale) => locale.input.Collision.object.doc), + getNameLocales( + locales, + (locale) => locale.input.Collision.object.names + ), + UnionType.make(TextType.make(), NoneType.make()), + // Default to none + NoneLiteral.make() + ); + + return StreamDefinition.make( + getDocLocales(locales, (locale) => locale.input.Collision.doc), + getNameLocales(locales, (locale) => locale.input.Collision.names), + [NameBind, OtherBind], + createStreamEvaluator( + ReboundType.getTypeReference(), + Collision, + (evaluation) => + new Collision( + evaluation.getEvaluator(), + evaluation.get(NameBind.names, TextValue)?.text, + evaluation.get(OtherBind.names, TextValue)?.text + ), + (stream, evaluation) => + stream.update( + evaluation.get(NameBind.names, TextValue)?.text, + evaluation.get(OtherBind.names, TextValue)?.text + ) + ), + UnionType.make(ReboundType.getTypeReference(), NoneType.make()) + ); +} diff --git a/src/input/Motion.ts b/src/input/Motion.ts index d3be611c4..88dc005cc 100644 --- a/src/input/Motion.ts +++ b/src/input/Motion.ts @@ -1,106 +1,38 @@ import type Evaluator from '@runtime/Evaluator'; -import { createPlaceStructure } from '@output/Place'; +import Place, { createPlaceStructure, toPlace } from '@output/Place'; import TemporalStreamValue from '@values/TemporalStreamValue'; import Bind from '@nodes/Bind'; -import NumberLiteral from '@nodes/NumberLiteral'; -import NumberType from '@nodes/NumberType'; import StreamDefinition from '@nodes/StreamDefinition'; -import StreamType from '@nodes/StreamType'; -import StructureType from '@nodes/StructureType'; -import Unit from '@nodes/Unit'; -import NumberValue from '@values/NumberValue'; import StructureValue from '@values/StructureValue'; import { getDocLocales } from '@locale/getDocLocales'; import { getNameLocales } from '@locale/getNameLocales'; import createStreamEvaluator from './createStreamEvaluator'; -import type TypeOutput from '../output/TypeOutput'; -import { toTypeOutput } from '../output/toTypeOutput'; -import Evaluate from '../nodes/Evaluate'; -import ValueException from '../values/ValueException'; import UnionType from '../nodes/UnionType'; import NoneLiteral from '../nodes/NoneLiteral'; import type Locale from '../locale/Locale'; import type StructureDefinition from '../nodes/StructureDefinition'; import type Value from '../values/Value'; -import Decimal from 'decimal.js'; -import { getFirstName } from '../locale/Locale'; -import NameType from '../nodes/NameType'; import type Context from '../nodes/Context'; import type Type from '../nodes/Type'; - -const Bounciness = 0.5; -const Gravity = 9.8; +import Matter from 'matter-js'; +import type { OutputBody } from '../output/Physics'; +import type Velocity from '../output/Velocity'; +import { toVelocity } from '../output/Velocity'; +import NoneValue from '../values/NoneValue'; export default class Motion extends TemporalStreamValue<Value, number> { - output: TypeOutput; - - /** The current location and angle of the object. */ - x: Decimal; - y: Decimal; - z: Decimal; - angle: Decimal; - - /** The current velocity the object. */ - vx: Decimal; - vy: Decimal; - vz: Decimal; - va: Decimal; - - /* Collision and gravity properties.. */ - mass: number; - bounciness: number; - gravity: number; + private initialPlace: Place | undefined; - constructor( - evaluator: Evaluator, - type: TypeOutput, - startplace: Value | undefined, - startvx: number | undefined, - startvy: number | undefined, - startvz: number | undefined, - startvangle: number | undefined, - mass: number | undefined, - bounciness: number | undefined, - gravity: number | undefined - ) { - super(evaluator, evaluator.project.shares.input.Motion, type.value, 0); + private place: Place | undefined; + private velocity: Velocity | undefined; - this.output = type; + constructor(evaluator: Evaluator, place: Value, velocity: Value) { + super(evaluator, evaluator.project.shares.input.Motion, place, 0); - const place = - startplace instanceof StructureValue && - startplace.is(evaluator.project.shares.output.Place) - ? startplace - : undefined; - const startX = place?.getInput(0); - const startY = place?.getInput(1); - const startZ = place?.getInput(2); + this.initialPlace = this.place = toPlace(place); + this.velocity = toVelocity(velocity); - this.x = new Decimal( - (startX instanceof NumberValue ? startX.toNumber() : undefined) ?? - type.place?.x ?? - 0 - ); - this.y = new Decimal( - (startY instanceof NumberValue ? startY.toNumber() : undefined) ?? - type.place?.y ?? - 0 - ); - this.z = new Decimal( - (startZ instanceof NumberValue ? startZ.toNumber() : undefined) ?? - type.place?.z ?? - 0 - ); - this.angle = new Decimal(type.pose.rotation ?? 0); - - this.vx = new Decimal(startvx ?? 0); - this.vy = new Decimal(startvy ?? 0); - this.vz = new Decimal(startvz ?? 0); - this.va = new Decimal(startvangle ?? 0); - - this.mass = mass ?? 1; - this.bounciness = bounciness ?? Bounciness; - this.gravity = gravity ?? Gravity; + this.updateBodies(); } // No setup or teardown, the Evaluator handles the requestAnimationFrame loop. @@ -111,95 +43,100 @@ export default class Motion extends TemporalStreamValue<Value, number> { return; } - update( - output: TypeOutput | undefined, - vx: number | undefined, - vy: number | undefined, - vz: number | undefined, - vangle: number | undefined, - mass: number | undefined, - bounciness: number | undefined, - gravity: number | undefined - ) { - if (output) { - this.output = output; - this.x = new Decimal(output.place?.x ?? this.x); - this.y = new Decimal(output.place?.y ?? this.y); - this.z = new Decimal(output.place?.z ?? this.z); - this.angle = new Decimal(output.pose.rotation ?? this.angle); - } - - this.vx = new Decimal(vx ?? this.vx); - this.vy = new Decimal(vy ?? this.vy); - this.vz = new Decimal(vz ?? this.vz); - this.va = new Decimal(vangle ?? this.va); + update(place: Value | undefined, velocity: Value | undefined) { + this.place = toPlace(place); + this.velocity = toVelocity(velocity); - if (mass !== undefined) this.mass = mass; - if (bounciness !== undefined) this.bounciness = bounciness; - if (gravity !== undefined) this.gravity = gravity; + // Immediately update the bodies. + this.updateBodies(); } - react(delta: number) { - // First, apply gravity to the y velocity proporitional to elapsed time. - this.vy = this.vy.sub(this.gravity * delta); + /** Find all of the bodies that this motion stream influences and update them. */ + updateBodies() { + if (this.evaluator.scene === undefined) return; - // Then, apply velocity to place. - this.x = this.x.plus(this.vx.times(delta)); - this.y = this.y.plus(this.vy.times(delta)); - this.z = this.z.plus(this.vz.times(delta)); - this.angle = this.angle.plus(this.va.times(delta)); + for (const output of this.getOutputs()) { + const body = this.evaluator.scene.physics.getOutputBody( + output.getName() + ); - // If we collide with 0, negate y velocity. - if (this.y.lessThanOrEqualTo(0)) { - this.y = new Decimal(0); - this.vy = this.vy.neg().times(this.bounciness); - this.vx = this.vx.times(this.bounciness); - this.va = this.va.times(this.bounciness); + if (body) this.updateBody(body); } + } - // Get the type so we can clone and modify it. - const output = this.output.value; - if (output instanceof StructureValue) { - const creator = - output.creator instanceof Evaluate - ? output.creator - : this.definition; + updateBody(rect: OutputBody) { + const body = rect.body; + if (this.velocity !== undefined) { + // Are either of the velocities non-zero? Update the body's velocity. + if ( + this.velocity.x !== undefined || + this.velocity.y !== undefined + ) { + const velocity = { + x: + this.velocity.x !== undefined + ? this.velocity.x + : body.velocity.x, + // Flip the axis + y: + this.velocity.y !== undefined + ? -this.velocity.y + : body.velocity.y, + }; + Matter.Body.setVelocity(body, velocity); + } + // Is the rotational velocity defined? Update the body's. + if (this.velocity.angle !== undefined) + Matter.Body.setAngularVelocity( + body, + (this.velocity.angle * Math.PI) / 180 + ); + } - const en = this.evaluator.project.basis.locales[0]; - const PlaceName = - typeof en.output.Phrase.place.names === 'string' - ? en.output.Phrase.place.names - : en.output.Phrase.place.names[0]; + // Did the place of the stream change? Reposition the body. + if (this.place) + Matter.Body.setPosition( + body, + rect.getPosition( + this.place.x, + this.place.y, + rect.width, + rect.height + ) + ); + } - const RotationName = - typeof en.output.Phrase.rotation.names === 'string' - ? en.output.Phrase.rotation.names - : en.output.Phrase.rotation.names[0]; + getOutputs() { + // Find the latest place in the scene + const latest = this.latest(); + if (this.evaluator.scene) + // Ask the scene for the output corresponding to the latest value this stream generated. + return latest + ? this.evaluator.scene.getOutputByPlace(latest) ?? [] + : []; + else return []; + } - // Create a new type output with an updated place. - const revised = output - .withValue( - creator, - PlaceName, + react(delta: number) { + if (this.evaluator.scene === undefined) return; + for (const output of this.getOutputs()) { + const name = output.getName(); + // Ask the scene for the latest x, y, z, and angle from the physics engine. + const placement = this.evaluator.scene.physics + .getOutputBody(name) + ?.getPlace(); + if (placement) { + this.add( createPlaceStructure( this.evaluator, - this.x.toNumber(), - this.y.toNumber(), - this.z.toNumber() - ) - ) - ?.withValue( - creator, - RotationName, - new NumberValue( - this.definition, - this.angle, - Unit.reuse(['°']) - ) + placement.x, + placement.y, + this.place?.z ?? this.initialPlace?.z ?? 0, + placement.angle + ), + delta ); - - // Finally, add the new place to the stream. - if (revised) this.add(revised, delta); + } } } @@ -212,205 +149,80 @@ export default class Motion extends TemporalStreamValue<Value, number> { } getType(context: Context): Type { - return StreamType.make( - NameType.make( - context.project.shares.output.Phrase.names.getNames()[0] - ) - ); + return context.project.shares.output.Place.getTypeReference(); } } -const SpeedUnit = Unit.reuse(['m'], ['s']); -const SpeedType = NumberType.make(SpeedUnit); -const AngleSpeedUnit = Unit.reuse(['°'], ['s']); -const AngleSpeedType = NumberType.make(AngleSpeedUnit); - export function createMotionDefinition( locales: Locale[], - TypeType: StructureDefinition, - PhraseType: StructureDefinition + placeType: StructureDefinition, + velocityType: StructureDefinition ) { - const PlaceName = getFirstName(locales[0].output.Place.names); - - const TypeBind = Bind.make( - getDocLocales(locales, (locale) => locale.input.Motion.type.doc), - getNameLocales(locales, (locale) => locale.input.Motion.type.names), - new StructureType(TypeType) - ); - const StartPlace = Bind.make( - getDocLocales(locales, (locale) => locale.input.Motion.startplace.doc), - getNameLocales( - locales, - (locale) => locale.input.Motion.startplace.names - ), - UnionType.orNone(NameType.make(PlaceName)), + getDocLocales(locales, (locale) => locale.input.Motion.place.doc), + getNameLocales(locales, (locale) => locale.input.Motion.place.names), + UnionType.orNone(placeType.getTypeReference()), NoneLiteral.make() ); - const StartVX = Bind.make( - getDocLocales(locales, (locale) => locale.input.Motion.startvx.doc), - getNameLocales(locales, (locale) => locale.input.Motion.startvx.names), - UnionType.orNone(SpeedType.clone()), + const StartVelocity = Bind.make( + getDocLocales(locales, (locale) => locale.input.Motion.velocity.doc), + getNameLocales(locales, (locale) => locale.input.Motion.velocity.names), + UnionType.orNone(velocityType.getTypeReference()), NoneLiteral.make() ); - const StartVY = Bind.make( - getDocLocales(locales, (locale) => locale.input.Motion.startvy.doc), - getNameLocales(locales, (locale) => locale.input.Motion.startvy.names), - UnionType.orNone(SpeedType.clone()), - NoneLiteral.make() - ); - - const StartVZ = Bind.make( - getDocLocales(locales, (locale) => locale.input.Motion.startvz.doc), - getNameLocales(locales, (locale) => locale.input.Motion.startvz.names), - UnionType.orNone(SpeedType.clone()), - NoneLiteral.make() - ); - - const StartVAngle = Bind.make( - getDocLocales(locales, (locale) => locale.input.Motion.startvangle.doc), + const NextPlace = Bind.make( + getDocLocales(locales, (locale) => locale.input.Motion.nextplace.doc), getNameLocales( locales, - (locale) => locale.input.Motion.startvangle.names + (locale) => locale.input.Motion.nextplace.names ), - UnionType.orNone(AngleSpeedType.clone()), - NoneLiteral.make() - ); - - const VX = Bind.make( - getDocLocales(locales, (locale) => locale.input.Motion.vx.doc), - getNameLocales(locales, (locale) => locale.input.Motion.vx.names), - UnionType.orNone(SpeedType.clone()), + UnionType.orNone(placeType.getTypeReference()), NoneLiteral.make() ); - const VY = Bind.make( - getDocLocales(locales, (locale) => locale.input.Motion.vy.doc), - getNameLocales(locales, (locale) => locale.input.Motion.vy.names), - UnionType.orNone(SpeedType.clone()), - NoneLiteral.make() - ); - - const VZ = Bind.make( - getDocLocales(locales, (locale) => locale.input.Motion.vz.doc), - getNameLocales(locales, (locale) => locale.input.Motion.vz.names), - UnionType.orNone(SpeedType.clone()), - NoneLiteral.make() - ); - - const VAngle = Bind.make( - getDocLocales(locales, (locale) => locale.input.Motion.vangle.doc), - getNameLocales(locales, (locale) => locale.input.Motion.vangle.names), - UnionType.orNone(AngleSpeedType.clone()), - NoneLiteral.make() - ); - - const MassBind = Bind.make( - getDocLocales(locales, (locale) => locale.input.Motion.mass.doc), - getNameLocales(locales, (locale) => locale.input.Motion.mass.names), - UnionType.orNone(NumberType.make(Unit.reuse(['kg']))), - // Default to 1kg. - NumberLiteral.make(1, Unit.reuse(['kg'])) - ); - - const BouncinessBind = Bind.make( - getDocLocales(locales, (locale) => locale.input.Motion.bounciness.doc), + const NextVelocity = Bind.make( + getDocLocales( + locales, + (locale) => locale.input.Motion.nextvelocity.doc + ), getNameLocales( locales, - (locale) => locale.input.Motion.bounciness.names + (locale) => locale.input.Motion.nextvelocity.names ), - UnionType.orNone(NumberType.make()), - NumberLiteral.make(Bounciness) - ); - - const GravityBind = Bind.make( - getDocLocales(locales, (locale) => locale.input.Motion.gravity.doc), - getNameLocales(locales, (locale) => locale.input.Motion.gravity.names), - UnionType.orNone(NumberType.make(Unit.reuse(['m'], ['s', 's']))), - NumberLiteral.make(9.8, Unit.reuse(['m'], ['s', 's'])) + UnionType.orNone(velocityType.getTypeReference()), + NoneLiteral.make() ); - const type = new StructureType(PhraseType); - return StreamDefinition.make( getDocLocales(locales, (locale) => locale.input.Motion.doc), getNameLocales(locales, (locale) => locale.input.Motion.names), - [ - TypeBind, - StartPlace, - StartVX, - StartVY, - StartVZ, - StartVAngle, - VX, - VY, - VZ, - VAngle, - MassBind, - BouncinessBind, - GravityBind, - ], + [StartPlace, StartVelocity, NextPlace, NextVelocity], createStreamEvaluator<Motion>( - type.clone(), + placeType.getTypeReference(), Motion, (evaluation) => { - const type = toTypeOutput( - evaluation.getEvaluator().project, - evaluation.get(TypeBind.names, StructureValue) + return new Motion( + evaluation.getEvaluator(), + evaluation.get(StartPlace.names, StructureValue) ?? + createPlaceStructure( + evaluation.getEvaluator(), + 0, + 0, + 0 + ), + evaluation.get(StartVelocity.names, StructureValue) ?? + new NoneValue(evaluation.getCreator()) ); - return type - ? new Motion( - evaluation.getEvaluator(), - type, - evaluation.get(StartPlace.names, StructureValue), - evaluation - .get(StartVX.names, NumberValue) - ?.toNumber(), - evaluation - .get(StartVY.names, NumberValue) - ?.toNumber(), - evaluation - .get(StartVZ.names, NumberValue) - ?.toNumber(), - evaluation - .get(StartVAngle.names, NumberValue) - ?.toNumber(), - evaluation - .get(MassBind.names, NumberValue) - ?.toNumber(), - evaluation - .get(BouncinessBind.names, NumberValue) - ?.toNumber(), - evaluation - .get(GravityBind.names, NumberValue) - ?.toNumber() - ) - : new ValueException( - evaluation.getEvaluator(), - evaluation.getCreator() - ); }, (stream, evaluation) => { stream.update( - // Not valid type output? Revert to the current value. - toTypeOutput( - evaluation.getEvaluator().project, - evaluation.get(TypeBind.names, StructureValue) - ), - evaluation.get(VX.names, NumberValue)?.toNumber(), - evaluation.get(VY.names, NumberValue)?.toNumber(), - evaluation.get(VZ.names, NumberValue)?.toNumber(), - evaluation.get(VAngle.names, NumberValue)?.toNumber(), - evaluation.get(MassBind.names, NumberValue)?.toNumber(), - evaluation - .get(BouncinessBind.names, NumberValue) - ?.toNumber(), - evaluation.get(GravityBind.names, NumberValue)?.toNumber() + evaluation.get(NextPlace.names, StructureValue), + evaluation.get(NextVelocity.names, StructureValue) ); } ), - type.clone() + placeType.getTypeReference() ); } diff --git a/src/locale/InputTexts.ts b/src/locale/InputTexts.ts index 7024f7ee2..bb982b202 100644 --- a/src/locale/InputTexts.ts +++ b/src/locale/InputTexts.ts @@ -45,32 +45,14 @@ type InputTexts = { }; /** A stream of phrases in places and rotations simulating physics */ Motion: NameAndDoc & { - /** The phrase template to use */ - type: NameAndDoc; - /** Where the phrase should start */ - startplace: NameAndDoc; - /** Starting x velocity */ - startvx: NameAndDoc; - /** Starting y velocity */ - startvy: NameAndDoc; - /** Starting z velocity */ - startvz: NameAndDoc; - /** Starting angular velocity */ - startvangle: NameAndDoc; - /** A constant x velocity to hold */ - vx: NameAndDoc; - /** A constant y velocity to hold */ - vy: NameAndDoc; - /** A constant z velocity to hold */ - vz: NameAndDoc; - /** A constant angular velocity to hold */ - vangle: NameAndDoc; - /** Mass, influencing collisions */ - mass: NameAndDoc; - /** Gravity, influencing change in y velocity */ - gravity: NameAndDoc; - /** A coefficient that dampens collisions */ - bounciness: NameAndDoc; + /** The initial place for the motion */ + place: NameAndDoc; + /** The initial velocity for the motion */ + velocity: NameAndDoc; + /** The next place for the motion, overriding physics */ + nextplace: NameAndDoc; + /** The next velocity for the motion, overriding physics */ + nextvelocity: NameAndDoc; }; /** A stream of Place for easily moving Phrases by keyboard */ Placement: NameAndDoc & { @@ -101,6 +83,29 @@ type InputTexts = { limit: string; }; }; + /** A stream of collisions between objects with matter. */ + Collision: NameAndDoc & { + /** The subject of a collision */ + subject: NameAndDoc; + /** The object of a collision. */ + object: NameAndDoc; + }; + /** The values that come out of a collision stream. */ + Rebound: NameAndDoc & { + /** The name a collision stream collided with. */ + subject: NameAndDoc; + /** The name a collision stream collided with. */ + object: NameAndDoc; + /** The direction of the collision, relative to the collision stream's subject. */ + direction: NameAndDoc; + }; + /** A vector indicating a direction and magnitude. */ + Direction: NameAndDoc & { + /** The direction and magnitude on the x-axis */ + x: NameAndDoc; + /** The direction and magnitude on the y-axis */ + y: NameAndDoc; + }; }; export default InputTexts; diff --git a/src/locale/OutputTexts.ts b/src/locale/OutputTexts.ts index 3fff8ecb4..b791f8063 100644 --- a/src/locale/OutputTexts.ts +++ b/src/locale/OutputTexts.ts @@ -42,14 +42,16 @@ export type TypeTexts = { }; type OutputTexts = { - /** The base interface for Phrase, Group, and Stage */ - Type: NameAndDoc; + /** The base interface for Phrase, Group, and Stage, and other types of Output */ + Output: NameAndDoc; /** A group of output with a layout */ Group: NameAndDoc & { /** The list of content in the group */ content: NameAndDoc; /** The layout to use to place the content in the group on stage */ layout: NameAndDoc; + /** The matter to use for the group if it's involved in collisions */ + matter: NameAndDoc; /** $1 = Layout description, $2 = pose description */ description: Template; } & TypeTexts; @@ -61,6 +63,8 @@ type OutputTexts = { wrap: NameAndDoc; /** The alignment to use when wrapped */ alignment: NameAndDoc; + /** The matter properties for the phrase */ + matter: NameAndDoc; /** A description of the phrase for screen readers. 1$: non-optional text, $2: optional name, $3: optional size, $4: optional font, $5: then non-optional pose */ description: Template; } & TypeTexts; @@ -72,15 +76,59 @@ type OutputTexts = { content: NameAndDoc; /** The shape of the frame to clip stage content */ frame: NameAndDoc; - } & TypeTexts; + } & TypeTexts & { + /** Gravity, influencing change in y velocity */ + gravity: NameAndDoc; + }; /** The base interface for shape types */ - Shape: NameAndDoc; + Shape: NameAndDoc & { + /** The kind of shape and its details */ + form: NameAndDoc; + /** The name of a phrase, group, or stage, used in Choice, Collision, and animations */ + name: NameAndDoc; + /** Whether a phrase, group, or stage is selectable by Choice */ + selectable: NameAndDoc; + /** The color of glyphs in a phrase, group, or stage */ + color: NameAndDoc; + /** The background color behind a phrase, group, or stage */ + background: NameAndDoc; + /** The opacity of a phrase, group, or stage */ + opacity: NameAndDoc; + /** The offset of phrase, group, or stage from its place */ + offset: NameAndDoc; + /** The rotation of a phrase, group, or stage */ + rotation: NameAndDoc; + /** The scale of phrase, group, or stage */ + scale: NameAndDoc; + /** Whether a phrase, group, or stage is flipped horizontally */ + flipx: NameAndDoc; + /** Whether a phrase, group, or stage is flipped vertically */ + flipy: NameAndDoc; + /** Pose or sequence for when a phrase, group, or stage enters stage */ + entering: NameAndDoc; + /** Pose or sequence for when a phrase, group, or stage is not moving */ + resting: NameAndDoc; + /** Pose or sequence for when a phrase, group, or stage is moving */ + moving: NameAndDoc; + /** Pose or sequence for when a phrase, group, or stage is leaving stage */ + exiting: NameAndDoc; + /** The curation of transition */ + duration: NameAndDoc; + /** The transition style of transitions */ + style: NameAndDoc; + }; /** A rectangle shape, for Stage.frame */ Rectangle: NameAndDoc & { + /** Left of the rectangle */ left: NameAndDoc; + /** Top of the rectangle */ top: NameAndDoc; + /** Right of the rectangle */ right: NameAndDoc; + /** Bottom of the rectangle */ bottom: NameAndDoc; + /** Depth of rectangle */ + z: NameAndDoc; }; /** A pose, for use in overriding an output's defaults for entering, resting, moving, or existing states */ Pose: NameAndDoc & { @@ -124,6 +172,32 @@ type OutputTexts = { y: NameAndDoc; /** z-coordinate */ z: NameAndDoc; + /** optional rotation */ + rotation: NameAndDoc; + }; + /** A velocity vector */ + Velocity: NameAndDoc & { + /** x-coordinate */ + x: NameAndDoc; + /** y-coordinate */ + y: NameAndDoc; + /** rotation */ + angle: NameAndDoc; + }; + /** Physical properties of matter */ + Matter: NameAndDoc & { + /** in kilograms, how much something weighs for the purposes of collisions */ + mass: NameAndDoc; + /** from 0-1, how bouncy something should be, where 0 means not bouncy at all, and 1 means retaining all of it's energy on collision */ + bounciness: NameAndDoc; + /** from 0-1, where 0 means no sliding, and 1 means sliding indefinitely */ + friction: NameAndDoc; + /** from 0-1, what percent of the size to round the corners of the output's rectangle. */ + roundedness: NameAndDoc; + /** whether the output can collide with other output */ + text: NameAndDoc; + /** whether the output can collide with other shapes */ + shapes: NameAndDoc; }; /** The base interface for arrangement types */ Arrangement: NameAndDoc; diff --git a/src/locale/UITexts.ts b/src/locale/UITexts.ts index cfbb1de38..b11a1c2fa 100644 --- a/src/locale/UITexts.ts +++ b/src/locale/UITexts.ts @@ -328,6 +328,12 @@ type UITexts = { addGroup: string; /** Add a phrase to the output */ addPhrase: string; + /** Add a shape to the output */ + addShape: string; + /** Set place to Motion stream */ + addMotion: string; + /** Set place to Placement stream */ + addPlacement: string; /** Remove child from this output */ remove: string; /** Move child up in list */ diff --git a/src/locale/en-US.json b/src/locale/en-US.json index e2cd78487..77272cf53 100644 --- a/src/locale/en-US.json +++ b/src/locale/en-US.json @@ -1252,7 +1252,6 @@ }, "UnknownType": { "name": "unknown type", - "unknown": "unknown", "connector": ", because ", "emotion": "curious", "doc": "Umm... I don't know what I represent, but I really am curious. Do you know? It seems like we should know. You might need to tell us if we can't figure it out." @@ -2834,57 +2833,21 @@ "Check out the many other ways to configure it below." ], "names": ["⚽️", "Motion"], - "type": { - "doc": "The @Phrase to refine with new @Phrase/resting places and rotations.", - "names": "type" - }, - "startplace": { - "doc": "The initial place for the @Phrase.", - "names": "startplace" - }, - "startvx": { - "doc": "The initial velocity on the x-axis at which the @Phrase should move.", - "names": "startvx" - }, - "startvy": { - "doc": "The initial velocity on the y-axis at which the @Phrase should move. If it, it overrides gravity.", - "names": "startvy" - }, - "startvz": { - "doc": "The initial velocity on the z-axis at which the @Phrase should move.", - "names": "startvz" - }, - "startvangle": { - "doc": "The initial rotational velocity at which the @Phrase should rotate.", - "names": "startvangle" - }, - "vx": { - "doc": "The velocity on the x-axis at which the @Phrase should move, independent of physics.", - "names": "vx" - }, - "vy": { - "doc": "The velocity on the y-axis at which the @Phrase should move, independent of physics.", - "names": "vy" - }, - "vz": { - "doc": "The velocity on the z-axis at which the @Phrase should move, independent of physics.", - "names": "vz" - }, - "vangle": { - "doc": "The rotational velocity at which the @Phrase should rotate, independent of physics.", - "names": "vangle" + "place": { + "doc": "The starting place.", + "names": "place" }, - "mass": { - "doc": "The mass of the @Phrase, which may someday influence collisions.", - "names": "mass" + "velocity": { + "doc": "The starting velocity", + "names": "velocity" }, - "bounciness": { - "doc": "The fraction between 0 and 1 that determines how much a velocity is dampened when a @Phrase collides.", - "names": "bounciness" + "nextplace": { + "doc": "The next place, overriding physics.", + "names": "nextplace" }, - "gravity": { - "doc": "The gravity to apply to @Motion/vy.", - "names": "gravity" + "nextvelocity": { + "doc": "The next place, overriding velocity.", + "names": "nextvelocity" } }, "Chat": { @@ -2959,11 +2922,58 @@ "noConnection": "no connection to Wordplay", "limit": "too many requests to this domain" } + }, + "Collision": { + "names": "Collision", + "doc": [ + "/Hi! @FunctionDefinition here. Check out this cool input./", + "It can help you find out when @Output bump into each other! This is great way to do something when we bump into each other, other than the normal bouncing off each other @Output might do.", + "Just give me a name of @Output, and I'll make a new @Rebound value whenever it bumps into another name. A @Rebound has info about the names that collided and the direction of their collision.", + "And if you give me two names, I'll only make a new value when the two names bump into each other.", + "Right after I make a new value, I'll make a \\ø\\ since the collision is done after it happens. This indicates that there's no more collision.", + "" + ], + "subject": { + "names": "subject", + "doc": "The name of the @Output I should look for collisions on." + }, + "object": { + "names": "other", + "doc": "The name of the other @Output I should look for collisions on." + } + }, + "Rebound": { + "names": "Rebound", + "doc": "I come from @Collision and represent the who was collided with and which direction the collision occurred on. Use me to decide whether to react to a collision in some special way, other than normal physics.", + "direction": { + "names": "direction", + "doc": "The direction and magnitude of the collision, relative to the subject of the collision" + }, + "subject": { + "names": "subject", + "doc": "The name of the output that was hit by the subject." + }, + "object": { + "names": "object", + "doc": "The name of the output that hit the subject" + } + }, + "Direction": { + "names": "Direction", + "doc": "I'm a direction and magnitude, along x and y axes.", + "x": { + "names": "x", + "doc": "The direction and magnitude of the direction along the x-axis." + }, + "y": { + "names": "y", + "doc": "The direction and magnitude of the direction along the y-axis." + } } }, "output": { - "Type": { - "names": "Type", + "Output": { + "names": "Output", "doc": [ "I am not a @StructureDefinition you can actually make. But I am a very important one, as I inspire the most important elements of our dance: @Phrase, @Group, and @Stage.", "Go meet them to learn more about how to use them." @@ -2973,20 +2983,24 @@ "doc": [ "Oh hello, how are you? I'm always fine when others are around, so it's great to be with you!", "I group together @Phrase and @Group on @Stage and put them in an @Arrangement, so there's some order to where they're placed.", - "To work, I need you to give me an @Arrangement, and then a @List of @Type to arrange.", + "To work, I need you to give me an @Arrangement, and then a @List of @Output to arrange.", "For example, here I am with a @Stack arrangement and a few @Phrase to stack vertically:", "\\Group(Stack() [Phrase('first') Phrase('second')])\\", "How exactly I arrange things depend on the @Arrangement you give me." ], "names": ["🔳", "Group"], "layout": { - "doc": "The arrangement to use to put @Type in their places.", + "doc": "The arrangement to use to put @Output in their places.", "names": "layout" }, "content": { - "doc": "The list of @Type to arrange.", + "doc": "The list of @Output to arrange.", "names": "content" }, + "matter": { + "doc": "How I should react if I were to bump into something else with matter.", + "names": "matter" + }, "size": { "doc": "How tall the wonderful content inside me should be, unless they have a size of their own!", "names": "size" @@ -3025,7 +3039,7 @@ }, "rotation": { "doc": "How tilted should I be around my center, my @Pose has a different one.", - "names": "rotation" + "names": ["📐", "rotation"] }, "scale": { "doc": "How big I should be relative to my original size.", @@ -3070,7 +3084,7 @@ "Hello, hello! Remember me? How could anyone forget /me/. That's right, I am the manificent @Phrase, ready to represent the loveliest of @Text on @Stage.", "Just make me like this, and I'll appear on @Stage:", "\\Phrase('magnificient!')\\", - "I need some @Text, obviously, but otherwise, I can do everything a @Type can do, including changing my size, font, rotation, and do all of my incredible dances with @Pose and @Sequence.", + "I need some @Text, obviously, but otherwise, I can do everything a @Output can do, including changing my size, font, rotation, and do all of my incredible dances with @Pose and @Sequence.", "You can also select me on @Stage and edit me on the palette next door." ], "names": ["💬", "Phrase"], @@ -3098,6 +3112,10 @@ "doc": "If there's a @Phrase/wrap boundary set, whether I should align symbols to the start, center, or end of edge.", "names": "alignment" }, + "matter": { + "doc": "The properties to use if I bump into things!", + "names": "matter" + }, "name": { "doc": [ "A name you give me! This is helpful for many things.", @@ -3175,7 +3193,7 @@ "names": ["⠿", "Arrangement"] }, "Row": { - "doc": "I am @Row, a horizontal @Arrangement of @Type, with optional padding in between. Have you met my twin, @Stack?", + "doc": "I am @Row, a horizontal @Arrangement of @Output, with optional padding in between. Have you met my twin, @Stack?", "names": ["➡", "Row"], "description": "row of $1 phrases and groups", "alignment": { @@ -3188,7 +3206,7 @@ } }, "Stack": { - "doc": "I am @Stack, a vertical @Arrangement of @Type, with optional padding in between. Have you met my twin, @Row? ", + "doc": "I am @Stack, a vertical @Arrangement of @Output, with optional padding in between. Have you met my twin, @Row? ", "names": ["⬇", "Stack"], "description": "stack of $1 phrases and groups", "alignment": { @@ -3201,7 +3219,7 @@ } }, "Grid": { - "doc": "I am grid of @Type. Give me a row and column count and I'll make a tidy arrangement with optional padding and cell sizes.", + "doc": "I am grid of @Output. Give me a row and column count and I'll make a tidy arrangement with optional padding and cell sizes.", "names": ["▦", "Grid"], "description": "$1 row $2 column grid", "rows": { @@ -3227,15 +3245,83 @@ }, "Free": { "doc": [ - "I'm like, whatever. Sit wherever you want. Just sit somewhere! Make sure all the @Type you give me have a @Place, otherwise they won't know where to go.", - "Oh, and remember that the @Place you give each @Type is relative to the @Group's @Place! So if you're wondering why things aren't appearing where you expect, try giving the @Group a place too." + "I'm like, whatever. Sit wherever you want. Just sit somewhere! Make sure all the @Output you give me have a @Place, otherwise they won't know where to go.", + "Oh, and remember that the @Place you give each @Output is relative to the @Group's @Place! So if you're wondering why things aren't appearing where you expect, try giving the @Group a place too." ], "names": ["Free"], "description": "free-form $1 outputs" }, "Shape": { "doc": "I'm an inspiration to all shapes. I'm useful for telling @Stage what shape to be.", - "names": "Shape" + "names": ["⬟", "Shape"], + "form": { + "doc": "I'm the kind of shape to show. Each shape requires different information to define its arrangement.", + "names": "form" + }, + "name": { + "doc": "I'm the name you can use, for animations and @Collision. For example, if a represent the ground, you might want to call me 'ground'.", + "names": "name" + }, + "selectable": { + "doc": "Whether I can be selected as part of @Choice.", + "names": "selectable" + }, + "color": { + "doc": "The color of my borders.", + "names": "color" + }, + "background": { + "doc": "The color of my background.", + "names": "background" + }, + "opacity": { + "doc": "How transparent I should be.", + "names": "opacity" + }, + "offset": { + "doc": "How far from my place I should appear, while remaining in place.", + "names": "offset" + }, + "rotation": { + "doc": "How rotated I should be. This affects @Collision.", + "names": "rotation" + }, + "scale": { + "doc": "How magnified I should be, without changing my actual size.", + "names": "scale" + }, + "flipx": { + "doc": "Whether to mirror me on my x-axis.", + "names": "flipx" + }, + "flipy": { + "doc": "Whether to mirror me on my y-axis.", + "names": "flipy" + }, + "entering": { + "doc": "The @Pose or @Sequence I should do when entering @Stage.", + "names": "entering" + }, + "resting": { + "doc": "The @Pose or @Sequence I should do after entering and while I'm not moving.", + "names": "resting" + }, + "moving": { + "doc": "The @Pose or @Sequence I should do when moving places.", + "names": "moving" + }, + "exiting": { + "doc": "The @Pose or @Sequence I should do when leaving @Stage.", + "names": "exiting" + }, + "duration": { + "doc": "How long my animations should take if they are a single @Pose.", + "names": "duration" + }, + "style": { + "doc": "The animation style I should use.", + "names": "style" + } }, "Rectangle": { "doc": "I am a rectangle, useful for making @Stage have a boundary the size of your choosing.", @@ -3255,11 +3341,15 @@ "bottom": { "doc": "The bottom edge of the stage on the y-axis", "names": "bottom" + }, + "z": { + "doc": "The depth position of the rectangle.", + "names": "z" } }, "Pose": { "doc": [ - "You know when someone strikes the most amazing way of standing, a pauses, and everyone looks? That's me. I capture a pose for @Type to be in, and am the building block of their movements.", + "You know when someone strikes the most amazing way of standing, a pauses, and everyone looks? That's me. I capture a pose for @Output to be in, and am the building block of their movements.", "So much goes into a pose. Check out my many inputs to see what kinds of poses you might make!" ], "names": ["🤪", "Pose"], @@ -3272,31 +3362,31 @@ "names": "style" }, "color": { - "doc": "The @Color a @Type should be in this pose, instead of it's default.", + "doc": "The @Color a @Output should be in this pose, instead of it's default.", "names": "color" }, "opacity": { - "doc": "How transparent a @Type should be, between \\0\\ and \\1\\, instead of it's default. Helpful for fading in and out.", + "doc": "How transparent a @Output should be, between \\0\\ and \\1\\, instead of it's default. Helpful for fading in and out.", "names": "opacity" }, "offset": { - "doc": "A @Place indicating how offset from a @Type's place it should be, instead of it's default. Helpful for wiggling in place.", + "doc": "A @Place indicating how offset from a @Output's place it should be, instead of it's default. Helpful for wiggling in place.", "names": "offset" }, "rotation": { - "doc": "How rotated a @Type should be, instead of it's default.", + "doc": "How rotated a @Output should be, instead of it's default.", "names": "rotation" }, "scale": { - "doc": "How magnified a @Type should be relative to its original size, instead of it's default.", + "doc": "How magnified a @Output should be relative to its original size, instead of it's default.", "names": "scale" }, "flipx": { - "doc": "Whether a @Type should be mirrored on the x-axis, instead of its default.", + "doc": "Whether a @Output should be mirrored on the x-axis, instead of its default.", "names": "flipx" }, "flipy": { - "doc": "Whether a @Type should be mirrored on the y-axis, instead of it's default.", + "doc": "Whether a @Output should be mirrored on the y-axis, instead of it's default.", "names": "flipy" }, "description": "$1[transparent $1|] $2[rotated $2 degrees|] $3[scaled $3|] $4[flipped horizontally|] $5[flipped vertically|]" @@ -3343,7 +3433,7 @@ ], "names": ["💃", "Sequence"], "poses": { - "doc": "A @Map of percentages between 0% and 100%, each paired with a @Pose. You don't have to provide all the percents; I will smoothly move a @Type between the ones you give me.", + "doc": "A @Map of percentages between 0% and 100%, each paired with a @Pose. You don't have to provide all the percents; I will smoothly move a @Output between the ones you give me.", "names": "poses" }, "duration": { @@ -3364,7 +3454,52 @@ "names": ["📍", "Place"], "x": { "doc": "A position on the x-axis.", "names": "x" }, "y": { "doc": "A position on the y-axis", "names": "y" }, - "z": { "doc": "A position on the z-axis", "names": "z" } + "z": { "doc": "A position on the z-axis", "names": "z" }, + "rotation": { + "doc": "Rotation at this position", + "names": ["📐", "rotation"] + } + }, + "Velocity": { + "doc": "I'm a location on @Stage. All my inputs are optional, because I'm at the center by default.", + "names": ["↗", "Velocity"], + "x": { + "doc": "How many meters to move each second on the x-axis.", + "names": "x" + }, + "y": { + "doc": "How many meters to move each second on the y-axis.", + "names": "y" + }, + "angle": { + "doc": "How many degrees to rotate each second", + "names": ["angle", "°"] + } + }, + "Matter": { + "doc": "I'm physical properties of output, which influence how I interact with other output on stage.", + "names": ["⚛️", "Matter"], + "mass": { "doc": "A weight, in kilograms", "names": "mass" }, + "bounciness": { + "doc": "How much of my energy to keep on collision, 0 means none, 1 means all of it.", + "names": "bounciness" + }, + "friction": { + "doc": "How much to keep sliding; 0 means none, 1 means forever.", + "names": "friction" + }, + "roundedness": { + "doc": "How much to round the corners of the output; 0 means none and 1 means 100% of its size, making the sizes circular.", + "names": "roundedness" + }, + "text": { + "doc": "Whether it can collide with other output.", + "names": "text" + }, + "shapes": { + "doc": "Whether it can collide with other shapes.", + "names": "ground" + } }, "Stage": { "doc": [ @@ -3377,7 +3512,7 @@ ], "names": ["🎭", "Stage"], "content": { - "doc": "The list of @Type to show on stage.", + "doc": "The list of @Output to show on stage.", "names": "content" }, "frame": { @@ -3422,7 +3557,7 @@ }, "rotation": { "doc": "SAME AS @Group/rotation", - "names": "rotation" + "names": ["📐", "rotation"] }, "scale": { "doc": "SAME AS @Group/scale", @@ -3460,6 +3595,10 @@ "doc": "SAME AS @Phrase/style!", "names": "style" }, + "gravity": { + "doc": "The gravity to apply to output whose's place is in @Motion.", + "names": "gravity" + }, "description": "stage $1[$1|] of $2 phrases $3[$3 frame|] $4" }, "Easing": { @@ -3470,7 +3609,7 @@ }, "sequence": { "sway": { - "doc": "I create a @Sequence that sways back and forth around a @Type's center.", + "doc": "I create a @Sequence that sways back and forth around a @Output's center.", "names": ["sway"], "angle": { "doc": "How much to tilt in the sway.", @@ -3478,24 +3617,24 @@ } }, "bounce": { - "doc": "I create a @Sequence that bounces @Type a given height.", + "doc": "I create a @Sequence that bounces @Output a given height.", "names": ["bounce"], "height": { "doc": "How high to bounce.", "names": ["height"] } }, "spin": { - "doc": "I create a @Sequence that rotates @Type around its center.", + "doc": "I create a @Sequence that rotates @Output around its center.", "names": ["spin"] }, "fadein": { - "doc": "I create a @Sequence that fades @Type in from invisible to visible.", + "doc": "I create a @Sequence that fades @Output in from invisible to visible.", "names": ["fadein"] }, "popup": { - "doc": "I create a @Sequence that makes @Type scale in quickly than shrink to its normal size.", + "doc": "I create a @Sequence that makes @Output scale in quickly than shrink to its normal size.", "names": ["popup"] }, "shake": { - "doc": "I create a @Sequence that makes it look like a @Type is scared.", + "doc": "I create a @Sequence that makes it look like a @Output is scared.", "names": ["shake"] } } @@ -3822,6 +3961,9 @@ "set": "edit this property", "addPhrase": "add a phrase after this", "addGroup": "add a group after this", + "addShape": "add a shape after this", + "addMotion": "set place to Motion stream", + "addPlacement": "set place to Placement stream", "remove": "remove this content", "up": "move this content up", "down": "move this content down", diff --git a/src/nodes/Evaluate.ts b/src/nodes/Evaluate.ts index 6f66a0883..49bf2d434 100644 --- a/src/nodes/Evaluate.ts +++ b/src/nodes/Evaluate.ts @@ -339,6 +339,14 @@ export default class Evaluate extends Expression { return mappings; } + getInput( + bind: Bind, + context: Context + ): Expression | Expression[] | undefined { + const mapping = this.getInputMapping(context); + return mapping?.inputs.find((input) => input.expected === bind)?.given; + } + getLastInput(): Expression | undefined { return this.inputs[this.inputs.length - 1]; } @@ -602,7 +610,7 @@ export default class Evaluate extends Expression { : undefined; } - is(def: StructureDefinition, context: Context) { + is(def: StructureDefinition | StreamDefinition, context: Context) { return this.getFunction(context) === def; } diff --git a/src/nodes/Expression.ts b/src/nodes/Expression.ts index b06156371..ac05f6b57 100644 --- a/src/nodes/Expression.ts +++ b/src/nodes/Expression.ts @@ -42,6 +42,22 @@ export default abstract class Expression extends Node { abstract getDependencies(_: Context): Expression[]; + getAllDependencies( + context: Context, + dependencies?: Set<Expression> + ): Set<Expression> { + if (dependencies === undefined) dependencies = new Set(); + + // Prevent cycles. + if (dependencies.has(this)) return dependencies; + + for (const dep of this.getDependencies(context)) { + dep.getAllDependencies(context, dependencies); + dependencies.add(dep); + } + return dependencies; + } + /** By default, an expression is constant if all of it's dependencies are constant. */ isConstant(context: Context): boolean { // Get the expression's dependencies. diff --git a/src/nodes/Reaction.ts b/src/nodes/Reaction.ts index b27fd3aa0..fcb2c6866 100644 --- a/src/nodes/Reaction.ts +++ b/src/nodes/Reaction.ts @@ -150,16 +150,11 @@ export default class Reaction extends Expression { if (!(conditionType instanceof BooleanType)) conflicts.push(new ExpectedBooleanCondition(this, conditionType)); - // The condition should reference a stream. if ( - !this.condition - .nodes() - .some( - (node) => - node instanceof Expression && - context.getStreamType(node.getType(context)) !== - undefined - ) + !Array.from(this.condition.getAllDependencies(context)).some( + (node) => + context.getStreamType(node.getType(context)) !== undefined + ) ) conflicts.push(new ExpectedStream(this)); @@ -220,16 +215,6 @@ export default class Reaction extends Expression { value ); - // Did any of the streams cause the current evaluation? - const dependencies = evaluator.reactionDependencies.pop(); - const streams = dependencies ? dependencies.streams : undefined; - const changed = - streams === undefined - ? false - : Array.from(streams).some((stream) => - evaluator.didStreamCauseReaction(stream) - ); - // See if there's a stream created for this. const stream = evaluator.getStreamFor(this); @@ -237,8 +222,7 @@ export default class Reaction extends Expression { // so evaluate the next step. if (stream) { // if the condition was true and a dependency changed, jump to the next step. - if (changed && value.bool) - evaluator.jump(initialSteps.length + 1); + if (value.bool) evaluator.jump(initialSteps.length + 1); // If it was false, push the last reaction value and skip the rest. else { const latest = stream.latest(); diff --git a/src/nodes/StreamDefinition.ts b/src/nodes/StreamDefinition.ts index 500709954..ec30eeadf 100644 --- a/src/nodes/StreamDefinition.ts +++ b/src/nodes/StreamDefinition.ts @@ -163,6 +163,10 @@ export default class StreamDefinition extends DefinitionExpression { return this.names.getPreferredNameString(locales); } + getReference(locales: Locale[] = []): Reference { + return Reference.make(this.getPreferredName(locales), this); + } + /** * Name, inputs, and outputs must match. */ diff --git a/src/nodes/StructureDefinition.ts b/src/nodes/StructureDefinition.ts index 3b720027f..0bfe0d731 100644 --- a/src/nodes/StructureDefinition.ts +++ b/src/nodes/StructureDefinition.ts @@ -30,7 +30,7 @@ import Reference from './Reference'; import NotAnInterface from '@conflicts/NotAnInterface'; import { optional, type Grammar, type Replacement, node, list } from './Node'; import type Locale from '@locale/Locale'; -import type NameType from './NameType'; +import NameType from './NameType'; import InternalException from '@values/InternalException'; import Glyphs from '../lore/Glyphs'; import Purpose from '../concepts/Purpose'; @@ -235,9 +235,22 @@ export default class StructureDefinition extends DefinitionExpression { this.getImplementedFunctions().length === 0 ); } + + getTypeReference(): NameType { + return new NameType(this.getNames()[0], undefined, this); + } + + getReference(locales: Locale[] = []): Reference { + return Reference.make( + this.names.getPreferredNameString(locales, true), + this + ); + } + getAbstractFunctions(): FunctionDefinition[] { return this.getFunctions(false); } + getImplementedFunctions(): FunctionDefinition[] { return this.getFunctions(true); } diff --git a/src/nodes/Unit.ts b/src/nodes/Unit.ts index 92329a6e1..6dc0c8a02 100644 --- a/src/nodes/Unit.ts +++ b/src/nodes/Unit.ts @@ -104,9 +104,9 @@ export default class Unit extends Type { else { this.denominator.push( Dimension.make( - this.numerator.length > 0, + this.denominator.length > 0, unit, - exp + Math.abs(exp) ) ); if (this.slash === undefined) diff --git a/src/output/Arrangement.ts b/src/output/Arrangement.ts index 9bd6e4799..2d70b9bba 100644 --- a/src/output/Arrangement.ts +++ b/src/output/Arrangement.ts @@ -2,10 +2,10 @@ import toStructure from '../basis/toStructure'; import { TYPE_SYMBOL } from '@parser/Symbols'; import type Value from '@values/Value'; import { getBind } from '@locale/getBind'; -import Output from './Output'; +import Valued from './Valued'; import type RenderContext from './RenderContext'; import type Place from './Place'; -import type TypeOutput from './TypeOutput'; +import type Output from './Output'; import type Locale from '../locale/Locale'; export function createArrangementType(locales: Locale[]) { @@ -14,14 +14,14 @@ export function createArrangementType(locales: Locale[]) { `); } -export default abstract class Arrangement extends Output { +export default abstract class Arrangement extends Valued { constructor(value: Value) { super(value); } /** Compute positions for all subgroups in the group. */ abstract getLayout( - output: (TypeOutput | null)[], + output: (Output | null)[], context: RenderContext ): { left: number; @@ -30,11 +30,11 @@ export default abstract class Arrangement extends Output { bottom: number; width: number; height: number; - places: [TypeOutput, Place][]; + places: [Output, Place][]; }; abstract getDescription( - output: (TypeOutput | null)[], + output: (Output | null)[], locales: Locale[] ): string; } diff --git a/src/output/Color.ts b/src/output/Color.ts index 00ba77933..51a0f8f41 100644 --- a/src/output/Color.ts +++ b/src/output/Color.ts @@ -1,7 +1,7 @@ import Decimal from 'decimal.js'; import toStructure from '../basis/toStructure'; import type Value from '@values/Value'; -import Output, { getOutputInputs } from './Output'; +import Valued, { getOutputInputs } from './Valued'; import { toDecimal } from './Stage'; import ColorJS from 'colorjs.io'; import { TYPE_SYMBOL } from '@parser/Symbols'; @@ -24,7 +24,7 @@ export function createColorType(locales: Locale[]) { `); } -export default class Color extends Output { +export default class Color extends Valued { readonly lightness: Decimal; readonly chroma: Decimal; readonly hue: Decimal; @@ -46,6 +46,10 @@ export default class Color extends Output { ); } + hash() { + return `${this.lightness}${this.chroma}${this.hue}`; + } + toCSS() { const color = new ColorJS( ColorJS.spaces.lch, diff --git a/src/output/Direction.ts b/src/output/Direction.ts new file mode 100644 index 000000000..72f00311d --- /dev/null +++ b/src/output/Direction.ts @@ -0,0 +1,46 @@ +import toStructure from '../basis/toStructure'; +import type Value from '@values/Value'; +import { getBind } from '@locale/getBind'; +import Valued from './Valued'; +import type Evaluator from '@runtime/Evaluator'; +import NumberValue from '@values/NumberValue'; +import type { EvaluationNode } from '@runtime/Evaluation'; +import StructureValue from '../values/StructureValue'; +import Unit from '../nodes/Unit'; +import type Locale from '../locale/Locale'; + +export function createDirectionType(locales: Locale[]) { + return toStructure(` + ${getBind(locales, (locale) => locale.input.Direction, '•')}( + ${getBind(locales, (locale) => locale.input.Direction.x)}•#m + ${getBind(locales, (locale) => locale.input.Direction.y)}•#m + ) +`); +} + +export default class Direction extends Valued { + readonly x: number; + readonly y: number; + + constructor(value: Value, x: number, y: number) { + super(value); + + this.x = x; + this.y = y; + } +} + +export function createDirectionStructure( + evaluator: Evaluator, + creator: EvaluationNode, + x: number, + y: number +): StructureValue { + return StructureValue.make( + evaluator, + creator, + evaluator.project.shares.output.Direction, + new NumberValue(creator, x, Unit.reuse(['m'])), + new NumberValue(creator, y, Unit.reuse(['m'])) + ); +} diff --git a/src/output/Shapes.ts b/src/output/Form.ts similarity index 51% rename from src/output/Shapes.ts rename to src/output/Form.ts index 8f219af38..bff012edb 100644 --- a/src/output/Shapes.ts +++ b/src/output/Form.ts @@ -1,70 +1,88 @@ -import toStructure from '../basis/toStructure'; -import { TYPE_SYMBOL } from '../parser/Symbols'; +import type Locale from '../locale/Locale'; +import { getFirstName } from '../locale/Locale'; import StructureValue from '../values/StructureValue'; import type Value from '../values/Value'; -import { getBind } from '../locale/getBind'; import { toNumber } from './Stage'; +import Valued, { getOutputInputs } from './Valued'; import { PX_PER_METER } from './outputToCSS'; -import type Locale from '../locale/Locale'; -import { getOutputInputs } from './Output'; -import { getFirstName } from '../locale/Locale'; - -export function createShapeType(locales: Locale[]) { - return toStructure(` - ${getBind(locales, (locale) => locale.output.Shape, TYPE_SYMBOL)}() -`); -} - -export function createRectangleType(locales: Locale[]) { - return toStructure(` - ${getBind(locales, (locale) => locale.output.Rectangle, '•')} Shape( - ${getBind(locales, (locale) => locale.output.Rectangle.left)}•#m - ${getBind(locales, (locale) => locale.output.Rectangle.top)}•#m - ${getBind(locales, (locale) => locale.output.Rectangle.right)}•#m - ${getBind(locales, (locale) => locale.output.Rectangle.bottom)}•#m - ) -`); -} -export abstract class Shape { +export abstract class Form extends Valued { /** Should return a valid CSS clip-path value */ abstract toCSSClip(): string; abstract toSVGPath(): string; abstract getLeft(): number; abstract getTop(): number; + abstract getZ(): number; abstract getWidth(): number; abstract getHeight(): number; - abstract getDescription(locale: Locale): string; + abstract getDescription(locales: Locale[]): string; + + getShortDescription(locales: Locale[]) { + return this.getDescription(locales); + } + + getOutput() { + return []; + } + + isEmpty() { + return true; + } + + find() { + return undefined; + } } -export class Rectangle extends Shape { +export class Rectangle extends Form { readonly left: number; readonly top: number; readonly right: number; readonly bottom: number; - - constructor(left: number, top: number, right: number, bottom: number) { - super(); + readonly z: number; + + constructor( + value: Value, + left: number, + top: number, + right: number, + bottom: number, + z: number + ) { + super(value); this.left = left; this.top = top; this.right = right; this.bottom = bottom; + this.z = z; } getLeft() { - return this.left * PX_PER_METER; + return Math.min(this.left, this.right); } getTop() { - return -this.top * PX_PER_METER; + return Math.max(this.top, this.bottom); + } + + getZ() { + return this.z; + } + + getWidth() { + return Math.abs(this.left - this.right); + } + + getHeight() { + return Math.abs(this.bottom - this.top); } getPoints() { - const left = this.getLeft(); - const top = this.getTop(); - const right = this.right * PX_PER_METER; - const bottom = -this.bottom * PX_PER_METER; + const left = this.getLeft() * PX_PER_METER; + const top = -this.getTop() * PX_PER_METER; + const right = (this.getLeft() + this.getWidth()) * PX_PER_METER; + const bottom = -(this.getTop() - this.getHeight()) * PX_PER_METER; return { left, top, right, bottom }; } @@ -82,34 +100,25 @@ export class Rectangle extends Shape { } L ${right - minX} ${bottom - minY} L ${right - minX} ${top - minY} Z`; } - getWidth() { - const { left, right } = this.getPoints(); - return Math.abs(left - right); - } - - getHeight() { - const { top, bottom } = this.getPoints(); - return Math.abs(bottom - top); - } - - getDescription(locale: Locale): string { - return getFirstName(locale.output.Rectangle.names); + getDescription(locales: Locale[]): string { + return getFirstName(locales[0].output.Rectangle.names); } } -export function toShape(value: Value | undefined) { +export function toRectangle(value: Value | undefined) { if (!(value instanceof StructureValue)) return undefined; - const [leftVal, topVal, rightVal, bottomVal] = getOutputInputs(value); + const [leftVal, topVal, rightVal, bottomVal, zVal] = getOutputInputs(value); const left = toNumber(leftVal); const top = toNumber(topVal); const right = toNumber(rightVal); const bottom = toNumber(bottomVal); + const z = toNumber(zVal) ?? 0; return left !== undefined && top !== undefined && right !== undefined && bottom !== undefined - ? new Rectangle(left, top, right, bottom) + ? new Rectangle(value, left, top, right, bottom, z) : undefined; } diff --git a/src/output/Free.ts b/src/output/Free.ts index 95641abc7..9dac147f4 100644 --- a/src/output/Free.ts +++ b/src/output/Free.ts @@ -1,7 +1,7 @@ import toStructure from '../basis/toStructure'; import type Value from '@values/Value'; import type Color from './Color'; -import type TypeOutput from './TypeOutput'; +import type Output from './Output'; import type RenderContext from './RenderContext'; import Place from './Place'; import { getBind } from '@locale/getBind'; @@ -21,8 +21,8 @@ export class Free extends Arrangement { super(value); } - getLayout(children: (TypeOutput | null)[], context: RenderContext) { - const places: [TypeOutput, Place][] = []; + getLayout(children: (Output | null)[], context: RenderContext) { + const places: [Output, Place][] = []; let left = 0, right = 0, bottom = 0, @@ -62,7 +62,7 @@ export class Free extends Arrangement { return undefined; } - getDescription(output: TypeOutput[], locales: Locale[]) { + getDescription(output: Output[], locales: Locale[]) { return concretize( locales[0], locales[0].output.Free.description, diff --git a/src/output/Grid.ts b/src/output/Grid.ts index 0b2eb0619..c4858a9a8 100644 --- a/src/output/Grid.ts +++ b/src/output/Grid.ts @@ -1,7 +1,7 @@ import toStructure from '../basis/toStructure'; import type Value from '@values/Value'; import type Color from './Color'; -import type TypeOutput from './TypeOutput'; +import type Output from './Output'; import type RenderContext from './RenderContext'; import { getBind } from '@locale/getBind'; import Arrangement from './Arrangement'; @@ -10,7 +10,7 @@ import Place from './Place'; import NoneValue from '@values/NoneValue'; import concretize from '../locale/concretize'; import type Locale from '../locale/Locale'; -import { getOutputInputs } from './Output'; +import { getOutputInputs } from './Valued'; import StructureValue from '../values/StructureValue'; export function createGridType(locales: Locale[]) { @@ -58,7 +58,7 @@ export class Grid extends Arrangement { : undefined; } - getLayout(outputs: (TypeOutput | null)[], context: RenderContext) { + getLayout(outputs: (Output | null)[], context: RenderContext) { const layouts = outputs.map((output) => output ? output.getLayout(context) : null ); @@ -139,7 +139,7 @@ export class Grid extends Arrangement { this.padding * (rows - 1); // Next, position each child in a cell, iterating through each row from left to right. - const places: [TypeOutput, Place][] = []; + const places: [Output, Place][] = []; for (let row = 0; row < rows; row++) { for (let col = 0; col < columns; col++) { // Get the output in this cell. @@ -184,7 +184,7 @@ export class Grid extends Arrangement { return undefined; } - getDescription(_: TypeOutput[], locales: Locale[]) { + getDescription(_: Output[], locales: Locale[]) { return concretize( locales[0], locales[0].output.Grid.description, diff --git a/src/output/Group.ts b/src/output/Group.ts index 586eb8572..3d9a7e076 100644 --- a/src/output/Group.ts +++ b/src/output/Group.ts @@ -8,24 +8,25 @@ import type Pose from './Pose'; import type RenderContext from './RenderContext'; import type Sequence from './Sequence'; import TextLang from './TextLang'; -import TypeOutput, { DefaultStyle } from './TypeOutput'; -import { getStyle, toArrangement, toTypeOutputList } from './toTypeOutput'; +import Output, { DefaultStyle } from './Output'; +import { getTypeStyle, toArrangement, toOutputList } from './toOutput'; import { TYPE_SYMBOL } from '../parser/Symbols'; import type { NameGenerator } from './Stage'; import type Locale from '../locale/Locale'; import type Project from '../models/Project'; import type { DefinitePose } from './Pose'; import StructureValue from '@values/StructureValue'; -import { getOutputInput } from './Output'; +import { getOutputInput } from './Valued'; import concretize from '../locale/concretize'; import { SupportedFontsFamiliesType, type SupportedFace } from '../basis/Fonts'; import { getFirstName } from '../locale/Locale'; +import Matter, { toMatter } from './Matter'; export function createGroupType(locales: Locale[]) { return toStructure(` - ${getBind(locales, (locale) => locale.output.Group, TYPE_SYMBOL)} Type( + ${getBind(locales, (locale) => locale.output.Group, TYPE_SYMBOL)} Output( ${getBind(locales, (locale) => locale.output.Group.layout)}•Arrangement - ${getBind(locales, (locale) => locale.output.Group.content)}•[Type|ø] + ${getBind(locales, (locale) => locale.output.Group.content)}•[Output|ø] ${getBind(locales, (locale) => locale.output.Group.size)}•${'#m|ø: ø'} ${getBind( locales, @@ -56,19 +57,22 @@ export function createGroupType(locales: Locale[]) { ) .flat() .join('|')}: "${DefaultStyle}" + ${getBind(locales, (locale) => locale.output.Group.matter)}•Matter|ø: ø )`); } -export default class Group extends TypeOutput { - readonly content: (TypeOutput | null)[]; +export default class Group extends Output { + readonly content: (Output | null)[]; readonly layout: Arrangement; + readonly matter: Matter | undefined; private _description: string | undefined = undefined; constructor( value: Value, layout: Arrangement, - content: (TypeOutput | null)[], + content: (Output | null)[], + matter: Matter | undefined, size: number | undefined = undefined, face: SupportedFace | undefined = undefined, place: Place | undefined = undefined, @@ -102,6 +106,7 @@ export default class Group extends TypeOutput { this.content = content; this.layout = layout; + this.matter = matter; } getLayout(context: RenderContext) { @@ -124,7 +129,7 @@ export default class Group extends TypeOutput { return this.content; } - find(check: (output: TypeOutput) => boolean): TypeOutput | undefined { + find(check: (output: Output) => boolean): Output | undefined { for (const output of this.content) { if (output !== null) { if (check(output)) return output; @@ -164,12 +169,13 @@ export default class Group extends TypeOutput { export function toGroup( project: Project, value: Value | undefined, - namer?: NameGenerator + namer: NameGenerator ): Group | undefined { if (!(value instanceof StructureValue)) return undefined; const layout = toArrangement(project, getOutputInput(value, 0)); - const content = toTypeOutputList(project, getOutputInput(value, 1), namer); + const content = toOutputList(project, getOutputInput(value, 1), namer); + const matter = toMatter(getOutputInput(value, 21)); const { size, @@ -185,7 +191,7 @@ export function toGroup( exiting: exit, duration, style, - } = getStyle(project, value, 2); + } = getTypeStyle(project, value, 2); return layout && content && @@ -197,6 +203,7 @@ export function toGroup( value, layout, content, + matter, size, font, place, diff --git a/src/output/Matter.ts b/src/output/Matter.ts new file mode 100644 index 000000000..c8f9b40f0 --- /dev/null +++ b/src/output/Matter.ts @@ -0,0 +1,90 @@ +import toStructure from '../basis/toStructure'; +import type Value from '@values/Value'; +import { getBind } from '@locale/getBind'; +import Valued, { getOutputInputs } from './Valued'; +import { toBoolean, toNumber } from './Stage'; +import StructureValue from '../values/StructureValue'; +import type Locale from '../locale/Locale'; +import { TRUE_SYMBOL } from '../parser/Symbols'; + +export const DefaultBounciness = 0.5; + +export function createMatterType(locales: Locale[]) { + return toStructure(` + ${getBind(locales, (locale) => locale.output.Matter, '•')}( + ${getBind(locales, (locale) => locale.output.Matter.mass)}•#kg: 1kg + ${getBind(locales, (locale) => locale.output.Matter.bounciness)}•#: 0 + ${getBind(locales, (locale) => locale.output.Matter.friction)}•#: 0.8 + ${getBind(locales, (locale) => locale.output.Matter.roundedness)}•#: 0.1 + ${getBind( + locales, + (locale) => locale.output.Matter.text + )}•?: ${TRUE_SYMBOL} + ${getBind( + locales, + (locale) => locale.output.Matter.shapes + )}•?: ${TRUE_SYMBOL} + ) +`); +} + +export default class Matter extends Valued { + readonly mass: number; + readonly bounciness: number; + readonly friction: number; + readonly roundedness: number; + readonly text: boolean; + readonly shapes: boolean; + + constructor( + value: Value, + mass: number, + bounciness: number, + friction: number, + roundedness: number, + text: boolean, + shapes: boolean + ) { + super(value); + + this.mass = mass; + this.bounciness = bounciness; + this.friction = friction; + this.roundedness = roundedness; + this.text = text; + this.shapes = shapes; + } +} + +export function toMatter(value: Value | undefined): Matter | undefined { + if (!(value instanceof StructureValue)) return undefined; + + const [ + massVal, + bounceVal, + frictionVal, + roundednessVal, + textVal, + shapesVal, + ] = getOutputInputs(value); + const mass = toNumber(massVal); + const bounce = toNumber(bounceVal); + const friction = toNumber(frictionVal); + const roundedness = toNumber(roundednessVal); + const text = toBoolean(textVal); + const shapes = toBoolean(shapesVal); + return mass !== undefined && + bounce !== undefined && + friction !== undefined && + roundedness !== undefined + ? new Matter( + value, + mass, + bounce, + friction, + roundedness, + text ?? true, + shapes ?? true + ) + : undefined; +} diff --git a/src/output/Output.ts b/src/output/Output.ts index 023310501..2157f37ad 100644 --- a/src/output/Output.ts +++ b/src/output/Output.ts @@ -1,36 +1,139 @@ +import toStructure from '../basis/toStructure'; import type Value from '@values/Value'; -import type StructureValue from '../values/StructureValue'; - -/** - * A base class that represents some part of Stage output. - * It's core responsibility is to store a link to a Structure value, - * maintaining provenance. - * */ -export default class Output { - /** - * The value on which this output component is based. - * If undefined, it means it was generated by the system and not by code. - * */ - readonly value: Value; +import type Color from './Color'; +import Valued from './Valued'; +import type Place from './Place'; +import { getBind } from '@locale/getBind'; +import { TYPE_SYMBOL } from '@parser/Symbols'; +import Sequence from './Sequence'; +import TextLang from './TextLang'; +import type Pose from './Pose'; +import type { DefinitePose } from './Pose'; +import type RenderContext from './RenderContext'; +import Fonts, { type SupportedFace } from '../basis/Fonts'; +import type Locale from '../locale/Locale'; - constructor(value: Value) { - this.value = value; - } +export function createOutputType(locales: Locale[]) { + return toStructure(` + ${getBind(locales, (locale) => locale.output.Output, TYPE_SYMBOL)}() +`); } -export function getOutputInputs( - value: StructureValue, - start = 0 -): (Value | undefined)[] { - return value.type.inputs - .slice(start) - .map((input) => value.resolve(input.names)); -} +export const DefaultStyle = 'zippy'; + +/** Every group has the same style information. */ +export default abstract class Output extends Valued { + readonly size: number | undefined; + readonly face: SupportedFace | undefined; + readonly place: Place | undefined; + readonly name: TextLang | string; + readonly selectable: boolean; + readonly background: Color | undefined; + readonly pose: DefinitePose; + readonly entering: Pose | Sequence | undefined; + readonly resting: Pose | Sequence | undefined; + readonly moving: Pose | Sequence | undefined; + readonly exiting: Pose | Sequence | undefined; + readonly duration: number; + readonly style: string; + + constructor( + value: Value, + size: number | undefined = undefined, + font: SupportedFace | undefined = undefined, + place: Place | undefined = undefined, + name: TextLang | string, + selectable: boolean, + background: Color | undefined, + pose: DefinitePose, + entry: Pose | Sequence | undefined = undefined, + resting: Pose | Sequence | undefined = undefined, + moving: Pose | Sequence | undefined = undefined, + exiting: Pose | Sequence | undefined = undefined, + duration: number, + style: string + ) { + super(value); + + this.size = size ? Math.max(0, size) : size; + this.face = font; + this.place = place; + this.name = name; + this.selectable = selectable; + this.background = background; + this.pose = pose; + this.entering = entry; + this.resting = resting; + this.moving = moving; + this.exiting = exiting; + this.duration = duration; + this.style = style; + + if (this.face) Fonts.loadFace(this.face); + } + + abstract getLayout(context: RenderContext): { + output: Output; + left: number; + right: number; + top: number; + bottom: number; + width: number; + height: number; + ascent: number; + descent: number; + places: [Output, Place][]; + }; + + abstract getOutput(): (Output | null)[]; + abstract getBackground(): Color | undefined; + abstract getShortDescription(locales: Locale[]): string; + abstract getDescription(locales: Locale[]): string; + + /* + Given a predict function that takes a type input, recursively scans + outputs for a match. + */ + abstract find(check: (output: Output) => boolean): Output | undefined; + + getRestOrDefaultPose(): Pose | Sequence { + return this.resting ?? this.pose; + } -export function getOutputInput( - value: StructureValue, - index: number -): Value | undefined { - const input = value.type.inputs[index]; - return input ? value.resolve(input.names) : undefined; + getFirstRestPose(): Pose { + return this.resting instanceof Sequence + ? this.resting.getFirstPose() ?? this.pose + : this.resting ?? this.pose; + } + + getDefaultPose(): DefinitePose { + return this.pose; + } + + getRenderContext(context: RenderContext) { + return context.withFontAndSize(this.face, this.size); + } + + getHTMLID(): string { + return `output-${this.getName()}`; + } + + abstract isEmpty(): boolean; + + /** + * By default, a group's name for the purpose of animations is the ID of the node that created it. + * */ + getName(): string { + return this.name instanceof TextLang ? this.name.text : this.name; + } + + isAnimated() { + return ( + this.entering !== undefined || + this.resting instanceof Sequence || + this.moving !== undefined || + this.exiting !== undefined || + this.duration > 0 + ); + } } diff --git a/src/output/OutputAnimation.ts b/src/output/OutputAnimation.ts index 43d9e2f5e..5dacf9a48 100644 --- a/src/output/OutputAnimation.ts +++ b/src/output/OutputAnimation.ts @@ -1,4 +1,4 @@ -import type TypeOutput from './TypeOutput'; +import type Output from './Output'; import { PX_PER_METER, sizeToPx, toOutputTransform } from './outputToCSS'; import Place from './Place'; import Pose from './Pose'; @@ -32,7 +32,7 @@ export default class OutputAnimation { scene: Scene; /** The current phrase for this name */ - output: TypeOutput; + output: Output; /** The current context for rendering */ context: RenderContext; @@ -51,7 +51,7 @@ export default class OutputAnimation { constructor( scene: Scene, - phrase: TypeOutput, + phrase: Output, context: RenderContext, entry: boolean ) { @@ -80,7 +80,7 @@ export default class OutputAnimation { } /** Update the current animation with a new phrase by the same name. */ - update(output: TypeOutput, context: RenderContext, entry: boolean) { + update(output: Output, context: RenderContext, entry: boolean) { // Before we update, see if the rest pose changed so we can tween it. const prior = this.output; @@ -103,7 +103,7 @@ export default class OutputAnimation { } /** Change to the still state and start a transition to it. */ - rest(prior?: TypeOutput) { + rest(prior?: Output) { this.state = State.Rest; const priorPose = prior?.getRestOrDefaultPose(); const currentPose = this.output.getRestOrDefaultPose(); diff --git a/src/output/Phrase.ts b/src/output/Phrase.ts index 5a4ff6529..df128cd5e 100644 --- a/src/output/Phrase.ts +++ b/src/output/Phrase.ts @@ -7,7 +7,7 @@ import Fonts, { type SupportedFace, } from '../basis/Fonts'; import TextValue from '@values/TextValue'; -import TypeOutput, { DefaultStyle } from './TypeOutput'; +import Output, { DefaultStyle } from './Output'; import type RenderContext from './RenderContext'; import type Place from './Place'; import ListValue from '@values/ListValue'; @@ -22,16 +22,18 @@ import type Locale from '../locale/Locale'; import type Project from '../models/Project'; import type { DefinitePose } from './Pose'; import StructureValue from '../values/StructureValue'; -import { getOutputInput } from './Output'; -import { getStyle } from './toTypeOutput'; +import { getOutputInput } from './Valued'; +import { getTypeStyle } from './toOutput'; import MarkupValue from '@values/MarkupValue'; import concretize from '../locale/concretize'; import type Markup from '../nodes/Markup'; import segmentWraps from './segmentWraps'; +import type Matter from './Matter'; +import { toMatter } from './Matter'; export function createPhraseType(locales: Locale[]) { return toStructure(` - ${getBind(locales, (locale) => locale.output.Phrase, '•')} Type( + ${getBind(locales, (locale) => locale.output.Phrase, '•')} Output( ${getBind(locales, (locale) => locale.output.Phrase.text)}•""|[""]|\`…\` ${getBind(locales, (locale) => locale.output.Phrase.size)}•${'#m|ø: ø'} ${getBind( @@ -77,6 +79,7 @@ export function createPhraseType(locales: Locale[]) { locales, (locale) => locale.output.Phrase.alignment )}•'<'|'|'|'>': '|' + ${getBind(locales, (locale) => locale.output.Phrase.matter)}•Matter|ø: ø )`); } @@ -91,10 +94,11 @@ export type Metrics = { descent: number; }; -export default class Phrase extends TypeOutput { +export default class Phrase extends Output { readonly text: TextLang[] | MarkupValue; readonly wrap: number | undefined; readonly alignment: string | undefined; + readonly matter: Matter | undefined; private _metrics: Metrics | undefined = undefined; @@ -117,7 +121,8 @@ export default class Phrase extends TypeOutput { duration: number, style: string, wrap: number | undefined, - alignment: string | undefined + alignment: string | undefined, + matter: Matter | undefined ) { super( value, @@ -139,13 +144,14 @@ export default class Phrase extends TypeOutput { this.text = text; this.wrap = wrap === undefined ? undefined : Math.max(1, wrap); this.alignment = alignment; + this.matter = matter; // Make sure this font is loaded. This is a little late -- we could do some static analysis // and try to determine this in advance -- but anything can compute a font name. Maybe an optimization later. if (this.face) Fonts.loadFace(this.face); } - find(check: (output: TypeOutput) => boolean): TypeOutput | undefined { + find(check: (output: Output) => boolean): Output | undefined { return check(this) ? this : undefined; } @@ -264,7 +270,7 @@ export default class Phrase extends TypeOutput { return dimensions; } - getOutput(): TypeOutput[] { + getOutput(): Output[] { return []; } @@ -348,7 +354,7 @@ export function toFont(value: Value | undefined): string | undefined { export function toPhrase( project: Project, value: Value | undefined, - namer: NameGenerator | undefined + namer: NameGenerator ): Phrase | undefined { if (!(value instanceof StructureValue)) return undefined; @@ -368,10 +374,11 @@ export function toPhrase( exiting: exit, duration, style, - } = getStyle(project, value, 1); + } = getTypeStyle(project, value, 1); const wrap = toNumber(getOutputInput(value, 20)); const alignment = toText(getOutputInput(value, 21)); + const matter = toMatter(getOutputInput(value, 22)); return texts !== undefined && duration !== undefined && @@ -384,7 +391,7 @@ export function toPhrase( size, font, place, - namer?.getName(name?.text, value) ?? `${value.creator.id}`, + namer.getName(name?.text, value), selectable, background, pose, @@ -395,7 +402,8 @@ export function toPhrase( duration, style, wrap, - alignment?.text + alignment?.text, + matter ) : undefined; } diff --git a/src/output/Physics.ts b/src/output/Physics.ts new file mode 100644 index 000000000..799e52b24 --- /dev/null +++ b/src/output/Physics.ts @@ -0,0 +1,508 @@ +import MatterJS from 'matter-js'; +import { PX_PER_METER } from './outputToCSS'; +import type Matter from './Matter'; +import type { OutputInfo, OutputInfoSet, OutputsByName } from './Scene'; +import Phrase from './Phrase'; +import Group from './Group'; +import Stage, { DefaultGravity } from './Stage'; +import type Value from '../values/Value'; +import Motion from '../input/Motion'; +import type Evaluator from '../runtime/Evaluator'; +import type { ReboundEvent } from '../input/Collision'; +import Collision from '../input/Collision'; +import { Rectangle } from './Form'; +import type Shape from './Shape'; + +const TextCategory = 0b0001; +const ShapeCategory = 0b0010; + +/** + * A MatterJS engine to keep Scene simpler. + * All details about MatterJS should be encapsulated here, + * hopefully not leaking out to Scene, Motion, or other related components. */ +export default class Physics { + /** The evaluator driving physics */ + readonly evaluator: Evaluator; + + /** A set of Matter JS physics engines, by integer z coordinate. */ + readonly enginesByZ: Map<number, MatterJS.Engine> = new Map(); + + /** A mapping from output names to body IDs */ + private bodyByName: Map<string, OutputBody> = new Map(); + + /** The latest stage synced */ + private stage: Stage | undefined = undefined; + + /** The hash of the barriers previously added */ + private previousShapes: Shape[] = []; + private currentShapeBodies: { + body: MatterJS.Body; + engine: MatterJS.Engine; + }[] = []; + + constructor(evaluator: Evaluator) { + this.evaluator = evaluator; + } + + getEngineAtZ(z: number) { + let engine = this.enginesByZ.get(Math.round(z)); + + // No engine yet for this depth? Make one. + if (engine === undefined) { + // A Matter JS engine for managing physics on output. + engine = MatterJS.Engine.create({ + positionIterations: 10, + velocityIterations: 8, + // Originally had sleeping enabled, but it prevented collisions from happening on sleeping bodies. + // In the future, could reenable it and wake up bodies in ways to avoid this collision loss. + enableSleeping: false, + }); + + // Set timing to match animation factor + const animationFactor = + this.evaluator.database.Settings.settings.animationFactor.get(); + if (animationFactor > 0) + engine.timing.timeScale = 1 / animationFactor; + + // Set the engine. + this.enginesByZ.set(z, engine); + + // Cap velocity + const limitMaxSpeed = () => { + for (const { body } of this.bodyByName.values()) { + const maxSpeed = 100; + if (body.velocity.x > maxSpeed) { + MatterJS.Body.setVelocity(body, { + x: maxSpeed, + y: body.velocity.y, + }); + } + + if (body.velocity.x < -maxSpeed) { + MatterJS.Body.setVelocity(body, { + x: -maxSpeed, + y: body.velocity.y, + }); + } + + if (body.velocity.y > maxSpeed) { + MatterJS.Body.setVelocity(body, { + x: body.velocity.x, + y: maxSpeed, + }); + } + + if (body.velocity.y < -maxSpeed) { + MatterJS.Body.setVelocity(body, { + x: -body.velocity.x, + y: -maxSpeed, + }); + } + } + }; + MatterJS.Events.on(engine, 'beforeUpdate', limitMaxSpeed); + + // Listen for collision starts. + MatterJS.Events.on(engine, 'collisionStart', (event) => { + this.report( + event.pairs.map((pair) => { + return { + subject: pair.bodyA.label, + object: pair.bodyB.label, + direction: pair.collision.penetration, + starting: true, + }; + }) + ); + }); + + // Listen for collision ends. + MatterJS.Events.on(engine, 'collisionEnd', (event) => { + this.report( + event.pairs.map((pair) => { + return { + subject: pair.bodyA.label, + object: pair.bodyB.label, + direction: pair.collision.penetration, + starting: false, + }; + }) + ); + }); + } + + // Return the engine. + return engine; + } + + /** Rotation is degrees */ + createRectangle(rectangle: Rectangle, rotation: number | undefined) { + // Compute rectangle boundaries in engine coordinates. + const left = rectangle.getLeft() * PX_PER_METER; + const right = + (rectangle.getLeft() + rectangle.getWidth()) * PX_PER_METER; + const top = -rectangle.getTop() * PX_PER_METER; + const bottom = + -(rectangle.getTop() - rectangle.getHeight()) * PX_PER_METER; + + // Place the rectangle at the center of bounds + const rect = MatterJS.Bodies.rectangle( + (left + right) / 2, + (top + bottom) / 2, + Math.abs(right - left), + Math.abs(bottom - top), + { + isStatic: true, + collisionFilter: { + group: 1, + category: ShapeCategory, + mask: TextCategory | ShapeCategory, + }, + } + ); + + if (rotation !== undefined && rotation !== 0) + MatterJS.Body.rotate(rect, (rotation * Math.PI) / 180); + + return rect; + } + + /** Given the current and prior scenes, and the time elapsed since the last one, sync the matter engine. */ + sync(stage: Stage, current: OutputInfoSet, exiting: OutputsByName) { + // Update the stage + this.stage = stage; + + // REMOVE all of the exited outputs from the engine. + for (const name of exiting.keys()) this.removeOutputBody(name); + + // CREATE and UPDATE bodies for all outputs currently in the scene. + + // Create an index of all of the Motion stream's recent places so we can map output's places back to motion streams. + const motionByPlace = new Map<Value, Motion>(); + for (const motion of this.evaluator.getBasisStreamsOfType(Motion)) { + for (const value of motion.values.slice(-10)) + motionByPlace.set(value.value, motion); + } + + // Iterate through all of the output in the current scene. + for (const [name, info] of current) { + // Is it inside a group? Pass. We only include top level elements in physics, since there's a single coordinate system that they share. + // Groups have their own local coordinate systems. + if (info.parents[0] instanceof Group) { + continue; + } + + // Is it a stage? Update all engine's gravity based on the stage's latest value. + if (info.output instanceof Stage) { + // Set the gravity to the Stage's gravity setting. + for (const engine of this.enginesByZ.values()) { + // The scale is the pixels per meter + engine.gravity.scale = 1 / PX_PER_METER; + engine.gravity.y = + (info.output.gravity ?? DefaultGravity) / 10; + } + } + // Other kind of output? Sync it. + else { + // Does the output have matter? + const matter = + info.output instanceof Phrase || + info.output instanceof Group + ? info.output.matter + : undefined; + + // Is there a motion stream responsible for this output's place? The motion may + const motion = info.output.place + ? motionByPlace.get(info.output.place.value) + : undefined; + + // If the output has matter or is in motion, make sure it's in the MatterJS world. + if (matter || motion) { + // Get the body that we already made using it's name. + let shape = this.bodyByName.get(name); + + // Doesn't exist or changed size? Make one and add it to the MatterJS world + // for it's z value. + if ( + shape === undefined || + shape.width !== info.width || + shape.height !== info.height + ) { + // Get the engine for this z depth + const engine = this.getEngineAtZ(info.global.z); + + // If we already had a body, remove it so we can replace with a new one. + if (shape !== undefined) { + this.removeOutputBody(name); + } + + // Make a new body for this new output. + shape = this.createOutputBody(info, matter); + + // Remember the body by name + this.bodyByName.set(name, shape); + + // Add to the MatterJS world + MatterJS.Composite.add(engine.world, shape.body); + } + + // Does the output have no motion but does have matter? Move it to its latest position and apply a velocity. + if (motion === undefined) { + // const previousPlace = shape.getPlace(); + + MatterJS.Body.setPosition( + shape.body, + shape.getPosition( + info.global.x, + info.global.y, + info.width, + info.height + ) + ); + // const delta = { + // x: PX_PER_METER * (info.global.x - previousPlace.x), + // y: PX_PER_METER * (info.global.y - previousPlace.y), + // }; + // MatterJS.Body.applyForce(); + // MatterJS.Body.setVelocity(shape.body, delta); + } + + // Did we make or find a corresponding body? Apply any Place or Velocity overrides in the Motion. + if (motion) motion.updateBody(shape); + + // Set the body's current angle if it has one, otherwise leave it alone. + if (info.output.pose.rotation !== undefined) + MatterJS.Body.setAngle( + shape.body, + (info.output.pose.rotation * Math.PI) / 180 + ); + + // Set matter properties if available. + if (matter) { + MatterJS.Body.setMass(shape.body, matter.mass); + shape.body.restitution = matter.bounciness; + shape.body.friction = matter.friction; + } + + // Set the collision filter based on the matter settings. + shape.body.collisionFilter = getCollisionFilter(matter); + + // If no motion, set inertia to infinity, since the output is immovable. + MatterJS.Body.setInertia(shape.body, Infinity); + } + // No motion or matter? Remove it from the MatterJS world so it doesn't mess with collisions. + else { + this.removeOutputBody(name); + } + } + } + + // Sync barriers. + if (this.stage) { + const shapes = this.stage.getShapes(); + + // If the shapes changed update them in the engine. + if ( + this.previousShapes.length !== shapes.length || + !this.previousShapes.every((shape, index) => + shape.value.isEqualTo(shapes[index].value) + ) + ) { + // Remove the bodies previously added + for (const record of this.currentShapeBodies) + MatterJS.Composite.remove(record.engine.world, record.body); + + // Add the revised bodies + this.currentShapeBodies = []; + for (const barrier of shapes) { + if (barrier.form instanceof Rectangle) { + const shape = this.createRectangle( + barrier.form, + barrier.pose.rotation + ); + if (barrier.name) shape.label = barrier.getName(); + const engine = this.getEngineAtZ(barrier.form.z); + MatterJS.Composite.add(engine.world, shape); + this.currentShapeBodies.push({ + body: shape, + engine: engine, + }); + } + } + + // Remember what we added. + this.previousShapes = shapes; + } + } + } + + tick(elapsed: number) { + // UPDATE all the engines forward by the duration that has elapsed with the new arrangement. + // Only do this if we haven't done it for the current delta. + if ( + this.evaluator.database.Settings.settings.animationFactor.get() > 0 + ) { + for (const engine of this.enginesByZ.values()) + MatterJS.Engine.update(engine, elapsed); + } + } + + removeOutputBody(name: string) { + const outputBody = this.bodyByName.get(name); + if (outputBody) { + // Search through the engines and find the one with the body to remove. + for (const [z, engine] of this.enginesByZ) { + if (MatterJS.World.allBodies) { + // Get the bodies in this engine. + const bodies = MatterJS.World.allBodies(engine.world); + // If this world contains the body, remove it from the engine. + if (bodies.some((body) => body === outputBody.body)) { + // Remove the body. + MatterJS.Composite.remove( + engine.world, + outputBody.body + ); + this.bodyByName.delete(name); + + // If the engine is now empty, remove the engine. + if (bodies.length === 1) { + this.stopEngine(engine); + this.enginesByZ.delete(z); + } + + return; + } + } + } + } + } + + createOutputBody(info: OutputInfo, matter: Matter | undefined) { + const { width, height, ascent } = info.output.getLayout(info.context); + return new OutputBody( + info.output.getName(), + info.global.x, + info.global.y, + width, + height, + ascent, + ((info.output.pose.rotation ?? 0) * Math.PI) / 180, + // Round corners by a fraction of their size + (matter?.roundedness ?? 0.1) * + (info.output.size ?? info.context.size), + matter + ); + } + + /** Get the body corresponding to the given output name */ + getOutputBody(name: string): OutputBody | undefined { + return this.bodyByName.get(name); + } + + /** Report the given collisions to the evaluator's collision streams */ + report(rebounds: ReboundEvent[]) { + // Get the streams. + const collisions = this.evaluator.getBasisStreamsOfType(Collision); + + for (const collision of collisions) { + for (const rebound of rebounds) collision.react(rebound); + } + } + + stop() { + // Clear the physics engines. + for (const engine of this.enginesByZ.values()) { + this.stopEngine(engine); + } + } + + stopEngine(engine: MatterJS.Engine) { + MatterJS.World.clear(engine.world, false); + MatterJS.Engine.clear(engine); + } +} + +/** This Matter.Body wrapper helps us remember width and height, avoiding redundant computation. */ +export class OutputBody { + readonly body: MatterJS.Body; + readonly width: number; + readonly height: number; + constructor( + name: string, + left: number, + bottom: number, + width: number, + height: number, + ascent: number, + angle: number, + corner: number, + matter: Matter | undefined + ) { + const position = this.getPosition(left, bottom, width, height); + + this.body = MatterJS.Bodies.rectangle( + position.x, + position.y, + PX_PER_METER * width, + PX_PER_METER * ascent, + // Round corners by a fraction of their size + { + chamfer: { + radius: corner * PX_PER_METER, + }, + restitution: matter?.bounciness ?? 0, + friction: matter?.friction ?? 0, + mass: (matter?.mass ?? 1) * 10, + angle, + sleepThreshold: 500, + label: name, + } + ); + + this.body.collisionFilter = getCollisionFilter(matter); + + this.width = width; + this.height = height; + } + + /** Convert a Place position into a stage Place. */ + getPosition(left: number, bottom: number, width: number, height: number) { + return { + // Body center is half the width from left + x: PX_PER_METER * (left + width / 2), + // Negate top to flip y-axes than add half of height to get center + y: PX_PER_METER * -(bottom + height / 2), + }; + } + + /** Convert the MatterJS position into stage Place values. */ + getPlace() { + return { + x: this.body.position.x / PX_PER_METER - this.width / 2, + y: -this.body.position.y / PX_PER_METER - this.height / 2, + angle: + (isNaN(this.body.angle) || + this.body.angle === Infinity || + this.body.angle === -Infinity + ? 0 + : this.body.angle * 180) / Math.PI, + }; + } +} + +/** Abstract away MatterJS's confusing collision filtering system. */ +function getCollisionFilter(matter: Matter | undefined) { + return matter + ? { + group: 0, + category: TextCategory, + mask: + (matter.text ? TextCategory : 0) | + (matter.shapes ? ShapeCategory : 0), + } + : { + group: -1, + category: TextCategory, + mask: 0, + }; +} diff --git a/src/output/Place.ts b/src/output/Place.ts index e60355fda..296e827e3 100644 --- a/src/output/Place.ts +++ b/src/output/Place.ts @@ -1,7 +1,7 @@ import toStructure from '../basis/toStructure'; import type Value from '@values/Value'; import { getBind } from '@locale/getBind'; -import Output, { getOutputInputs } from './Output'; +import Valued, { getOutputInputs } from './Valued'; import { toNumber } from './Stage'; import type Evaluator from '@runtime/Evaluator'; import type Names from '../nodes/Names'; @@ -10,6 +10,7 @@ import Evaluation from '@runtime/Evaluation'; import StructureValue from '../values/StructureValue'; import Unit from '../nodes/Unit'; import type Locale from '../locale/Locale'; +import NoneValue from '../values/NoneValue'; export function createPlaceType(locales: Locale[]) { return toStructure(` @@ -17,21 +18,30 @@ export function createPlaceType(locales: Locale[]) { ${getBind(locales, (locale) => locale.output.Place.x)}•#m: 0m ${getBind(locales, (locale) => locale.output.Place.y)}•#m: 0m ${getBind(locales, (locale) => locale.output.Place.z)}•#m: 0m + ${getBind(locales, (locale) => locale.output.Place.rotation)}•#°|ø: ø ) `); } -export default class Place extends Output { +export default class Place extends Valued { readonly x: number; readonly y: number; readonly z: number; - - constructor(value: Value, x: number, y: number, z: number) { + readonly rotation: number | undefined; + + constructor( + value: Value, + x: number, + y: number, + z: number, + rotation?: number | undefined + ) { super(value); this.x = x; this.y = y; this.z = z; + this.rotation = rotation; } /** Adds the given place's x and y to this Place's x and y (but leaves z and rotation alone) */ @@ -40,7 +50,8 @@ export default class Place extends Output { this.value, this.x + place.x, this.y - place.y, - this.z + this.z, + this.rotation ); } @@ -49,7 +60,16 @@ export default class Place extends Output { this.value, this.x - place.x, this.y + place.y, - this.z + this.z, + this.rotation + ); + } + + distanceFrom(place: Place) { + return Math.sqrt( + Math.pow(place.x - this.x, 2) + + Math.pow(place.y - this.y, 2) + + Math.pow(place.z - this.z, 2) ); } @@ -65,12 +85,13 @@ export default class Place extends Output { export function toPlace(value: Value | undefined): Place | undefined { if (!(value instanceof StructureValue)) return undefined; - const [xVal, yVal, zVal] = getOutputInputs(value); + const [xVal, yVal, zVal, rotationVal] = getOutputInputs(value); const x = toNumber(xVal); const y = toNumber(yVal); const z = toNumber(zVal); + const rotation = toNumber(rotationVal); return x !== undefined && y !== undefined && z !== undefined - ? new Place(value, x, y, z) + ? new Place(value, x, y, z, rotation) : undefined; } @@ -80,14 +101,15 @@ export function createPlace( y: number, z: number ): Place { - return new Place(createPlaceStructure(evaluator, x, y, z), x, y, z); + return new Place(createPlaceStructure(evaluator, x, y, z), x, y, z, 0); } export function createPlaceStructure( evaluator: Evaluator, x: number, y: number, - z: number + z: number, + rotation?: number | undefined ): StructureValue { const creator = evaluator.getMain(); @@ -105,6 +127,12 @@ export function createPlaceStructure( PlaceType.inputs[2].names, new NumberValue(creator, z, Unit.reuse(['m'])) ); + place.set( + PlaceType.inputs[3].names, + rotation !== undefined + ? new NumberValue(creator, rotation, Unit.reuse(['°'])) + : new NoneValue(creator) + ); const evaluation = new Evaluation( evaluator, diff --git a/src/output/Pose.ts b/src/output/Pose.ts index e17936ea6..0656c9192 100644 --- a/src/output/Pose.ts +++ b/src/output/Pose.ts @@ -2,7 +2,7 @@ import toStructure from '@basis/toStructure'; import StructureValue from '@values/StructureValue'; import type Value from '@values/Value'; import type Color from './Color'; -import Output, { getOutputInputs } from './Output'; +import Valued, { getOutputInputs } from './Valued'; import type Place from './Place'; import { toPlace } from './Place'; import { toBoolean, toNumber } from './Stage'; @@ -28,7 +28,7 @@ export function createPoseType(locales: Locale[]) { `); } -export default class Pose extends Output { +export default class Pose extends Valued { readonly color?: Color; readonly opacity?: number; readonly offset?: Place; diff --git a/src/output/Rebound.ts b/src/output/Rebound.ts new file mode 100644 index 000000000..d08708223 --- /dev/null +++ b/src/output/Rebound.ts @@ -0,0 +1,38 @@ +import toStructure from '../basis/toStructure'; +import { getBind } from '@locale/getBind'; +import type Evaluator from '@runtime/Evaluator'; +import type { EvaluationNode } from '@runtime/Evaluation'; +import StructureValue from '../values/StructureValue'; +import type Locale from '../locale/Locale'; +import { createDirectionStructure } from './Direction'; +import TextValue from '../values/TextValue'; + +export function createReboundType(locales: Locale[]) { + return toStructure(` + ${getBind(locales, (locale) => locale.input.Rebound, '•')}( + ${getBind(locales, (locale) => locale.input.Rebound.subject)}•'' + ${getBind(locales, (locale) => locale.input.Rebound.object)}•'' + ${getBind( + locales, + (locale) => locale.input.Rebound.direction + )}•Direction + ) +`); +} + +export function createReboundStructure( + evaluator: Evaluator, + creator: EvaluationNode, + subject: string, + object: string, + direction: { x: number; y: number } +): StructureValue { + return StructureValue.make( + evaluator, + creator, + evaluator.project.shares.output.Rebound, + new TextValue(creator, subject), + new TextValue(creator, object), + createDirectionStructure(evaluator, creator, direction.x, direction.y) + ); +} diff --git a/src/output/Row.ts b/src/output/Row.ts index c22f7c7a5..e61f17401 100644 --- a/src/output/Row.ts +++ b/src/output/Row.ts @@ -1,7 +1,7 @@ import toStructure from '../basis/toStructure'; import type Value from '@values/Value'; import type Color from './Color'; -import type TypeOutput from './TypeOutput'; +import type Output from './Output'; import type RenderContext from './RenderContext'; import Place from './Place'; import { getBind } from '@locale/getBind'; @@ -11,7 +11,7 @@ import Group from './Group'; import Phrase from './Phrase'; import concretize from '../locale/concretize'; import type Locale from '../locale/Locale'; -import { getOutputInput } from './Output'; +import { getOutputInput } from './Valued'; import StructureValue from '../values/StructureValue'; export function createRowType(locales: Locale[]) { @@ -35,7 +35,7 @@ export class Row extends Arrangement { this.padding = padding.toNumber(); } - getLayout(children: (TypeOutput | null)[], context: RenderContext) { + getLayout(children: (Output | null)[], context: RenderContext) { // Layout the children. const layouts = children.map((child) => child === null ? null : child.getLayout(context) @@ -60,7 +60,7 @@ export class Row extends Arrangement { top = 0, right = 0, bottom = 0; - const positions: [TypeOutput, Place][] = []; + const positions: [Output, Place][] = []; // Layout each child from start to end. for (const child of layouts) { if (child) { @@ -111,7 +111,7 @@ export class Row extends Arrangement { return undefined; } - getDescription(output: TypeOutput[], locales: Locale[]) { + getDescription(output: Output[], locales: Locale[]) { return concretize( locales[0], locales[0].output.Row.description, diff --git a/src/output/Scene.ts b/src/output/Scene.ts index 8f059bded..afc99098a 100644 --- a/src/output/Scene.ts +++ b/src/output/Scene.ts @@ -1,4 +1,4 @@ -import type TypeOutput from './TypeOutput'; +import type Output from './Output'; import Place from './Place'; import { createPlace } from './Place'; import Stage from './Stage'; @@ -7,28 +7,34 @@ import type Transition from './Transition'; import type Node from '@nodes/Node'; import type RenderContext from './RenderContext'; import type Evaluator from '@runtime/Evaluator'; +import Sequence from './Sequence'; +import Pose from './Pose'; +import type Value from '../values/Value'; +import Physics from './Physics'; export type OutputName = string; export type OutputInfo = { - output: TypeOutput; + output: Output; global: Place; local: Place; rotation: number | undefined; - parents: TypeOutput[]; + width: number; + height: number; + parents: Output[]; context: RenderContext; }; export type Moved = Map< OutputName, { - output: TypeOutput; + output: Output; prior: Orientation; present: Orientation; } >; -export type OutputsByName = Map<OutputName, TypeOutput>; +export type OutputsByName = Map<OutputName, Output>; export type OutputInfoSet = Map<OutputName, OutputInfo>; export type Orientation = { place: Place; rotation: number | undefined }; @@ -65,6 +71,9 @@ export default class Scene { scene: OutputInfoSet = new Map<OutputName, OutputInfo>(); priorScene: OutputInfoSet = new Map<OutputName, OutputInfo>(); + /** The current outputs by their corresponding values */ + outputByPlace: Map<Value, Output[]> = new Map(); + /** The active animations, responsible for tracking transitions and animations on named output. */ readonly animations = new Map<OutputName, OutputAnimation>(); @@ -83,18 +92,25 @@ export default class Scene { /** If true, the scene has been stopped and will no longer be animated */ private stopped = false; + /** A physics engine for managing motion and collisions of output. */ + readonly physics: Physics; + constructor( evaluator: Evaluator, exit: (name: OutputName) => void, tick: (nodes: Set<Node>) => void ) { this.evaluator = evaluator; + evaluator.scene = this; + this.exit = exit; this.tick = tick; // Initialize unintialized defaults. this.focus = createPlace(this.evaluator, 0, 0, -6); this.priorStagePlace = this.focus; + + this.physics = new Physics(evaluator); } /** @@ -125,7 +141,7 @@ export default class Scene { const exited: OutputsByName = new Map(); const present: OutputsByName = new Map(); - // Add the verse to the scene. This is necessary so that animations can get its context. + // Add the stage to the scene. This is necessary so that animations can get its context. const newScene = this.layout( this.stage, [], @@ -139,6 +155,8 @@ export default class Scene { // We keep these at the center for cacluations, but use the focus place below to detect movement. global: center, local: center, + width: 0, + height: 0, rotation: stage.pose.rotation, parents: [], context, @@ -185,7 +203,7 @@ export default class Scene { } // A mapping from exiting groups to where they previously were. - const exiting = new Map<OutputName, OutputInfo>(); + const exiting: OutputInfoSet = new Map(); // Now that we have a list of everyone present, remove everyone that was present in the prior scene that is no longer, and note that they exited. // We only do this if this is an animated stage; exiting isn't animated on non-live stages. @@ -203,6 +221,8 @@ export default class Scene { output, global: place, local: place, + width: info.width, + height: info.height, rotation: info.rotation, context: info.context, parents: [this.stage], @@ -223,6 +243,23 @@ export default class Scene { this.priorScene = this.scene; this.scene = newScene; + // Remember a mapping between Place values and outputs so that Motion can + // map back to the outputs it created Places for, and update their corresponding Bodies. + this.outputByPlace = new Map(); + for (const [, output] of this.scene) { + if (output.output.place) { + const outputs = + this.outputByPlace.get(output.output.place.value) ?? []; + this.outputByPlace.set(output.output.place.value, [ + ...outputs, + output.output, + ]); + } + } + + // Sync this scene with the Matter engine. + this.physics.sync(this.stage, this.scene, exited); + // Return the layout for rendering. return { entered, @@ -243,17 +280,17 @@ export default class Scene { * deletion. */ animate( - present: Map<OutputName, TypeOutput>, - entered: Map<OutputName, TypeOutput>, + present: Map<OutputName, Output>, + entered: Map<OutputName, Output>, moved: Map< OutputName, { - output: TypeOutput; + output: Output; prior: Orientation; present: Orientation; } >, - exited: Map<OutputName, TypeOutput> + exited: Map<OutputName, Output> ): Set<OutputName> { if (this.stopped) return new Set(); @@ -302,6 +339,8 @@ export default class Scene { stop() { this.stopped = true; this.animations.forEach((animation) => animation.exited()); + + this.physics.stop(); } exited(animation: OutputAnimation) { @@ -328,8 +367,8 @@ export default class Scene { // A top down layout algorithm that places groups first, then their subgroups, and uses the // ancestor list to compute global places for each group. layout( - output: TypeOutput, - parents: TypeOutput[], + output: Output, + parents: Output[], outputInfo: Map<OutputName, OutputInfo>, context: RenderContext ) { @@ -343,14 +382,25 @@ export default class Scene { const info = outputInfo.get(name); // Get this output's place, so we can offset its subgroups. const parentPlace = info?.global; + // Get the layout of the output + const layout = output.getLayout(context); + // Update the info's width and height + if (info) { + info.width = layout.width; + info.height = layout.height; + } + // Get the places of each of this group's subgroups. - for (const [subgroup, place] of output.getLayout(context).places) { + for (const [subgroup, place] of layout.places) { // Set the place of this subgroup, offseting it by the parent's position to keep it in global coordinates. outputInfo.set(subgroup.getName(), { output: subgroup, local: place, global: parentPlace ? place.offset(parentPlace) : place, rotation: info?.rotation, + // These dimensions will be set in the recursive call below. + width: 0, + height: 0, context, parents: parents.slice(), }); @@ -362,4 +412,35 @@ export default class Scene { return outputInfo; } + + /** Computes velocity in meters per second. */ + getOutputVelocity( + name: string, + output: OutputInfo, + prior: OutputInfoSet, + secondsElapsed: number + ) { + const currentPlace = output.global; + const priorPlace = prior.get(name)?.global; + + // If we don't know where it was before, it has no velocity + if (priorPlace === undefined) return undefined; + + // If the output has a moving sequence, use it's duration. If it has a pose, use the phrase's duration. Otherwise, use the given duration between frames. + const duration = + output.output.moving instanceof Sequence + ? output.output.moving.duration + : output.output.moving instanceof Pose + ? output.output.duration + : secondsElapsed; + + return { + vx: (currentPlace.x - priorPlace.x) / duration, + vy: (currentPlace.y - priorPlace.y) / duration, + }; + } + + getOutputByPlace(value: Value) { + return this.outputByPlace.get(value); + } } diff --git a/src/output/Sequence.ts b/src/output/Sequence.ts index 32e2502fa..9e5acaae9 100644 --- a/src/output/Sequence.ts +++ b/src/output/Sequence.ts @@ -3,7 +3,7 @@ import { TYPE_SYMBOL } from '@parser/Symbols'; import StructureValue from '@values/StructureValue'; import type Value from '@values/Value'; import { getBind } from '@locale/getBind'; -import Output, { getOutputInputs } from './Output'; +import Valued, { getOutputInputs } from './Valued'; import type Pose from './Pose'; import { toPose } from './Pose'; import { toDecimal } from './Stage'; @@ -47,7 +47,7 @@ export function createSequenceType(locales: Locale[]) { type SequenceStep = { percent: number; pose: Pose }; -export default class Sequence extends Output { +export default class Sequence extends Valued { readonly count: number; readonly poses: SequenceStep[]; readonly duration: number; diff --git a/src/output/Shape.ts b/src/output/Shape.ts new file mode 100644 index 000000000..ea9eb7db7 --- /dev/null +++ b/src/output/Shape.ts @@ -0,0 +1,197 @@ +import toStructure from '../basis/toStructure'; +import { TYPE_SYMBOL } from '../parser/Symbols'; +import StructureValue from '../values/StructureValue'; +import { getBind } from '../locale/getBind'; +import type Locale from '../locale/Locale'; +import type Color from './Color'; +import Output, { DefaultStyle } from './Output'; +import type TextLang from './TextLang'; +import type { DefinitePose } from './Pose'; +import type Pose from './Pose'; +import type Sequence from './Sequence'; +import { Form, toRectangle } from './Form'; +import type Project from '../models/Project'; +import type Value from '../values/Value'; +import type { NameGenerator } from './Stage'; +import { getOutputInput } from './Valued'; +import { getStyle } from './toOutput'; +import Place from './Place'; + +export function createShapeType(locales: Locale[]) { + return toStructure(` + ${getBind(locales, (locale) => locale.output.Shape, TYPE_SYMBOL)} Output( + ${getBind(locales, (locale) => locale.output.Shape.form)}•Rectangle + ${getBind(locales, (locale) => locale.output.Shape.name)}•""|ø: ø + ${getBind(locales, (locale) => locale.output.Shape.selectable)}•?: ⊥ + ${getBind(locales, (locale) => locale.output.Shape.color)}•🌈${'|ø: ø'} + ${getBind( + locales, + (locale) => locale.output.Shape.background + )}•Color${'|ø: ø'} + ${getBind(locales, (locale) => locale.output.Shape.opacity)}•%${'|ø: ø'} + ${getBind(locales, (locale) => locale.output.Shape.offset)}•📍|ø: ø + ${getBind( + locales, + (locale) => locale.output.Phrase.rotation + )}•#°${'|ø: ø'} + ${getBind(locales, (locale) => locale.output.Shape.scale)}•#${'|ø: ø'} + ${getBind(locales, (locale) => locale.output.Shape.flipx)}•?${'|ø: ø'} + ${getBind(locales, (locale) => locale.output.Shape.flipy)}•?${'|ø: ø'} + ${getBind(locales, (locale) => locale.output.Shape.entering)}•ø|🤪|💃: ø + ${getBind(locales, (locale) => locale.output.Shape.resting)}•ø|🤪|💃: ø + ${getBind(locales, (locale) => locale.output.Shape.moving)}•ø|🤪|💃: ø + ${getBind(locales, (locale) => locale.output.Shape.exiting)}•ø|🤪|💃: ø + ${getBind(locales, (locale) => locale.output.Shape.duration)}•#s: 0.25s + ${getBind(locales, (locale) => locale.output.Shape.style)}•${locales + .map((locale) => + Object.values(locale.output.Easing).map((id) => `"${id}"`) + ) + .flat() + .join('|')}: "${DefaultStyle}" + ) +`); +} + +export function createRectangleType(locales: Locale[]) { + return toStructure(` + ${getBind(locales, (locale) => locale.output.Rectangle, TYPE_SYMBOL)}( + ${getBind(locales, (locale) => locale.output.Rectangle.left)}•#m + ${getBind(locales, (locale) => locale.output.Rectangle.top)}•#m + ${getBind(locales, (locale) => locale.output.Rectangle.right)}•#m + ${getBind(locales, (locale) => locale.output.Rectangle.bottom)}•#m + ${getBind(locales, (locale) => locale.output.Rectangle.z)}•#m: 0m + ) +`); +} + +export default class Shape extends Output { + readonly form: Form; + + constructor( + value: StructureValue, + form: Form, + name: TextLang | string, + selectable: boolean, + background: Color | undefined, + pose: DefinitePose, + entering: Pose | Sequence | undefined = undefined, + resting: Pose | Sequence | undefined = undefined, + moving: Pose | Sequence | undefined = undefined, + exiting: Pose | Sequence | undefined = undefined, + duration: number, + style: string + ) { + super( + value, + 0, + undefined, + new Place( + value, + form.getLeft(), + // We render all output from the baseline + form.getTop() - form.getHeight(), + form.getZ() + ), + name, + selectable, + background, + pose, + entering, + resting, + moving, + exiting, + duration, + style + ); + + this.form = form; + } + + find() { + return undefined; + } + + getOutput(): Output[] { + return []; + } + + getLayout() { + const left = this.form.getLeft(); + const top = this.form.getTop(); + const width = this.form.getWidth(); + const height = this.form.getHeight(); + + return { + output: this, + left, + right: left + width, + top, + bottom: top - height, + width, + height, + ascent: height, + descent: 9, + places: [], + }; + } + + getBackground(): Color | undefined { + return this.background; + } + + getShortDescription(locales: Locale[]) { + return this.getDescription(locales); + } + + getDescription(locales: Locale[]) { + return this.form.getDescription(locales); + } + + isEmpty() { + return false; + } +} + +export function toShape( + project: Project, + value: Value | undefined, + namer: NameGenerator +): Shape | undefined { + if (!(value instanceof StructureValue)) return undefined; + + const form = toRectangle(getOutputInput(value, 0)); + + const { + name, + selectable, + background, + pose, + resting: rest, + entering: enter, + moving: move, + exiting: exit, + duration, + style, + } = getStyle(project, value, 1); + + return form instanceof Form && + pose && + selectable !== undefined && + duration !== undefined && + style !== undefined + ? new Shape( + value, + form, + namer.getName(name?.text, value), + selectable, + background, + pose, + enter, + rest, + move, + exit, + duration, + style + ) + : undefined; +} diff --git a/src/output/Stack.ts b/src/output/Stack.ts index a9db4b206..b14d7edfb 100644 --- a/src/output/Stack.ts +++ b/src/output/Stack.ts @@ -1,7 +1,7 @@ import toStructure from '../basis/toStructure'; import type Value from '@values/Value'; import type Color from './Color'; -import type TypeOutput from './TypeOutput'; +import type Output from './Output'; import type RenderContext from './RenderContext'; import Place from './Place'; import { getBind } from '@locale/getBind'; @@ -11,7 +11,7 @@ import Phrase from './Phrase'; import Group from './Group'; import concretize from '../locale/concretize'; import type Locale from '../locale/Locale'; -import { getOutputInput } from './Output'; +import { getOutputInput } from './Valued'; import StructureValue from '../values/StructureValue'; import Decimal from 'decimal.js'; @@ -36,7 +36,7 @@ export class Stack extends Arrangement { this.alignment = align === 0 ? 0 : align < 0 ? -1 : 1; } - getLayout(children: (TypeOutput | null)[], context: RenderContext) { + getLayout(children: (Output | null)[], context: RenderContext) { // Get the layouts of the children const layouts = children.map((child) => child ? child.getLayout(context) : null @@ -62,7 +62,7 @@ export class Stack extends Arrangement { // Start at the top and work our way down. let y = new Decimal(height); - const places: [TypeOutput, Place][] = []; + const places: [Output, Place][] = []; let left = 0, bottom = 0, right = 0, @@ -118,7 +118,7 @@ export class Stack extends Arrangement { return undefined; } - getDescription(output: TypeOutput[], locales: Locale[]) { + getDescription(output: Output[], locales: Locale[]) { return concretize( locales[0], locales[0].output.Stack.description, diff --git a/src/output/Stage.ts b/src/output/Stage.ts index 9dd087b7d..d487e7c3d 100644 --- a/src/output/Stage.ts +++ b/src/output/Stage.ts @@ -1,6 +1,6 @@ import StructureValue from '@values/StructureValue'; import type Value from '@values/Value'; -import TypeOutput, { DefaultStyle } from './TypeOutput'; +import Output, { DefaultStyle } from './Output'; import type RenderContext from './RenderContext'; import Color from './Color'; import Place from './Place'; @@ -10,27 +10,30 @@ import Decimal from 'decimal.js'; import ListValue from '@values/ListValue'; import { getBind } from '@locale/getBind'; import BoolValue from '@values/BoolValue'; -import { getStyle, toTypeOutput, toTypeOutputList } from './toTypeOutput'; +import { getTypeStyle, toOutput, toOutputList } from './toOutput'; import TextLang from './TextLang'; import Pose, { DefinitePose } from './Pose'; import type Sequence from './Sequence'; -import { toShape, type Shape } from './Shapes'; import concretize from '../locale/concretize'; import type Locale from '../locale/Locale'; import type Project from '../models/Project'; -import { getOutputInput } from './Output'; +import { getOutputInput } from './Valued'; import { SupportedFontsFamiliesType, type SupportedFace } from '../basis/Fonts'; import { getFirstName } from '../locale/Locale'; +import { toRectangle, type Rectangle } from './Form'; +import Shape from './Shape'; + +export const DefaultGravity = 9.8; export const CSSFallbackFaces = `"Noto Color Emoji"`; export const DefaultSize = 1; export function createStageType(locales: Locale[]) { return toStructure(` - ${getBind(locales, (locale) => locale.output.Stage, '•')} Type( - ${getBind(locales, (locale) => locale.output.Stage.content)}•[Type] - ${getBind(locales, (locale) => locale.output.Stage.frame)}•Shape|ø: ø - ${getBind(locales, (locale) => locale.output.Stage.size)}•${'#m: 1m'} + ${getBind(locales, (locale) => locale.output.Stage, '•')} Output( + ${getBind(locales, (locale) => locale.output.Stage.content)}•[Output] + ${getBind(locales, (locale) => locale.output.Stage.frame)}•Rectangle|ø: ø + ${getBind(locales, (locale) => locale.output.Stage.size)}•${'#m: 1m'} ${getBind( locales, (locale) => locale.output.Stage.face @@ -63,25 +66,30 @@ export function createStageType(locales: Locale[]) { ) .flat() .join('|')}: "${DefaultStyle}" + ${getBind( + locales, + (locale) => locale.output.Stage.gravity + )}•#m/s^2: ${DefaultGravity}m/s^2 ) `); } -export default class Stage extends TypeOutput { +export default class Stage extends Output { /** True if the stage was explicit in the program or generated to wrap some other content. */ readonly explicit: boolean; - readonly content: (TypeOutput | null)[]; - readonly frame: Shape | undefined; + readonly content: (Output | null)[]; + readonly frame: Rectangle | undefined; readonly back: Color; + readonly gravity: number; private _description: string | undefined = undefined; constructor( value: Value, explicit: boolean, - content: (TypeOutput | null)[], + content: (Output | null)[], background: Color, - frame: Shape | undefined = undefined, + frame: Rectangle | undefined = undefined, size: number, face: SupportedFace, place: Place | undefined = undefined, @@ -93,7 +101,8 @@ export default class Stage extends TypeOutput { moveing: Pose | Sequence | undefined = undefined, exiting: Pose | Sequence | undefined = undefined, duration = 0, - style: string | undefined = 'zippy' + style: string | undefined = 'zippy', + gravity: number ) { super( value, @@ -116,13 +125,20 @@ export default class Stage extends TypeOutput { this.content = content; this.frame = frame; this.back = background; + this.gravity = gravity; } getOutput() { return this.content; } - find(check: (output: TypeOutput) => boolean): TypeOutput | undefined { + getShapes() { + return this.content.filter( + (shape): shape is Shape => shape instanceof Shape + ); + } + + find(check: (output: Output) => boolean): Output | undefined { for (const output of this.content) { if (output !== null) { if (check(output)) return output; @@ -132,7 +148,7 @@ export default class Stage extends TypeOutput { } getLayout(context: RenderContext) { - const places: [TypeOutput, Place][] = []; + const places: [Output, Place][] = []; let left = 0, right = 0, bottom = 0, @@ -140,17 +156,20 @@ export default class Stage extends TypeOutput { for (const child of this.content) { if (child) { const layout = child.getLayout(context); - const place = child.place - ? child.place - : new Place( - this.value, - // Place everything in the center - -layout.width / 2, - // We would normally not negate the y because its in math coordinates, but we want to move it - // down the y-axis by half, so we subtract. - -layout.height / 2, - 0 - ); + const place = + // Does the child have it's own place? Put it there. + child.place + ? child.place + : // Otherwise, put it in the center + new Place( + this.value, + // Place everything in the center + -layout.width / 2, + // We would normally not negate the y because its in math coordinates, but we want to move it + // down the y-axis by half, so we subtract. + -layout.height / 2, + 0 + ); places.push([child, place]); if (place.x < left) left = place.x; @@ -193,7 +212,7 @@ export default class Stage extends TypeOutput { locales[0].output.Stage.description, this.content.length, this.name instanceof TextLang ? this.name.text : undefined, - this.frame?.getDescription(locales[0]), + this.frame?.getDescription(locales), this.pose.getDescription(locales) ).toText(); } @@ -231,19 +250,25 @@ export class NameGenerator { } } -export function toStage(project: Project, value: Value): Stage | undefined { +export function toStage( + project: Project, + value: Value, + namer?: NameGenerator +): Stage | undefined { if (!(value instanceof StructureValue)) return undefined; // Create a name generator to guarantee unique default names for all TypeOutput. - const namer = new NameGenerator(); + if (namer === undefined) namer = new NameGenerator(); if (value.type === project.shares.output.Stage) { const possibleGroups = getOutputInput(value, 0); const content = possibleGroups instanceof ListValue - ? toTypeOutputList(project, possibleGroups, namer) - : toTypeOutput(project, possibleGroups, namer); - const frame = toShape(getOutputInput(value, 1)); + ? toOutputList(project, possibleGroups, namer) + : toOutput(project, possibleGroups, namer); + const frame = toRectangle(getOutputInput(value, 1)); + + const gravity = toNumber(getOutputInput(value, 21)) ?? DefaultGravity; const { size, @@ -259,7 +284,7 @@ export function toStage(project: Project, value: Value): Stage | undefined { exiting: exit, duration, style, - } = getStyle(project, value, 2); + } = getTypeStyle(project, value, 2); return content !== undefined && background !== undefined && @@ -284,13 +309,14 @@ export function toStage(project: Project, value: Value): Stage | undefined { move, exit, duration, - style + style, + gravity ) : undefined; } // Just a phrase or group? Wrap it in a stage. else { - const type = toTypeOutput(project, value, namer); + const type = toOutput(project, value, namer); return type === undefined ? undefined @@ -330,7 +356,8 @@ export function toStage(project: Project, value: Value): Stage | undefined { undefined, undefined, 0, - 'zippy' + DefaultStyle, + DefaultGravity ); } } diff --git a/src/output/TextLang.ts b/src/output/TextLang.ts index 8e70bbe93..28e27cb13 100644 --- a/src/output/TextLang.ts +++ b/src/output/TextLang.ts @@ -1,7 +1,7 @@ import type Value from '@values/Value'; -import Output from './Output'; +import Valued from './Valued'; -export default class TextLang extends Output { +export default class TextLang extends Valued { readonly text: string; readonly lang: string | undefined; diff --git a/src/output/TypeOutput.ts b/src/output/TypeOutput.ts deleted file mode 100644 index c6a8e6dae..000000000 --- a/src/output/TypeOutput.ts +++ /dev/null @@ -1,141 +0,0 @@ -import toStructure from '../basis/toStructure'; -import type Value from '@values/Value'; -import type Color from './Color'; -import Output from './Output'; -import type Place from './Place'; -import { getBind } from '@locale/getBind'; -import { TYPE_SYMBOL } from '@parser/Symbols'; -import Sequence from './Sequence'; -import TextLang from './TextLang'; -import type Pose from './Pose'; -import type { DefinitePose } from './Pose'; -import type RenderContext from './RenderContext'; -import Fonts, { type SupportedFace } from '../basis/Fonts'; -import type Locale from '../locale/Locale'; - -export function createTypeType(locales: Locale[]) { - return toStructure(` - ${getBind(locales, (locale) => locale.output.Type, TYPE_SYMBOL)}() -`); -} - -export const DefaultStyle = 'zippy'; - -/** Every group has the same style information. */ -export default abstract class TypeOutput extends Output { - readonly size: number | undefined; - readonly face: SupportedFace | undefined; - readonly place: Place | undefined; - readonly name: TextLang | string; - readonly selectable: boolean; - readonly background: Color | undefined; - readonly pose: DefinitePose; - readonly entering: Pose | Sequence | undefined; - readonly resting: Pose | Sequence | undefined; - readonly moving: Pose | Sequence | undefined; - readonly exiting: Pose | Sequence | undefined; - readonly duration: number; - readonly style: string; - - constructor( - value: Value, - size: number | undefined = undefined, - font: SupportedFace | undefined = undefined, - place: Place | undefined = undefined, - name: TextLang | string, - selectable: boolean, - background: Color | undefined, - pose: DefinitePose, - entry: Pose | Sequence | undefined = undefined, - resting: Pose | Sequence | undefined = undefined, - moving: Pose | Sequence | undefined = undefined, - exiting: Pose | Sequence | undefined = undefined, - duration: number, - style: string - ) { - super(value); - - this.size = size ? Math.max(0, size) : size; - this.face = font; - this.place = place; - this.name = name; - this.selectable = selectable; - this.background = background; - this.pose = pose; - this.entering = entry; - this.resting = resting; - this.moving = moving; - this.exiting = exiting; - this.duration = duration; - this.style = style; - - if (this.face) Fonts.loadFace(this.face); - } - - abstract getLayout(context: RenderContext): { - output: TypeOutput; - left: number; - right: number; - top: number; - bottom: number; - width: number; - height: number; - ascent: number; - descent: number; - places: [TypeOutput, Place][]; - }; - - abstract getOutput(): (TypeOutput | null)[]; - abstract getBackground(): Color | undefined; - abstract getShortDescription(locales: Locale[]): string; - abstract getDescription(locales: Locale[]): string; - - /* - Given a predict function that takes a type input, recursively scans - outputs for a match. - */ - abstract find( - check: (output: TypeOutput) => boolean - ): TypeOutput | undefined; - - getRestOrDefaultPose(): Pose | Sequence { - return this.resting ?? this.pose; - } - - getFirstRestPose(): Pose { - return this.resting instanceof Sequence - ? this.resting.getFirstPose() ?? this.pose - : this.resting ?? this.pose; - } - - getDefaultPose(): DefinitePose { - return this.pose; - } - - getRenderContext(context: RenderContext) { - return context.withFontAndSize(this.face, this.size); - } - - getHTMLID(): string { - return `output-${this.getName()}`; - } - - abstract isEmpty(): boolean; - - /** - * By default, a group's name for the purpose of animations is the ID of the node that created it. - * */ - getName(): string { - return this.name instanceof TextLang ? this.name.text : this.name; - } - - isAnimated() { - return ( - this.entering !== undefined || - this.resting instanceof Sequence || - this.moving !== undefined || - this.exiting !== undefined || - this.duration > 0 - ); - } -} diff --git a/src/output/Valued.ts b/src/output/Valued.ts new file mode 100644 index 000000000..b939269b9 --- /dev/null +++ b/src/output/Valued.ts @@ -0,0 +1,36 @@ +import type Value from '@values/Value'; +import type StructureValue from '../values/StructureValue'; + +/** + * A base class that represents some part of Stage output. + * It's core responsibility is to store a link to a Structure value, + * maintaining provenance. + * */ +export default class Valued { + /** + * The value on which this output component is based. + * If undefined, it means it was generated by the system and not by code. + * */ + readonly value: Value; + + constructor(value: Value) { + this.value = value; + } +} + +export function getOutputInputs( + value: StructureValue, + start = 0 +): (Value | undefined)[] { + return value.type.inputs + .slice(start) + .map((input) => value.resolve(input.names)); +} + +export function getOutputInput( + value: StructureValue, + index: number +): Value | undefined { + const input = value.type.inputs[index]; + return input ? value.resolve(input.names) : undefined; +} diff --git a/src/output/Velocity.ts b/src/output/Velocity.ts new file mode 100644 index 000000000..b118f1fe1 --- /dev/null +++ b/src/output/Velocity.ts @@ -0,0 +1,46 @@ +import toStructure from '../basis/toStructure'; +import type Value from '@values/Value'; +import { getBind } from '@locale/getBind'; +import Valued, { getOutputInputs } from './Valued'; +import { toNumber } from './Stage'; +import StructureValue from '../values/StructureValue'; +import type Locale from '../locale/Locale'; + +export function createVelocityType(locales: Locale[]) { + return toStructure(` + ${getBind(locales, (locale) => locale.output.Velocity, '•')}( + ${getBind(locales, (locale) => locale.output.Velocity.x)}•#m/s|ø: ø + ${getBind(locales, (locale) => locale.output.Velocity.y)}•#m/s|ø: ø + ${getBind(locales, (locale) => locale.output.Velocity.angle)}•#°/s|ø: ø + ) +`); +} + +export default class Velocity extends Valued { + readonly x: number | undefined; + readonly y: number | undefined; + readonly angle: number | undefined; + + constructor( + value: Value, + x: number | undefined, + y: number | undefined, + rotation: number | undefined + ) { + super(value); + + this.x = x; + this.y = y; + this.angle = rotation; + } +} + +export function toVelocity(value: Value | undefined): Velocity | undefined { + if (!(value instanceof StructureValue)) return undefined; + + const [xVal, yVal, rotationVal] = getOutputInputs(value); + const x = toNumber(xVal); + const y = toNumber(yVal); + const rotation = toNumber(rotationVal); + return new Velocity(value, x, y, rotation); +} diff --git a/src/output/geometry.test.ts b/src/output/geometry.test.ts new file mode 100644 index 000000000..914dd3a4b --- /dev/null +++ b/src/output/geometry.test.ts @@ -0,0 +1,83 @@ +import { test, expect } from 'vitest'; +import { + getSegmentIntersect, + getPolygonIntersect, + isPoint, + segment, + polygon, + type Point, + type Polygon, +} from './geometry'; + +test.each([ + [segment(0, 0, 0, 0), true], + [segment(0, 0, 0, 1), false], + [segment(1, 0, 0, 1), false], +])('isPoint(%s) = %s', (line, is) => { + expect(isPoint(line)).toBe(is); +}); + +test.each([ + /** A horizontal and vertical line on the axes should intersect at the origin */ + [segment(-2, 0, 2, 0), segment(0, -2, 0, 2), { x: 0, y: 0 }], + /** A horizontal line and a point should have no intersection */ + [segment(-2, 0, 2, 0), segment(0, 2, 0, 2), undefined], + /** Two diagonal lines through the origin should intersect at the origin */ + [segment(-2, -2, 2, 2), segment(-2, 2, 2, -2), { x: 0, y: 0 }], + /** Two parallel lines should have no intersection */ + [segment(-1, -2, -1, 2), segment(1, -2, 1, 2), undefined], +])( + 'getLineIntersection(%s, %s) = %s', + (line1, line2, point: Point | undefined) => { + const intersection = getSegmentIntersect(line1, line2); + if (point === undefined) expect(intersection).toBeUndefined(); + else { + expect(intersection).toBeDefined(); + if (intersection) { + expect(intersection.x).toBe(point.x); + expect(intersection.x).toBe(point.y); + } + } + } +); + +test.each([ + /** Two non-overlapping rectangles should have no intersecting points */ + [ + polygon([-2, 2], [-1, 2], [-1, 1], [-2, 1]), + polygon([2, -1], [2, -2], [1, -2], [1, -1]), + [], + ], + /** Two overlapping rectangles should have two intersecting points */ + [ + polygon([-2, 2], [1, 2], [1, -1], [-2, -1]), + polygon([-1, 1], [2, 1], [2, -2], [-1, -2]), + [ + { x: 1, y: 1 }, + { x: -1, y: -1 }, + ], + ], + /** A square centered in the origin and a right triangle intersecting the origin should intersect in two points */ + [ + polygon([-2, 2], [2, 2], [2, -2], [-2, -2]), + polygon([0, 0], [0, 4], [4, 4]), + [ + { x: 0, y: 2 }, + { x: 2, y: 2 }, + ], + ], +])( + 'getPolygonIntersection(%s, %s) = %s', + (poly1: Polygon, poly2: Polygon, expected: Point[]) => { + const intersection = getPolygonIntersect(poly1, poly2); + expect(intersection.length).toBe(expected.length); + for (const point of expected) { + const match = intersection.find( + (intersect) => + intersect.point.x === point.x && + intersect.point.y === point.y + ); + expect(match).toBeDefined(); + } + } +); diff --git a/src/output/geometry.ts b/src/output/geometry.ts new file mode 100644 index 000000000..0b607b7f1 --- /dev/null +++ b/src/output/geometry.ts @@ -0,0 +1,239 @@ +export type Point = { readonly x: number; readonly y: number }; +export type Velocity = { readonly vx: number; readonly vy: number }; +/** Assumes consecutive points form edges and that edges form a convex polygon */ +export type Polygon = Point[]; +export type Segment = readonly [Point, Point]; + +export function point(x: number, y: number) { + return { x, y }; +} + +export function segment(x1: number, y1: number, x2: number, y2: number) { + return [point(x1, y1), point(x2, y2)] as const; +} + +export function polygon(...points: [x: number, y: number][]) { + return points.map((p) => point(...p)); +} + +/** Get the magnitude of the segment */ +export function segmentMagnitude(segment: Segment) { + // Find the magnitude of the vector defined by the segment. + return magnitude(segment[0].x, segment[0].y, segment[1].x, segment[1].y); +} + +export function magnitude(x1: number, y1: number, x2 = 0, y2 = 0) { + return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); +} + +/** Get the unit vector of a segment */ +// Normalize +export function unit(segment: Segment): Point { + const mag = segmentMagnitude(segment); + return { + x: (segment[1].x - segment[0].x) / mag, + y: (segment[1].y - segment[0].y) / mag, + }; +} + +/** Take a point, and a center, and rotate the point around the center */ +export function rotateAround( + x: number, + y: number, + cx: number, + cy: number, + radians: number +): Point { + return { + x: cx + (x - cx) * Math.cos(radians) - (y - cy) * Math.sin(radians), + y: cy + (x - cx) * Math.sin(radians) + (y - cy) * Math.cos(radians), + }; +} + +/** + * Finds the point of intersection between two lines, if it exists, modeling lines as Bezier + * parameters interpolations. See the vector algebra here: https://paulbourke.net/geometry/pointlineplane/. + */ +export function getSegmentIntersect( + segment1: Segment, + segment2: Segment +): Point | undefined { + // If either line is a point, they don't intersect. + if (isPoint(segment1) || isPoint(segment2)) return undefined; + + // Compute the denominator + const denominator = + (segment2[1].y - segment2[0].y) * (segment1[1].x - segment1[0].x) - + (segment2[1].x - segment2[0].x) * (segment1[1].y - segment1[0].y); + + // Check if lines are parallel + if (denominator === 0) return undefined; + + // Compute interpolation parameters + const ua = + ((segment2[1].x - segment2[0].x) * (segment1[0].y - segment2[0].y) - + (segment2[1].y - segment2[0].y) * (segment1[0].x - segment2[0].x)) / + denominator; + const ub = + ((segment1[1].x - segment1[0].x) * (segment1[0].y - segment2[0].y) - + (segment1[1].y - segment1[0].y) * (segment1[0].x - segment2[0].x)) / + denominator; + + // If parameters are outside a 0 to 1 range, it means the intersection is not along the segment. + if (ua < 0 || ua > 1 || ub < 0 || ub > 1) { + return undefined; + } + + // Compute the intersection coordinate. + return { + x: segment1[0].x + ua * (segment1[1].x - segment1[0].x), + y: segment1[0].y + ua * (segment1[1].y - segment1[0].y), + }; +} + +/** True if the points are the same */ +export function isPoint(line: Segment) { + return line[0].x === line[1].x && line[0].y === line[1].y; +} + +/** + * Given two convex polygons, finds all points of intersection between their edges. + */ +export function getPolygonIntersect( + poly1: Polygon, + poly2: Polygon +): { segment1: Segment; segment2: Segment; point: Point }[] { + // Keep a list of intersection points + const intersections: { + segment1: Segment; + segment2: Segment; + point: Point; + }[] = []; + + // Iterate through all points + for (let n = 0; n < poly1.length; n++) { + // Construct a segment from this point to the next point in the polygon + const segment1 = [poly1[n], poly1[(n + 1) % poly1.length]] as const; + + // Iterate through the edges of the other polygon + for (let k = 0; k < poly2.length; k++) { + const segment2 = [poly2[k], poly2[(k + 1) % poly2.length]] as const; + + const intersection = getSegmentIntersect(segment1, segment2); + + // If the segments intersect, and the point is not already in the list, add it. + if ( + intersection && + !intersections.some( + (i) => + i.point.x === intersection.x && + i.point.y === intersection.y + ) + ) + intersections.push({ + segment1, + segment2, + point: intersection, + }); + } + } + return intersections; +} + +/** + * Compute new velocities based on current velocities and two points forming a collision segment. + * Use an impulse reponse: https://en.wikipedia.org/wiki/Collision_response + */ +export function getCollisionVelocities( + velocity1: Velocity, + mass1: number, + elasticity1: number, + velocity2: Velocity, + mass2: number, + elasticity2: number, + collisionPoint1: Point, + collisionPoint2: Point +): [Velocity, Velocity] { + // Compute relative velocity + const relativeVelocity = { + vx: velocity1.vx - velocity2.vx, + vy: velocity1.vy - velocity2.vy, + }; + + // Compute the segment between the two collision points + const collisionNormal = unit( + segment( + -(collisionPoint2.y - collisionPoint1.y), + collisionPoint2.x - collisionPoint1.x, + collisionPoint2.y - collisionPoint1.y, + -(collisionPoint2.x - collisionPoint1.x) + ) + ); + + // Compute the dot product of the collision normal and the relative velocity to get the relative velocity along the normal of the collision. + const vRelNormal = + relativeVelocity.vx * collisionNormal.x + + relativeVelocity.vy * collisionNormal.y; + + // Compute the impulse of the collision using relative velocity, unit vector of normal of collision, elasticity coefficient, and masses. + // J = -(1 + elasticity) · (vRel · normal) / (1 ÷ massA + 1 ÷ massB) + const elasticityFactor = -(1 + Math.min(elasticity1, elasticity2)); + const J = (elasticityFactor * vRelNormal) / (1 / mass1 + 1 / mass2); + + // Compute the new velocities + // vA' = vA + J·normal ÷ massA + // vB' = vB + J·normal ÷ massB + return [ + { + vx: velocity1.vx + (J * collisionNormal.x) / mass1, + vy: velocity1.vy + (J * collisionNormal.y) / mass1, + }, + { + vx: velocity2.vx - (J * collisionNormal.x) / mass2, + vy: velocity2.vy - (J * collisionNormal.y) / mass2, + }, + ]; +} + +export function getCommonPoint(segment1: Segment, segment2: Segment) { + if (pointsAreEqual(segment1[0], segment2[0])) return segment1[0]; + else if (pointsAreEqual(segment1[0], segment2[1])) return segment1[0]; + else if (pointsAreEqual(segment1[1], segment2[0])) return segment1[1]; + else if (pointsAreEqual(segment1[1], segment2[1])) return segment1[1]; + else return undefined; +} + +export function pointsAreEqual(point1: Point, point2: Point) { + return point1.x === point2.x && point1.y === point2.y; +} + +export function getDistanceFromSegmentToPoint(segment: Segment, point: Point) { + const A = point.x - segment[0].x; + const B = point.y - segment[0].y; + const C = segment[1].x - segment[0].x; + const D = segment[1].y - segment[0].y; + + const dot = A * C + B * D; + const len_sq = C * C + D * D; + let param = -1; + if (len_sq != 0) + //in case of 0 length line + param = dot / len_sq; + + let xx, yy; + + if (param < 0) { + xx = segment[0].x; + yy = segment[0].y; + } else if (param > 1) { + xx = segment[1].x; + yy = segment[1].y; + } else { + xx = segment[0].x + param * C; + yy = segment[0].y + param * D; + } + + const dx = point.x - xx; + const dy = point.y - yy; + return Math.sqrt(dx * dx + dy * dy); +} diff --git a/src/output/outputToCSS.ts b/src/output/outputToCSS.ts index ab846eafd..3bc82d2e8 100644 --- a/src/output/outputToCSS.ts +++ b/src/output/outputToCSS.ts @@ -77,8 +77,8 @@ export function getOpacityCSS(primary: Pose, secondary: Pose) { } export function toOutputTransform( - primaryPose: Pose, - secondaryPose: Pose, + primaryPose: Pose | undefined, + secondaryPose: Pose | undefined, place: Place, focus: Place, parentAscent: number, @@ -87,15 +87,25 @@ export function toOutputTransform( ) { const root = viewport !== undefined; + const posed = primaryPose && secondaryPose; + // Compute rendered scale based on scale and and flip - const scale = primaryPose.scale ?? secondaryPose.scale ?? 1; - const xScale = scale * (primaryPose.flipx ?? secondaryPose.flipx ? -1 : 1); - const yScale = scale * (primaryPose.flipy ?? secondaryPose.flipy ? -1 : 1); - const offset = primaryPose.offset ?? secondaryPose.offset; + const scale = posed ? primaryPose.scale ?? secondaryPose.scale ?? 1 : 1; + const xScale = posed + ? scale * (primaryPose.flipx ?? secondaryPose.flipx ? -1 : 1) + : 1; + const yScale = posed + ? scale * (primaryPose.flipy ?? secondaryPose.flipy ? -1 : 1) + : 1; + const offset = posed + ? primaryPose.offset ?? secondaryPose.offset + : undefined; const xOffset = offset ? offset.x * PX_PER_METER : 0; const yOffset = offset ? offset.y * PX_PER_METER : 0; const zOffset = offset ? offset.z : 0; - const rotation = primaryPose.rotation ?? secondaryPose.rotation ?? 0; + const rotation = posed + ? primaryPose.rotation ?? secondaryPose.rotation ?? 0 + : 0; // Compute the final z position of the output based on it's place and it's offset. const z = place.z + zOffset; @@ -127,7 +137,7 @@ export function toOutputTransform( // Translate the focus to focus coordinates. // Negate y to account for flipped y axis. const focusX = focus.x * PX_PER_METER; - const focusY = -focus.y * PX_PER_METER; + const focusY = focus.y * PX_PER_METER; // These are applied in reverse. const transform = [ diff --git a/src/output/toTypeOutput.ts b/src/output/toOutput.ts similarity index 78% rename from src/output/toTypeOutput.ts rename to src/output/toOutput.ts index 6032892de..13d70edb6 100644 --- a/src/output/toTypeOutput.ts +++ b/src/output/toOutput.ts @@ -6,7 +6,7 @@ import { toGroup } from './Group'; import { toPhrase } from './Phrase'; import { toRow } from './Row'; import { toStack } from './Stack'; -import type TypeOutput from './TypeOutput'; +import type Output from './Output'; import { NameGenerator, toStage } from './Stage'; import { toGrid } from './Grid'; import NoneValue from '@values/NoneValue'; @@ -20,16 +20,17 @@ import type TextLang from './TextLang'; import { DefinitePose, toPose } from './Pose'; import type Pose from './Pose'; import type Sequence from './Sequence'; -import { getOutputInputs } from './Output'; +import { getOutputInputs } from './Valued'; import { toSequence } from './Sequence'; import TextValue from '../values/TextValue'; import type { SupportedFace } from '../basis/Fonts'; +import { toShape } from './Shape'; -export function toTypeOutput( +export function toOutput( project: Project, value: Value | undefined, - namer?: NameGenerator -): TypeOutput | undefined { + namer: NameGenerator +): Output | undefined { if (!(value instanceof StructureValue)) return undefined; switch (value.type) { case project.shares.output.Phrase: @@ -37,24 +38,26 @@ export function toTypeOutput( case project.shares.output.Group: return toGroup(project, value, namer); case project.shares.output.Stage: - return toStage(project, value); + return toStage(project, value, namer); + case project.shares.output.Shape: + return toShape(project, value, namer); } return undefined; } -export function toTypeOutputList( +export function toOutputList( project: Project, value: Value | undefined, - namer?: NameGenerator -): (TypeOutput | null)[] | undefined { + namer: NameGenerator +): (Output | null)[] | undefined { if (value === undefined || !(value instanceof ListValue)) return undefined; - const phrases: (TypeOutput | null)[] = []; + const phrases: (Output | null)[] = []; for (const val of value.values) { if (!(val instanceof StructureValue || val instanceof NoneValue)) return undefined; const phrase = - val instanceof NoneValue ? null : toTypeOutput(project, val, namer); + val instanceof NoneValue ? null : toOutput(project, val, namer); if (phrase === undefined) return undefined; phrases.push(phrase); } @@ -79,7 +82,7 @@ export function toArrangement( return undefined; } -export function getStyle( +export function getTypeStyle( project: Project, value: StructureValue, index: number @@ -98,10 +101,38 @@ export function getStyle( duration: number | undefined; style: string | undefined; } { + const [sizeVal, faceVal, placeVal] = getOutputInputs(value, index); + + const size = toNumber(sizeVal); + const face = toFace(faceVal) as SupportedFace; + const place = toPlace(placeVal); + + const style = getStyle(project, value, index + 3, place); + + return { + size, + face, + place, + name: style.name, + selectable: style.selectable, + background: style.background, + pose: style.pose, + resting: style.resting, + entering: style.entering, + moving: style.moving, + exiting: style.exiting, + duration: style.duration, + style: style.style, + }; +} + +export function getStyle( + project: Project, + value: StructureValue, + index: number, + place?: Place | undefined +) { const [ - sizeVal, - faceVal, - placeVal, nameVal, selectableVal, colorVal, @@ -120,9 +151,6 @@ export function getStyle( styleVal, ] = getOutputInputs(value, index); - const size = toNumber(sizeVal); - const face = toFace(faceVal) as SupportedFace; - const place = toPlace(placeVal); const name = toText(nameVal); const selectable = toBoolean(selectableVal); const background = toColor(backgroundVal); @@ -139,7 +167,8 @@ export function getStyle( color, opacity, offset, - rotation, + // Default to place rotation if it has one + place?.rotation ?? rotation, scale, flipx, flipy @@ -152,9 +181,6 @@ export function getStyle( const duration = toNumber(durationVal); return { - size, - face, - place, name, selectable, background, diff --git a/src/runtime/Evaluation.ts b/src/runtime/Evaluation.ts index e2a4c381c..76ddc9444 100644 --- a/src/runtime/Evaluation.ts +++ b/src/runtime/Evaluation.ts @@ -130,27 +130,35 @@ export default class Evaluation { getSource() { return this.#source; } + getCreator() { return this.#evaluation; } + getCurrentNode() { return this.currentStep()?.node ?? this.getCreator(); } + getEvaluator(): Evaluator { return this.#evaluator; } + getDefinition() { return this.#definition; } + getClosure() { return this.#closure; } + getContext() { return this.#context; } + getStepNumber() { return this.#stepNumber; } + /** Utility function for generating a missing value exception */ getValueOrTypeException( expression: Expression, @@ -315,7 +323,10 @@ export default class Evaluation { } /** A convience function for getting a value by name, but only if it is a certain type */ - get<Kind>(name: string | Names, type: new (...params: never[]) => Kind) { + get<Kind>( + name: string | Names, + type: new (...params: never[]) => Kind + ): Kind | undefined { const value = this.resolve(name); return value instanceof type ? value : undefined; } diff --git a/src/runtime/Evaluator.ts b/src/runtime/Evaluator.ts index 358e4733a..5575cdc04 100644 --- a/src/runtime/Evaluator.ts +++ b/src/runtime/Evaluator.ts @@ -36,6 +36,7 @@ import Evaluate from '../nodes/Evaluate'; import NumberGenerator from 'recoverable-random'; import type { Database } from '../db/Database'; import ReactionStream from '../values/ReactionStream'; +import type Scene from '../output/Scene'; /** Anything that wants to listen to changes in the state of this evaluator */ export type EvaluationObserver = () => void; @@ -47,7 +48,7 @@ export type StreamChange = { }; export type StreamCreator = Evaluate | Reaction; export type IndexedValue = { value: Value | undefined; stepNumber: StepNumber }; -export const MAX_CALL_STACK_DEPTH = 256; +export const MAX_CALL_STACK_DEPTH = 512; export const MAX_STEP_COUNT = 262144; // Don't let source values take more than 256 MB of memory. @@ -179,6 +180,11 @@ export default class Evaluator { */ previousTime: DOMHighResTimeStamp | undefined = undefined; + /** + * The time between the last evaluation + */ + timeDelta: number | undefined = undefined; + /** The relative time, accounting for pauses, accumulated from deltas */ currentTime = 0; animating = false; @@ -206,6 +212,9 @@ export default class Evaluator { streams: Set<StreamValue>; }[] = []; + /** The scene corresponding to what's rendered, which is needed in providing streams access to collisions */ + scene: Scene | undefined = undefined; + /** * Create a new evalutor, given some project. * @param project The project to evaluate. @@ -410,12 +419,15 @@ export default class Evaluator { getStepCount() { return this.#stepCount; } + getStepIndex() { return this.#stepIndex; } + getEarliestStepIndexAvailable() { return this.reactions[0]?.stepIndex ?? 0; } + getSteps(evaluation: DefinitionNode): Step[] { // No expression? No steps. let steps = this.steps.get(evaluation); @@ -1125,18 +1137,21 @@ export default class Evaluator { tick(time: DOMHighResTimeStamp) { // First time? Just record it and bail. - let delta = 0; if (this.previousTime === undefined) { this.previousTime = time; + this.timeDelta = 0; } else { // Compute the delta and remember the previous time. - delta = time - this.previousTime; + this.timeDelta = time - this.previousTime; this.previousTime = time; } if (!this.isStepping()) { // Add the delta to the current time. - this.currentTime += delta; + this.currentTime += this.timeDelta; + + // Tick physics one frame, so Motion streams get new places. + if (this.scene) this.scene.physics.tick(this.timeDelta); // If we're in play mode, tick all the temporal streams. if (this.temporalReactions.length > 0) @@ -1145,7 +1160,11 @@ export default class Evaluator { ); // Tick each one, indirectly filling this.temporalReactions. for (const stream of this.temporalStreams) - stream.tick(this.currentTime, delta, this.timeMultiplier); + stream.tick( + this.currentTime, + this.timeDelta, + this.timeMultiplier + ); // Now reevaluate with all of the temporal stream updates. this.flush(); diff --git a/src/runtime/createDefaultShares.ts b/src/runtime/createDefaultShares.ts index 9af122aa9..acaedce2a 100644 --- a/src/runtime/createDefaultShares.ts +++ b/src/runtime/createDefaultShares.ts @@ -1,7 +1,7 @@ import { createStageType } from '../output/Stage'; import { createPhraseType } from '../output/Phrase'; import { createGroupType } from '../output/Group'; -import { createTypeType } from '../output/TypeOutput'; +import { createOutputType } from '../output/Output'; import { createPoseType } from '../output/Pose'; import { createStackType } from '../output/Stack'; import { createRowType } from '../output/Row'; @@ -18,7 +18,7 @@ import { createArrangementType } from '../output/Arrangement'; import { getDefaultSequences } from '../output/DefaultSequences'; import { createChoiceDefinition } from '../input/Choice'; import { createGridType } from '../output/Grid'; -import { createRectangleType, createShapeType } from '../output/Shapes'; +import { createRectangleType, createShapeType } from '../output/Shape'; import { createFreeType } from '../output/Free'; import type Locale from '../locale/Locale'; import { createCameraDefinition } from '../input/Camera'; @@ -27,21 +27,34 @@ import { createPlacementDefinition } from '../input/Placement'; import { createPitchDefinition } from '../input/Pitch'; import { createWebpageDefinition } from '../input/Webpage'; import { createChatDefinition } from '../input/Chat'; +import { createMatterType } from '../output/Matter'; +import { createVelocityType } from '../output/Velocity'; +import { createDirectionType } from '../output/Direction'; +import { createReboundType } from '../output/Rebound'; +import { createCollisionDefinition } from '../input/Collision'; export default function createDefaultShares(locales: Locale[]) { const PlaceType = createPlaceType(locales); + const VelocityType = createVelocityType(locales); + const MatterType = createMatterType(locales); const ColorType = createColorType(locales); + const DirectionType = createDirectionType(locales); + const ReboundType = createReboundType(locales); const OutputTypes = { - Type: createTypeType(locales), + Type: createOutputType(locales), Phrase: createPhraseType(locales), Group: createGroupType(locales), Stage: createStageType(locales), + Shape: createShapeType(locales), Pose: createPoseType(locales), Sequence: createSequenceType(locales), Color: ColorType, Place: PlaceType, - Shape: createShapeType(locales), + Matter: MatterType, + Velocity: VelocityType, + Direction: DirectionType, + Rebound: ReboundType, Rectangle: createRectangleType(locales), Arrangement: createArrangementType(locales), Stack: createStackType(locales), @@ -54,11 +67,7 @@ export default function createDefaultShares(locales: Locale[]) { Time: createTimeType(locales), Random: createRandomFunction(locales), Choice: createChoiceDefinition(locales), - Motion: createMotionDefinition( - locales, - OutputTypes.Type, - OutputTypes.Phrase - ), + Motion: createMotionDefinition(locales, PlaceType, VelocityType), Placement: createPlacementDefinition(locales, PlaceType), Key: createKeyDefinition(locales), Button: createButtonDefinition(locales), @@ -68,6 +77,7 @@ export default function createDefaultShares(locales: Locale[]) { Camera: createCameraDefinition(locales, ColorType), Webpage: createWebpageDefinition(locales), Chat: createChatDefinition(locales), + Collision: createCollisionDefinition(locales, ReboundType), }; const Sequences = getDefaultSequences(locales); diff --git a/src/values/ListValue.ts b/src/values/ListValue.ts index 150248e20..9626ee348 100644 --- a/src/values/ListValue.ts +++ b/src/values/ListValue.ts @@ -29,7 +29,11 @@ export default class ListValue extends SimpleValue { get(index: NumberValue | number) { const num = index instanceof NumberValue ? index.toNumber() : index; const value = - num === 0 ? undefined : this.values.at(num > 0 ? num - 1 : num); + num === 0 + ? undefined + : this.values.at( + num > 0 ? (num - 1) % this.values.length : num + ); return value === undefined ? new NoneValue(this.creator) : value; } diff --git a/src/values/NumberValue.ts b/src/values/NumberValue.ts index 2849eae16..57d0dea77 100644 --- a/src/values/NumberValue.ts +++ b/src/values/NumberValue.ts @@ -98,7 +98,10 @@ export default class NumberValue extends SimpleValue { } else if (number.isSymbol(Sym.JapaneseNumeral)) { return [convertJapanese(text).times(negated ? -1 : 1), undefined]; } else if (number.isSymbol(Sym.Number)) { - return [NumberValue.fromUnknown(text), undefined]; + return [ + NumberValue.fromUnknown(text).times(negated ? -1 : 1), + undefined, + ]; } else return [new Decimal(NaN), undefined]; } diff --git a/src/values/StreamValue.ts b/src/values/StreamValue.ts index 68f3b7276..838b70445 100644 --- a/src/values/StreamValue.ts +++ b/src/values/StreamValue.ts @@ -70,7 +70,7 @@ export default abstract class StreamValue< // Update the time. this.values.push({ - value: value, + value, stepIndex: this.evaluator.getStepIndex(), }); diff --git a/src/values/StructureValue.ts b/src/values/StructureValue.ts index 2c3a843d4..0ff65c872 100644 --- a/src/values/StructureValue.ts +++ b/src/values/StructureValue.ts @@ -31,6 +31,39 @@ export default class StructureValue extends Value { this.context = context; } + /** Creates an evaluation with the inputs of the given type */ + static make( + evaluator: Evaluator, + creator: EvaluationNode, + type: StructureDefinition, + ...inputs: Value[] + ) { + const map = new Map<Names, Value>(); + for (let index = 0; index < type.inputs.length; index++) { + const bind = type.inputs[index]; + const input = inputs[index]; + if (input === undefined) + throw new Error( + `Inputs are missing input # ${index}, ${type.inputs[index] + .getNames() + .join(', ')}` + ); + map.set(bind.names, inputs[index]); + } + + const evaluation = new Evaluation( + evaluator, + creator, + type, + undefined, + map + ); + + const structure = new StructureValue(creator, evaluation); + + return structure; + } + /** * A structure is equal to another structure if all of its bindings are equal and they have the same definition. */ diff --git a/static/locales/en-US/en-US-tutorial.json b/static/locales/en-US/en-US-tutorial.json index cd03ef533..34a7c4533 100644 --- a/static/locales/en-US/en-US-tutorial.json +++ b/static/locales/en-US/en-US-tutorial.json @@ -1451,7 +1451,7 @@ "concept": "None", "performance": [ "fix", - "Motion(Phrase('ø' size: 5m) startvy: 5m/s)" + "Phrase('ø' size: 5m place: Motion(velocity: Velocity(y: 5m/s)))" ], "lines": [ [ @@ -2009,17 +2009,17 @@ "gravity•#m/s^2: 15m/s^2", "", "Stage(count → [].translate(", - " ƒ(_) Motion(", - " Phrase(", - " '→?' → [].random()", - " place: ◆ ? Place(y: 10m) ø", + " ƒ(_) ", + " Phrase(", + " '→?' → [].random()", + " place: Motion(", + " Place(y: 10m)", + " Velocity(Random(-5 5) · 1m/s angle: Random(0 360) · 1°/s)", " )", - " vx: ◆ ? Random(-5 5) · 1m/s ø ", - " vangle: ◆ ? Random(0 360) · 1°/s ø", - " bounciness: Random()", - " gravity: gravity", + " matter: Matter(bounciness: Random())", " )", " )", + " gravity: gravity", ")" ], "scenes": [ @@ -2660,13 +2660,7 @@ "concept": "Motion", "performance": [ "fix", - "Motion(", - " Phrase('🏀' 3m)", - " startplace: Place(0m 5m)", - " startvx: 0m/s", - " startvy: 0m/s", - " startvangle: 0°/s", - ")" + "Stage([Phrase('🏀' 1m place: Motion(Place(0m 10m)) matter: Matter(2kg 0.8)) Shape(Rectangle(-10m 0m 10m -1m))])" ], "lines": [ ["use", "fit", "DarkVoid"], @@ -2674,74 +2668,115 @@ "FunctionDefinition", "excited", "So far, all of the *streams* we've talked about are sequences of simple values, like @Text or @Number. Some streams, however, can produce complex values.", - "@Motion is one of the most interesting of those." + "@Motion is one of the most interesting of those. It makes a stream of @Place." ], null, + ["edit", "Phrase('🏀' 3m place: Motion(Place(0m 5m)))"], [ - "fix", - "Motion(", - " Phrase('🏀' 3m) startplace: Place(0m 5m)", + "FunctionDefinition", + "excited", + "Here's the simplest way to use it. This creates a @Motion stream that starts with this @Phrase. It'll keep moving the ball based on *gravity*." + ], + ["Motion", "excited", "/Woosh…/"], + null, + [ + "edit", + "Stage([Phrase('🏀' 1m place: Motion(Place(0m 10m))) Shape(Rectangle(-10m 0m 10m -2m))])" + ], + [ + "FunctionDefinition", + "excited", + "But the ball keeps falling because there's no ground! We can put something called a @Shape on @Stage, like a @Rectangle. It takes two corners. We'll make it nice and thick.", + "Hm, it falls through the ground..." + ], + ["Motion", "excited", "/Woosh…/"], + null, + [ + "edit", + "Stage(", + " [", + " Phrase('🏀' 1m place: Motion(Place(0m 10m)) matter: Matter(1kg 0.9))", + " Shape(Rectangle(-10m 0m 10m -2m))", + " ]", ")" ], [ "FunctionDefinition", "excited", - "Here's the simplest way to use it. This creates a @Motion stream that starts with this @Phrase. It'll keep moving the ball based on *gravity*. The ground is at \\0m\\ on the y-axes." + "Oh right! That's because we forgot to give the ball @Matter. Matter is a way of telling us how heavy the @Output is, how bouncy it is, and how much friction it should have.", + "Let's make the ball really bouncy and light. Yay, now it bounces!" ], - ["Motion", "excited", "Woosh…"], null, [ - "fix", - "Motion(", - " Phrase('🏀' 3m) ", - " startplace: Place(0m 5m)", - " startvx: -5m/s", - " startvy: 5m/s", - " startvangle: 30°/s", + "edit", + "Stage(", + "\t[", + "\t\tPhrase('🏀' 1m place: Motion(Place(-10m 10m) Velocity(5m/s 15m/s 10°/s)) matter: Matter(1kg 0.9))", + "\t\tShape(Rectangle(-10m 0m 10m -2m))", + "\t]", ")" ], [ "FunctionDefinition", "excited", "But @Motion has many other tricks.", - "For example, it can have initial velocities, @Motion/startvx, @Motion/startvy, @Motion/startvz, and @Motion/startvangle.", - "This example makes the ball move left and up, spinning a bit initially." + "For example, we can give it a start @Motion/velocity.", + "This example makes the ball move right and up and spinning a bit initially." ], ["Motion", "excited", "Woooosh…"], null, [ - "fix", - "Motion(", - " Phrase('🏀' 3m) ", - " startplace: Place(0m 5m)", - " startvx: -5m/s", - " startvy: 5m/s", - " startvangle: 30°/s", - " gravity: 200m/s^2", + "edit", + "Stage(", + "\t[", + "\t\tPhrase('🏀' 1m place: Motion(Place(-10m 10m) Velocity(5m/s 15m/s 10°/s)) matter: Matter(1kg 0.9))", + "\t\tShape(Rectangle(-10m 0m 10m -2m))", + "\t]", + "\tgravity: 1m/s^s", ")" ], [ "FunctionDefinition", "excited", - "You can even change @Motion/gravity to be really extreme.", + "You can even change @Stage/gravity to be really extreme.", "Try changing it to be like the moon, where gravity is really low!", - "Or try changing @Motion/mass or @Motion/bounciness, which affects how @Phrase bounce." + "Or try changing @Matter/mass or @Matter/bounciness, which affects how @Phrase bounce." ], ["Motion", "excited", "Wsh…"], null, [ - "fix", - "Motion(", - " Phrase('🏀' 3m)", - " Place(0m 5m)", - " startvy: 50m/s", + "edit", + "Stage(", + "\t[", + "\t\tPhrase(", + "\t\t\t'🏀'", + "\t\t\t1m", + "\t\t\tname: 'ball'", + "\t\t\tplace: Motion(Place(-10m 10m) Velocity(5m/s 15m/s 10°/s))", + "\t\t\tmatter: Matter(1kg 0.9)", + "\t\t\tscale: Collision('ball' 'ground')•ø ? 1 2)", + "\t\tShape(Rectangle(-10m 0m 10m -2m) name: 'ground')", + "\t]", ")" ], [ "FunctionDefinition", "excited", - "There are a lot of fun things you can do with a @Motion stream!", - "Maybe you already have some ideas..." + "It's even possible to know when some @Output bumps into another output with related stream called @Collision.", + "We just need to give names to our two @Output. How about 'ball' and 'ground'?", + "Then, we @Collision will give us a @Rebound when they touch and @None with they don't.", + "Let's make the ball scale up each time it hits the ground for emphasis!" + ], + null, + [ + "fix", + "Stage([Phrase('🏀' 1m place: Motion(Place(0m 10m)) matter: Matter(2kg 0.8)) Shape(Rectangle(-10m 0m 10m -1m))])" + ], + [ + "FunctionDefinition", + "kind", + "There's so much more you can do with @Motion, @Collision, and @Shape!", + "I hope you'll try lots of bouncy things." ] ] }, diff --git a/static/locales/es-MX/es-MX.json b/static/locales/es-MX/es-MX.json index 89b5ad860..66f0a7dd5 100644 --- a/static/locales/es-MX/es-MX.json +++ b/static/locales/es-MX/es-MX.json @@ -830,7 +830,6 @@ }, "UnknownType": { "name": "desconocida", - "unknown": "$?", "connector": "$?", "emotion": "neutral", "doc": "$?" @@ -1585,19 +1584,10 @@ "Motion": { "names": "movimiento", "doc": "$?", - "type": { "doc": "$?", "names": "tipo" }, - "startplace": { "doc": "$?", "names": "$? startplace" }, - "startvx": { "doc": "$?", "names": "$? startvx" }, - "startvy": { "doc": "$?", "names": "$? startvy" }, - "startvz": { "doc": "$?", "names": "$? startvz" }, - "startvangle": { "doc": "$?", "names": "$? startvangle" }, - "vx": { "doc": "$?", "names": "vx" }, - "vy": { "doc": "$?", "names": "vy" }, - "vz": { "doc": "$?", "names": "vz" }, - "vangle": { "doc": "$?", "names": "vangle" }, - "mass": { "doc": "$?", "names": "masa" }, - "bounciness": { "doc": "$?", "names": "rebote" }, - "gravity": { "doc": "$?", "names": "gravedad" } + "place": { "doc": "$?", "names": "$? place" }, + "velocity": { "doc": "$?", "names": "$? velocity" }, + "nextplace": { "doc": "$?", "names": "$? nextplace" }, + "nextvelocity": { "doc": "$?", "names": "$? nextvelocity'" } }, "Chat": { "doc": ["$?"], @@ -1642,11 +1632,51 @@ "noConnection": "$?", "limit": "$?" } + }, + "Collision": { + "names": "$? Collision", + "doc": "$?", + "subject": { + "names": "$? name", + "doc": "$?" + }, + "object": { + "names": "$? other", + "doc": "$?" + } + }, + "Rebound": { + "names": "$? Rebound", + "doc": "$?", + "direction": { + "names": "$? direction", + "doc": "$?" + }, + "subject": { + "names": "$? subject", + "doc": "$?" + }, + "object": { + "names": "$? object", + "doc": "$?" + } + }, + "Direction": { + "names": "$? Direction", + "doc": "$?", + "x": { + "names": "$? x", + "doc": "$?" + }, + "y": { + "names": "$? y", + "doc": "$?" + } } }, "output": { - "Type": { - "names": "tipo", + "Output": { + "names": "$? Output", "doc": "$?" }, "Stage": { @@ -1694,7 +1724,8 @@ "moving": { "doc": "$?", "names": "$! mover" }, "exiting": { "doc": "$?", "names": "$! salida" }, "duration": { "doc": "$?", "names": "duración" }, - "style": { "doc": "$?", "names": "estilo" } + "style": { "doc": "$?", "names": "estilo" }, + "gravity": { "doc": "$?", "names": "gravedad" } }, "Group": { "names": "Group", @@ -1702,6 +1733,10 @@ "description": "$?", "content": { "doc": "$?", "names": "content" }, "layout": { "doc": "$?", "names": "layout" }, + "matter": { + "doc": "$?", + "names": "$? matter" + }, "size": { "doc": "$?", "names": "tamaño" }, "face": { "doc": "$?", "names": "fuente" }, "place": { "doc": "$?", "names": "place" }, @@ -1759,6 +1794,10 @@ "doc": "$?", "names": "$? alignment" }, + "matter": { + "doc": "$?", + "names": "$? matter" + }, "name": { "doc": "$?", "names": "nombre" }, "selectable": { "doc": "$?", "names": "seleccionable" }, "color": { @@ -1833,14 +1872,56 @@ "doc": "$?", "description": "forma libre, $1 producción" }, - "Shape": { "names": "forma", "doc": "$?" }, + "Shape": { + "names": "forma", + "doc": "$?", + "form": { "doc": "$?", "names": "$? form" }, + "name": { "doc": "$?", "names": "$? name" }, + "selectable": { "doc": "$?", "names": "$? selectable" }, + "color": { + "doc": "$?", + "names": "$? color" + }, + "background": { "doc": "$?", "names": "$? background" }, + "opacity": { + "doc": "$?", + "names": "$? opacity" + }, + "offset": { + "doc": "$?", + "names": "$? offset" + }, + "rotation": { + "doc": "$?", + "names": "$? rotation" + }, + "scale": { + "doc": "$?", + "names": "$? scale" + }, + "flipx": { + "doc": "$?", + "names": "$? flipx" + }, + "flipy": { + "doc": "$?", + "names": "$? flipy" + }, + "entering": { "doc": "$?", "names": "$? entering" }, + "resting": { "doc": "$?", "names": "$? resting" }, + "moving": { "doc": "$?", "names": "$? moving" }, + "exiting": { "doc": "$?", "names": "$? exiting" }, + "duration": { "doc": "$?", "names": ["$? duration"] }, + "style": { "doc": "$?", "names": "$? style" } + }, "Rectangle": { "names": "Rectángulo", "doc": "$?", "left": { "doc": "$?", "names": "izquierda" }, "top": { "doc": "$?", "names": "masalta" }, "right": { "doc": "$?", "names": "derecha" }, - "bottom": { "doc": "$?", "names": "abajo" } + "bottom": { "doc": "$?", "names": "abajo" }, + "z": { "doc": "$?", "names": "$? z" } }, "Pose": { "names": "Pose", @@ -1880,9 +1961,51 @@ "Place": { "names": ["Posición"], "doc": "$?", - "x": { "doc": "$?", "names": "x" }, - "y": { "doc": "$?", "names": "y" }, - "z": { "doc": "$?", "names": "z" } + "x": { "doc": "$?", "names": "$? x" }, + "y": { "doc": "$?", "names": "$? y" }, + "z": { "doc": "$?", "names": "$? z" }, + "rotation": { "doc": "$?", "names": "$? rotation" } + }, + "Velocity": { + "doc": "$?", + "names": "$? Velocity", + "x": { + "doc": "$?", + "names": "$? x" + }, + "y": { + "doc": "$?", + "names": "$? y" + }, + "angle": { + "doc": "$? ", + "names": "$? angle" + } + }, + "Matter": { + "doc": "$?", + "names": "$? Matter", + "mass": { "doc": "$?", "names": "$? mass" }, + "bounciness": { + "doc": "$?", + "names": "$? bounciness" + }, + "friction": { + "doc": "$?", + "names": "$? friction" + }, + "roundedness": { + "doc": "$?", + "names": "$? roundedness" + }, + "text": { + "doc": "$?", + "names": "$? text" + }, + "shapes": { + "doc": "$?", + "names": "$? shapes" + } }, "Easing": { "straight": "lineal", @@ -2208,6 +2331,9 @@ "set": "$?", "addPhrase": "$?", "addGroup": "$?", + "addShape": "$?", + "addMotion": "$?", + "addPlacement": "$?", "remove": "$?", "up": "$?", "down": "$?", diff --git a/static/locales/example/example.json b/static/locales/example/example.json index 908ba1d58..a92fa729e 100644 --- a/static/locales/example/example.json +++ b/static/locales/example/example.json @@ -1564,21 +1564,12 @@ "frequency": { "names": ["frecuencia"], "doc": "$?" } }, "Motion": { - "names": "$?", + "names": "movimiento", "doc": "$?", - "type": { "doc": "$?", "names": "$?" }, - "startplace": { "doc": "$?", "names": "$? startplace" }, - "startvx": { "doc": "$?", "names": "$? startvx" }, - "startvy": { "doc": "$?", "names": "$? startvy" }, - "startvz": { "doc": "$?", "names": "$? startvz" }, - "startvangle": { "doc": "$?", "names": "$? startvangle" }, - "vx": { "doc": "$?", "names": "$?" }, - "vy": { "doc": "$?", "names": "$?" }, - "vz": { "doc": "$?", "names": "$?" }, - "vangle": { "doc": "$?", "names": "$?" }, - "mass": { "doc": "$?", "names": "$?" }, - "bounciness": { "doc": "$?", "names": "$?" }, - "gravity": { "doc": "$?", "names": "$?" } + "place": { "doc": "$?", "names": "$? place" }, + "velocity": { "doc": "$?", "names": "$? velocity" }, + "nextplace": { "doc": "$?", "names": "$? nextplace" }, + "nextvelocity": { "doc": "$?", "names": "$? nextvelocity" } }, "Chat": { "doc": ["$?"], @@ -1623,11 +1614,51 @@ "noConnection": "$?", "limit": "$?" } + }, + "Collision": { + "names": "$? Collision", + "doc": "$?", + "subject": { + "names": "$? name", + "doc": "$?" + }, + "object": { + "names": "$? other", + "doc": "$?" + } + }, + "Rebound": { + "names": "$? Rebound", + "doc": "$?", + "direction": { + "names": "$? direction", + "doc": "$?" + }, + "subject": { + "names": "$? subject", + "doc": "$?" + }, + "object": { + "names": "$? object", + "doc": "$?" + } + }, + "Direction": { + "names": "$? Direction", + "doc": "$?", + "x": { + "names": "$? x", + "doc": "$?" + }, + "y": { + "names": "$? y", + "doc": "$?" + } } }, "output": { - "Type": { - "names": "$?", + "Output": { + "names": "$? Output", "doc": "$?" }, "Stage": { @@ -1675,7 +1706,8 @@ "moving": { "doc": "$?", "names": "$?" }, "exiting": { "doc": "$?", "names": "$?" }, "duration": { "doc": "$?", "names": ["duración"] }, - "style": { "doc": "$?", "names": "$?" } + "style": { "doc": "$?", "names": "$?" }, + "gravity": { "doc": "$?", "names": "$?" } }, "Group": { "names": "$?", @@ -1683,6 +1715,10 @@ "description": "$?", "content": { "doc": "$?", "names": "$?" }, "layout": { "doc": "$?", "names": "$?" }, + "matter": { + "doc": "$?", + "names": "$? matter" + }, "size": { "doc": "$?", "names": "$?" }, "face": { "doc": "$?", "names": "$?" }, "place": { "doc": "$?", "names": "$?" }, @@ -1740,6 +1776,10 @@ "doc": "$?", "names": "$? alignment" }, + "matter": { + "doc": "$?", + "names": "$? matter" + }, "name": { "doc": "$?", "names": "$?" }, "selectable": { "doc": "$?", "names": "$?" }, "color": { @@ -1814,14 +1854,56 @@ "doc": "$?", "description": "$?" }, - "Shape": { "names": "$?", "doc": "$?" }, + "Shape": { + "names": "$? shape", + "doc": "$?", + "form": { "doc": "$?", "names": "$? form" }, + "name": { "doc": "$?", "names": "$? name" }, + "selectable": { "doc": "$?", "names": "$? selectable" }, + "color": { + "doc": "$?", + "names": "$? color" + }, + "background": { "doc": "$?", "names": "$? background" }, + "opacity": { + "doc": "$?", + "names": "$? opacity" + }, + "offset": { + "doc": "$?", + "names": "$? offset" + }, + "rotation": { + "doc": "$?", + "names": "$? rotation" + }, + "scale": { + "doc": "$?", + "names": "$? scale" + }, + "flipx": { + "doc": "$?", + "names": "$? flipx" + }, + "flipy": { + "doc": "$?", + "names": "$? flipy" + }, + "entering": { "doc": "$?", "names": "$? entering" }, + "resting": { "doc": "$?", "names": "$? resting" }, + "moving": { "doc": "$?", "names": "$? moving" }, + "exiting": { "doc": "$?", "names": "$? exiting" }, + "duration": { "doc": "$?", "names": ["$? duration"] }, + "style": { "doc": "$?", "names": "$? style" } + }, "Rectangle": { "names": "$?", "doc": "$?", "left": { "doc": "$?", "names": "$?" }, "top": { "doc": "$?", "names": "$?" }, "right": { "doc": "$?", "names": "$?" }, - "bottom": { "doc": "$?", "names": "$?" } + "bottom": { "doc": "$?", "names": "$?" }, + "z": { "doc": "$?", "names": "$? z" } }, "Pose": { "names": "$?", @@ -1861,9 +1943,51 @@ "Place": { "names": ["Posición"], "doc": "$?", - "x": { "doc": "$?", "names": "$?" }, - "y": { "doc": "$?", "names": "$?" }, - "z": { "doc": "$?", "names": "$?" } + "x": { "doc": "$?", "names": "$? x" }, + "y": { "doc": "$?", "names": "$? y" }, + "z": { "doc": "$?", "names": "$? z" }, + "rotation": { "doc": "$?", "names": "$? rotation" } + }, + "Velocity": { + "doc": "$?", + "names": "$? Velocity", + "x": { + "doc": "$?", + "names": "$? x" + }, + "y": { + "doc": "$?", + "names": "$? y" + }, + "angle": { + "doc": "$? ", + "names": "$? angle" + } + }, + "Matter": { + "doc": "$?", + "names": "$? Matter", + "mass": { "doc": "$?", "names": "$? mass" }, + "bounciness": { + "doc": "$?", + "names": "$? bounciness" + }, + "friction": { + "doc": "$?", + "names": "$? friction" + }, + "roundedness": { + "doc": "$?", + "names": "$? roundedness" + }, + "text": { + "doc": "$?", + "names": "$? text" + }, + "shapes": { + "doc": "$?", + "names": "$? shapes" + } }, "Easing": { "straight": "$?", @@ -2187,6 +2311,9 @@ "set": "$?", "addPhrase": "$?", "addGroup": "$?", + "addShape": "$?", + "addMotion": "$?", + "addPlacement": "$?", "remove": "$?", "up": "$?", "down": "$?", diff --git a/static/locales/zh-CN/zh-CN.json b/static/locales/zh-CN/zh-CN.json index 5432dff32..48252818a 100644 --- a/static/locales/zh-CN/zh-CN.json +++ b/static/locales/zh-CN/zh-CN.json @@ -830,7 +830,6 @@ }, "UnknownType": { "name": "$?", - "unknown": "$?", "connector": "$?", "emotion": "$?", "doc": "$?" @@ -1565,21 +1564,12 @@ "frequency": { "names": ["frecuencia"], "doc": "$?" } }, "Motion": { - "names": "$?", + "names": "movimiento", "doc": "$?", - "type": { "doc": "$?", "names": "$?" }, - "startplace": { "doc": "$?", "names": "$? startplace" }, - "startvx": { "doc": "$?", "names": "$? startvx" }, - "startvy": { "doc": "$?", "names": "$? startvy" }, - "startvz": { "doc": "$?", "names": "$? startvz" }, - "startvangle": { "doc": "$?", "names": "$? startvangle" }, - "vx": { "doc": "$?", "names": "$?" }, - "vy": { "doc": "$?", "names": "$?" }, - "vz": { "doc": "$?", "names": "$?" }, - "vangle": { "doc": "$?", "names": "$?" }, - "mass": { "doc": "$?", "names": "$?" }, - "bounciness": { "doc": "$?", "names": "$?" }, - "gravity": { "doc": "$?", "names": "$?" } + "place": { "doc": "$?", "names": "$? place" }, + "velocity": { "doc": "$?", "names": "$? velocity" }, + "nextplace": { "doc": "$?", "names": "$? nextplace" }, + "nextvelocity": { "doc": "$?", "names": "$? nextvelocity" } }, "Chat": { "doc": ["$?"], @@ -1624,11 +1614,51 @@ "noConnection": "$?", "limit": "$?" } + }, + "Collision": { + "names": "$? Collision", + "doc": "$?", + "subject": { + "names": "$? name", + "doc": "$?" + }, + "object": { + "names": "$? other", + "doc": "$?" + } + }, + "Rebound": { + "names": "$? Rebound", + "doc": "$?", + "direction": { + "names": "$? direction", + "doc": "$?" + }, + "subject": { + "names": "$? subject", + "doc": "$?" + }, + "object": { + "names": "$? object", + "doc": "$?" + } + }, + "Direction": { + "names": "$? Direction", + "doc": "$?", + "x": { + "names": "$? x", + "doc": "$?" + }, + "y": { + "names": "$? y", + "doc": "$?" + } } }, "output": { - "Type": { - "names": "$?", + "Output": { + "names": "$? Output", "doc": "$?" }, "Stage": { @@ -1676,7 +1706,8 @@ "moving": { "doc": "$?", "names": "$?" }, "exiting": { "doc": "$?", "names": "$?" }, "duration": { "doc": "$?", "names": ["$?"] }, - "style": { "doc": "$?", "names": "$?" } + "style": { "doc": "$?", "names": "$?" }, + "gravity": { "doc": "$?", "names": "$? gravity" } }, "Group": { "names": "$?", @@ -1684,6 +1715,10 @@ "description": "$?", "content": { "doc": "$?", "names": "$?" }, "layout": { "doc": "$?", "names": "$?" }, + "matter": { + "doc": "$?", + "names": "$? matter" + }, "size": { "doc": "$?", "names": "$?" }, "face": { "doc": "$?", "names": "$?" }, "place": { "doc": "$?", "names": "$?" }, @@ -1741,6 +1776,10 @@ "doc": "$?", "names": "$? alignment" }, + "matter": { + "doc": "$?", + "names": "$? matter" + }, "name": { "doc": "$?", "names": "$?" }, "selectable": { "doc": "$?", "names": "$?" }, "color": { @@ -1815,14 +1854,56 @@ "doc": "$?", "description": "$?" }, - "Shape": { "names": "$?", "doc": "$?" }, + "Shape": { + "names": "$? shape", + "doc": "$?", + "form": { "doc": "$?", "names": "$? form" }, + "name": { "doc": "$?", "names": "$? name" }, + "selectable": { "doc": "$?", "names": "$? selectable" }, + "color": { + "doc": "$?", + "names": "$? color" + }, + "background": { "doc": "$?", "names": "$? background" }, + "opacity": { + "doc": "$?", + "names": "$? opacity" + }, + "offset": { + "doc": "$?", + "names": "$? offset" + }, + "rotation": { + "doc": "$?", + "names": "$? rotation" + }, + "scale": { + "doc": "$?", + "names": "$? scale" + }, + "flipx": { + "doc": "$?", + "names": "$? flipx" + }, + "flipy": { + "doc": "$?", + "names": "$? flipy" + }, + "entering": { "doc": "$?", "names": "$? entering" }, + "resting": { "doc": "$?", "names": "$? resting" }, + "moving": { "doc": "$?", "names": "$? moving" }, + "exiting": { "doc": "$?", "names": "$? exiting" }, + "duration": { "doc": "$?", "names": ["$? duration"] }, + "style": { "doc": "$?", "names": "$? style" } + }, "Rectangle": { "names": "$?", "doc": "$?", "left": { "doc": "$?", "names": "$?" }, "top": { "doc": "$?", "names": "$?" }, "right": { "doc": "$?", "names": "$?" }, - "bottom": { "doc": "$?", "names": "$?" } + "bottom": { "doc": "$?", "names": "$?" }, + "z": { "doc": "$?", "names": "$? z" } }, "Pose": { "names": "$?", @@ -1862,9 +1943,51 @@ "Place": { "names": ["Posición"], "doc": "$?", - "x": { "doc": "$?", "names": "$?" }, - "y": { "doc": "$?", "names": "$?" }, - "z": { "doc": "$?", "names": "$?" } + "x": { "doc": "$?", "names": "$? x" }, + "y": { "doc": "$?", "names": "$? y" }, + "z": { "doc": "$?", "names": "$? z" }, + "rotation": { "doc": "$?", "names": "$? rotation" } + }, + "Velocity": { + "doc": "$?", + "names": "$? Velocity", + "x": { + "doc": "$?", + "names": "$? x" + }, + "y": { + "doc": "$?", + "names": "$? y" + }, + "angle": { + "doc": "$? ", + "names": "$? angle" + } + }, + "Matter": { + "doc": "$?", + "names": "$? Matter", + "mass": { "doc": "$?", "names": "$? mass" }, + "bounciness": { + "doc": "$?", + "names": "$? bounciness" + }, + "friction": { + "doc": "$?", + "names": "$? friction" + }, + "roundedness": { + "doc": "$?", + "names": "$? roundedness" + }, + "text": { + "doc": "$?", + "names": "$? text" + }, + "shapes": { + "doc": "$?", + "names": "$? shapes" + } }, "Easing": { "straight": "$?", @@ -2188,6 +2311,9 @@ "set": "$?", "addPhrase": "$?", "addGroup": "$?", + "addShape": "$?", + "addMotion": "$?", + "addPlacement": "$?", "remove": "$?", "up": "$?", "down": "$?", diff --git a/static/schemas/Locale.json b/static/schemas/Locale.json index 791ec902a..e8a722159 100644 --- a/static/schemas/Locale.json +++ b/static/schemas/Locale.json @@ -1836,6 +1836,64 @@ "$ref": "#/definitions/NameAndDoc", "description": "A stream of names selected with the pointer or keyboard" }, + "Collision": { + "additionalProperties": false, + "description": "A stream of collisions between objects with matter.", + "properties": { + "doc": { + "$ref": "#/definitions/DocText", + "description": "Documentation for this definition, to appear in the documentation browser." + }, + "names": { + "$ref": "#/definitions/NameText", + "description": "One or more names for this definition. Be careful not to introduce duplicates." + }, + "object": { + "$ref": "#/definitions/NameAndDoc", + "description": "The object of a collision." + }, + "subject": { + "$ref": "#/definitions/NameAndDoc", + "description": "The subject of a collision" + } + }, + "required": [ + "doc", + "names", + "object", + "subject" + ], + "type": "object" + }, + "Direction": { + "additionalProperties": false, + "description": "A vector indicating a direction and magnitude.", + "properties": { + "doc": { + "$ref": "#/definitions/DocText", + "description": "Documentation for this definition, to appear in the documentation browser." + }, + "names": { + "$ref": "#/definitions/NameText", + "description": "One or more names for this definition. Be careful not to introduce duplicates." + }, + "x": { + "$ref": "#/definitions/NameAndDoc", + "description": "The direction and magnitude on the x-axis" + }, + "y": { + "$ref": "#/definitions/NameAndDoc", + "description": "The direction and magnitude on the y-axis" + } + }, + "required": [ + "doc", + "names", + "x", + "y" + ], + "type": "object" + }, "Key": { "additionalProperties": false, "description": "A stream of keys pressed, configurable to only show up or down states", @@ -1869,83 +1927,38 @@ "additionalProperties": false, "description": "A stream of phrases in places and rotations simulating physics", "properties": { - "bounciness": { - "$ref": "#/definitions/NameAndDoc", - "description": "A coefficient that dampens collisions" - }, "doc": { "$ref": "#/definitions/DocText", "description": "Documentation for this definition, to appear in the documentation browser." }, - "gravity": { - "$ref": "#/definitions/NameAndDoc", - "description": "Gravity, influencing change in y velocity" - }, - "mass": { - "$ref": "#/definitions/NameAndDoc", - "description": "Mass, influencing collisions" - }, "names": { "$ref": "#/definitions/NameText", "description": "One or more names for this definition. Be careful not to introduce duplicates." }, - "startplace": { + "nextplace": { "$ref": "#/definitions/NameAndDoc", - "description": "Where the phrase should start" + "description": "The next place for the motion, overriding physics" }, - "startvangle": { + "nextvelocity": { "$ref": "#/definitions/NameAndDoc", - "description": "Starting angular velocity" + "description": "The next velocity for the motion, overriding physics" }, - "startvx": { - "$ref": "#/definitions/NameAndDoc", - "description": "Starting x velocity" - }, - "startvy": { - "$ref": "#/definitions/NameAndDoc", - "description": "Starting y velocity" - }, - "startvz": { - "$ref": "#/definitions/NameAndDoc", - "description": "Starting z velocity" - }, - "type": { - "$ref": "#/definitions/NameAndDoc", - "description": "The phrase template to use" - }, - "vangle": { - "$ref": "#/definitions/NameAndDoc", - "description": "A constant angular velocity to hold" - }, - "vx": { - "$ref": "#/definitions/NameAndDoc", - "description": "A constant x velocity to hold" - }, - "vy": { + "place": { "$ref": "#/definitions/NameAndDoc", - "description": "A constant y velocity to hold" + "description": "The initial place for the motion" }, - "vz": { + "velocity": { "$ref": "#/definitions/NameAndDoc", - "description": "A constant z velocity to hold" + "description": "The initial velocity for the motion" } }, "required": [ - "bounciness", "doc", - "gravity", - "mass", "names", - "startplace", - "startvangle", - "startvx", - "startvy", - "startvz", - "type", - "vangle", - "vx", - "vy", - "vz" + "nextplace", + "nextvelocity", + "place", + "velocity" ], "type": "object" }, @@ -2035,6 +2048,40 @@ ], "type": "object" }, + "Rebound": { + "additionalProperties": false, + "description": "The values that come out of a collision stream.", + "properties": { + "direction": { + "$ref": "#/definitions/NameAndDoc", + "description": "The direction of the collision, relative to the collision stream's subject." + }, + "doc": { + "$ref": "#/definitions/DocText", + "description": "Documentation for this definition, to appear in the documentation browser." + }, + "names": { + "$ref": "#/definitions/NameText", + "description": "One or more names for this definition. Be careful not to introduce duplicates." + }, + "object": { + "$ref": "#/definitions/NameAndDoc", + "description": "The name a collision stream collided with." + }, + "subject": { + "$ref": "#/definitions/NameAndDoc", + "description": "The name a collision stream collided with." + } + }, + "required": [ + "direction", + "doc", + "names", + "object", + "subject" + ], + "type": "object" + }, "Time": { "additionalProperties": false, "description": "A stream of times since evaluation began", @@ -2166,7 +2213,10 @@ "Motion", "Placement", "Chat", - "Webpage" + "Webpage", + "Collision", + "Rebound", + "Direction" ], "type": "object" }, @@ -6287,6 +6337,10 @@ "$ref": "#/definitions/NameAndDoc", "description": "The layout to use to place the content in the group on stage" }, + "matter": { + "$ref": "#/definitions/NameAndDoc", + "description": "The matter to use for the group if it's involved in collisions" + }, "moving": { "$ref": "#/definitions/NameAndDoc", "description": "Pose or sequence for when a phrase, group, or stage is moving" @@ -6349,6 +6403,7 @@ "flipx", "flipy", "layout", + "matter", "moving", "name", "names", @@ -6364,6 +6419,59 @@ ], "type": "object" }, + "Matter": { + "additionalProperties": false, + "description": "Physical properties of matter", + "properties": { + "bounciness": { + "$ref": "#/definitions/NameAndDoc", + "description": "from 0-1, how bouncy something should be, where 0 means not bouncy at all, and 1 means retaining all of it's energy on collision" + }, + "doc": { + "$ref": "#/definitions/DocText", + "description": "Documentation for this definition, to appear in the documentation browser." + }, + "friction": { + "$ref": "#/definitions/NameAndDoc", + "description": "from 0-1, where 0 means no sliding, and 1 means sliding indefinitely" + }, + "mass": { + "$ref": "#/definitions/NameAndDoc", + "description": "in kilograms, how much something weighs for the purposes of collisions" + }, + "names": { + "$ref": "#/definitions/NameText", + "description": "One or more names for this definition. Be careful not to introduce duplicates." + }, + "roundedness": { + "$ref": "#/definitions/NameAndDoc", + "description": "from 0-1, what percent of the size to round the corners of the output's rectangle." + }, + "shapes": { + "$ref": "#/definitions/NameAndDoc", + "description": "whether the output can collide with other shapes" + }, + "text": { + "$ref": "#/definitions/NameAndDoc", + "description": "whether the output can collide with other output" + } + }, + "required": [ + "bounciness", + "doc", + "friction", + "mass", + "names", + "roundedness", + "shapes", + "text" + ], + "type": "object" + }, + "Output": { + "$ref": "#/definitions/NameAndDoc", + "description": "The base interface for Phrase, Group, and Stage, and other types of Output" + }, "Phrase": { "additionalProperties": false, "description": "A sequence of glyphs", @@ -6412,6 +6520,10 @@ "$ref": "#/definitions/NameAndDoc", "description": "Whether a phrase, group, or stage is flipped vertically" }, + "matter": { + "$ref": "#/definitions/NameAndDoc", + "description": "The matter properties for the phrase" + }, "moving": { "$ref": "#/definitions/NameAndDoc", "description": "Pose or sequence for when a phrase, group, or stage is moving" @@ -6481,6 +6593,7 @@ "face", "flipx", "flipy", + "matter", "moving", "name", "names", @@ -6510,6 +6623,10 @@ "$ref": "#/definitions/NameText", "description": "One or more names for this definition. Be careful not to introduce duplicates." }, + "rotation": { + "$ref": "#/definitions/NameAndDoc", + "description": "optional rotation" + }, "x": { "$ref": "#/definitions/NameAndDoc", "description": "x-coordinate" @@ -6526,6 +6643,7 @@ "required": [ "doc", "names", + "rotation", "x", "y", "z" @@ -6597,24 +6715,32 @@ "description": "A rectangle shape, for Stage.frame", "properties": { "bottom": { - "$ref": "#/definitions/NameAndDoc" + "$ref": "#/definitions/NameAndDoc", + "description": "Bottom of the rectangle" }, "doc": { "$ref": "#/definitions/DocText", "description": "Documentation for this definition, to appear in the documentation browser." }, "left": { - "$ref": "#/definitions/NameAndDoc" + "$ref": "#/definitions/NameAndDoc", + "description": "Left of the rectangle" }, "names": { "$ref": "#/definitions/NameText", "description": "One or more names for this definition. Be careful not to introduce duplicates." }, "right": { - "$ref": "#/definitions/NameAndDoc" + "$ref": "#/definitions/NameAndDoc", + "description": "Right of the rectangle" }, "top": { - "$ref": "#/definitions/NameAndDoc" + "$ref": "#/definitions/NameAndDoc", + "description": "Top of the rectangle" + }, + "z": { + "$ref": "#/definitions/NameAndDoc", + "description": "Depth of rectangle" } }, "required": [ @@ -6623,7 +6749,8 @@ "left", "names", "right", - "top" + "top", + "z" ], "type": "object" }, @@ -6701,8 +6828,108 @@ "type": "object" }, "Shape": { - "$ref": "#/definitions/NameAndDoc", - "description": "The base interface for shape types" + "additionalProperties": false, + "description": "The base interface for shape types", + "properties": { + "background": { + "$ref": "#/definitions/NameAndDoc", + "description": "The background color behind a phrase, group, or stage" + }, + "color": { + "$ref": "#/definitions/NameAndDoc", + "description": "The color of glyphs in a phrase, group, or stage" + }, + "doc": { + "$ref": "#/definitions/DocText", + "description": "Documentation for this definition, to appear in the documentation browser." + }, + "duration": { + "$ref": "#/definitions/NameAndDoc", + "description": "The curation of transition" + }, + "entering": { + "$ref": "#/definitions/NameAndDoc", + "description": "Pose or sequence for when a phrase, group, or stage enters stage" + }, + "exiting": { + "$ref": "#/definitions/NameAndDoc", + "description": "Pose or sequence for when a phrase, group, or stage is leaving stage" + }, + "flipx": { + "$ref": "#/definitions/NameAndDoc", + "description": "Whether a phrase, group, or stage is flipped horizontally" + }, + "flipy": { + "$ref": "#/definitions/NameAndDoc", + "description": "Whether a phrase, group, or stage is flipped vertically" + }, + "form": { + "$ref": "#/definitions/NameAndDoc", + "description": "The kind of shape and its details" + }, + "moving": { + "$ref": "#/definitions/NameAndDoc", + "description": "Pose or sequence for when a phrase, group, or stage is moving" + }, + "name": { + "$ref": "#/definitions/NameAndDoc", + "description": "The name of a phrase, group, or stage, used in Choice, Collision, and animations" + }, + "names": { + "$ref": "#/definitions/NameText", + "description": "One or more names for this definition. Be careful not to introduce duplicates." + }, + "offset": { + "$ref": "#/definitions/NameAndDoc", + "description": "The offset of phrase, group, or stage from its place" + }, + "opacity": { + "$ref": "#/definitions/NameAndDoc", + "description": "The opacity of a phrase, group, or stage" + }, + "resting": { + "$ref": "#/definitions/NameAndDoc", + "description": "Pose or sequence for when a phrase, group, or stage is not moving" + }, + "rotation": { + "$ref": "#/definitions/NameAndDoc", + "description": "The rotation of a phrase, group, or stage" + }, + "scale": { + "$ref": "#/definitions/NameAndDoc", + "description": "The scale of phrase, group, or stage" + }, + "selectable": { + "$ref": "#/definitions/NameAndDoc", + "description": "Whether a phrase, group, or stage is selectable by Choice" + }, + "style": { + "$ref": "#/definitions/NameAndDoc", + "description": "The transition style of transitions" + } + }, + "required": [ + "background", + "color", + "doc", + "duration", + "entering", + "exiting", + "flipx", + "flipy", + "form", + "moving", + "name", + "names", + "offset", + "opacity", + "resting", + "rotation", + "scale", + "selectable", + "style" + ], + "type": "object" }, "Stack": { "additionalProperties": false, @@ -6790,6 +7017,10 @@ "$ref": "#/definitions/NameAndDoc", "description": "The shape of the frame to clip stage content" }, + "gravity": { + "$ref": "#/definitions/NameAndDoc", + "description": "Gravity, influencing change in y velocity" + }, "moving": { "$ref": "#/definitions/NameAndDoc", "description": "Pose or sequence for when a phrase, group, or stage is moving" @@ -6852,6 +7083,7 @@ "flipx", "flipy", "frame", + "gravity", "moving", "name", "names", @@ -6867,9 +7099,39 @@ ], "type": "object" }, - "Type": { - "$ref": "#/definitions/NameAndDoc", - "description": "The base interface for Phrase, Group, and Stage" + "Velocity": { + "additionalProperties": false, + "description": "A velocity vector", + "properties": { + "angle": { + "$ref": "#/definitions/NameAndDoc", + "description": "rotation" + }, + "doc": { + "$ref": "#/definitions/DocText", + "description": "Documentation for this definition, to appear in the documentation browser." + }, + "names": { + "$ref": "#/definitions/NameText", + "description": "One or more names for this definition. Be careful not to introduce duplicates." + }, + "x": { + "$ref": "#/definitions/NameAndDoc", + "description": "x-coordinate" + }, + "y": { + "$ref": "#/definitions/NameAndDoc", + "description": "y-coordinate" + } + }, + "required": [ + "angle", + "doc", + "names", + "x", + "y" + ], + "type": "object" }, "sequence": { "additionalProperties": false, @@ -6952,7 +7214,7 @@ } }, "required": [ - "Type", + "Output", "Group", "Phrase", "Stage", @@ -6962,6 +7224,8 @@ "Sequence", "Color", "Place", + "Velocity", + "Matter", "Arrangement", "Row", "Stack", @@ -8473,10 +8737,22 @@ "description": "Add a group to the output", "type": "string" }, + "addMotion": { + "description": "Set place to Motion stream", + "type": "string" + }, "addPhrase": { "description": "Add a phrase to the output", "type": "string" }, + "addPlacement": { + "description": "Set place to Placement stream", + "type": "string" + }, + "addShape": { + "description": "Add a shape to the output", + "type": "string" + }, "createGroup": { "description": "The button that creates a group when there is none", "type": "string" @@ -8523,6 +8799,9 @@ "set", "addGroup", "addPhrase", + "addShape", + "addMotion", + "addPlacement", "remove", "up", "down", diff --git a/static/schemas/Tutorial.json b/static/schemas/Tutorial.json index 9b72fe4ed..abb1a8f5e 100644 --- a/static/schemas/Tutorial.json +++ b/static/schemas/Tutorial.json @@ -138,7 +138,10 @@ "Placement", "Chat", "Webpage", - "Type", + "Collision", + "Rebound", + "Direction", + "Output", "Group", "Phrase", "Stage", @@ -148,6 +151,8 @@ "Sequence", "Color", "Place", + "Velocity", + "Matter", "Arrangement", "Stack", "Grid", diff --git a/static/style/global.css b/static/style/global.css index 9ecc073fa..7d7ec8b00 100644 --- a/static/style/global.css +++ b/static/style/global.css @@ -68,7 +68,7 @@ --color-black: #000000; --color-light-grey: #484848; --color-very-light-grey: #252525; - --color-dark-grey: #afafaf; + --color-dark-grey: #595256; --color-shadow: rgb(32, 32, 32, 0.2); --wordplay-foreground: var(--color-white);