yuuna-engine 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.cjs CHANGED
@@ -149,6 +149,12 @@ const createPlaceholderSheet = (size) => {
149
149
  }
150
150
  return canvas;
151
151
  };
152
+ // Used only when a resource declares neither `size` (see settle() below)
153
+ // nor actually loads — there's no image to measure and nothing declared
154
+ // to fall back to, so there's no way to know the intended dimensions at
155
+ // all. An arbitrary, small-but-visible size, purely so the placeholder
156
+ // still draws as *something* instead of a 0x0/NaN canvas.
157
+ const DEFAULT_PLACEHOLDER_SIZE = { width: 64, height: 64 };
152
158
  const runEngine = async (props) => {
153
159
  const runId = ++latestRunId;
154
160
  resetCanvas?.();
@@ -172,9 +178,64 @@ const runEngine = async (props) => {
172
178
  if (props.canvas?.backgroundColor !== undefined) {
173
179
  canvas.style.backgroundColor = props.canvas.backgroundColor;
174
180
  }
181
+ // The canvas's *logical* resolution — the fixed space every renderable
182
+ // position, and every mouse/touch coordinate, is expressed in. Captured
183
+ // here, before pixelRatio (below) scales the actual backing buffer past
184
+ // it, so both stay anchored to this regardless of what pixelRatio does.
185
+ const logicalWidth = canvas.width;
186
+ const logicalHeight = canvas.height;
187
+ // Scales the backing buffer beyond logicalWidth/logicalHeight so text
188
+ // and vector shapes (fillText, arc, ...) render crisply on a HiDPI
189
+ // screen — most phones — instead of the same logical-resolution buffer
190
+ // just being stretched larger by applyResize below. `true` follows the
191
+ // display's own devicePixelRatio; a number sets it explicitly; leaving
192
+ // this unset keeps today's 1x behavior. Sprites are unaffected either
193
+ // way — imageSmoothingEnabled stays false below regardless — so pixel
194
+ // art has no reason to turn this on.
195
+ const pixelRatio = props.canvas?.pixelRatio === true ? window.devicePixelRatio || 1 : props.canvas?.pixelRatio ?? 1;
196
+ if (pixelRatio !== 1) {
197
+ // Resizing the backing buffer resets the 2D context's transform (and
198
+ // everything else about its state), so context.scale below has to
199
+ // come after this, not before.
200
+ canvas.width = logicalWidth * pixelRatio;
201
+ canvas.height = logicalHeight * pixelRatio;
202
+ canvas.style.width = `${logicalWidth}px`;
203
+ canvas.style.height = `${logicalHeight}px`;
204
+ context.scale(pixelRatio, pixelRatio);
205
+ }
175
206
  // Make the canvas focusable so keyboard input is scoped to it instead of
176
207
  // leaking to the rest of the page (e.g. arrow keys scrolling the window).
177
208
  canvas.tabIndex = 0;
209
+ // Stops the browser from treating a drag/pinch on the canvas as page
210
+ // scroll/zoom — backs up the touchstart/touchmove preventDefault calls
211
+ // below for gestures (e.g. a pinch starting on the canvas) preventDefault
212
+ // alone doesn't reliably stop.
213
+ canvas.style.touchAction = "none";
214
+ // Resizes the *display* size only (CSS width/height) — canvas.width/
215
+ // height above stays the fixed logical resolution every renderable's
216
+ // position is already expressed in, so this never needs to touch any
217
+ // of that math. See RunEngineProps.canvas.resize for the mode semantics.
218
+ const applyResize = () => {
219
+ const mode = props.canvas?.resize;
220
+ if (mode === undefined || mode === "none") {
221
+ return;
222
+ }
223
+ if (mode === "stretch") {
224
+ canvas.style.width = `${window.innerWidth}px`;
225
+ canvas.style.height = `${window.innerHeight}px`;
226
+ return;
227
+ }
228
+ const scale = Math.min(window.innerWidth / canvas.width, window.innerHeight / canvas.height);
229
+ canvas.style.width = `${canvas.width * scale}px`;
230
+ canvas.style.height = `${canvas.height * scale}px`;
231
+ };
232
+ applyResize();
233
+ window.addEventListener("resize", applyResize);
234
+ const requestFullscreen = () => canvas.requestFullscreen();
235
+ // exitFullscreen() rejects with a TypeError if nothing is fullscreen —
236
+ // guarded into a no-op instead, so callers don't need to track that
237
+ // state themselves just to call this safely.
238
+ const exitFullscreen = () => window.document.fullscreenElement === null ? Promise.resolve() : window.document.exitFullscreen();
178
239
  let state = props.initialState;
179
240
  const events = [];
180
241
  // Lets a caller report something that happened outside the render loop
@@ -187,24 +248,35 @@ const runEngine = async (props) => {
187
248
  const resources = props.resources ?? {};
188
249
  const resourceById = await iterateRecordAsync(resources, async ({ value }) => new Promise((resolve) => {
189
250
  const image = new Image();
190
- const settle = (loadedImage) => {
251
+ // A single, unsliced image (1x1) if unset — only an actual
252
+ // spritesheet needs this declared.
253
+ const slices = value.slices ?? { horizontal: 1, vertical: 1 };
254
+ // sheetSize is the whole loaded sheet's pixel dimensions — value's
255
+ // declared `size` if set, otherwise whatever the image actually
256
+ // measures once it's loaded (or DEFAULT_PLACEHOLDER_SIZE if even
257
+ // that isn't available, i.e. no `size` declared *and* the image
258
+ // failed to load too).
259
+ const settle = (loadedImage, sheetSize) => {
191
260
  resolve({
192
261
  image: loadedImage,
193
262
  size: {
194
- width: value.size.width / value.slices.horizontal,
195
- height: value.size.height / value.slices.vertical,
263
+ width: sheetSize.width / slices.horizontal,
264
+ height: sheetSize.height / slices.vertical,
196
265
  },
197
- slices: value.slices,
266
+ slices,
198
267
  animations: value.animations ?? {},
199
268
  });
200
269
  };
201
270
  image.src = value.src;
202
- image.onload = () => settle(image);
271
+ image.onload = () => settle(image, value.size ?? { width: image.naturalWidth, height: image.naturalHeight });
203
272
  // Missing/failed-to-load asset (see .gitignore's dist/resources/
204
273
  // note) — a placeholder sheet, sized to match what this resource
205
274
  // declared, keeps every frame/slice/animation index the example
206
275
  // already computes valid instead of drawing nothing or throwing.
207
- image.onerror = () => settle(createPlaceholderSheet(value.size));
276
+ image.onerror = () => {
277
+ const placeholderSize = value.size ?? DEFAULT_PLACEHOLDER_SIZE;
278
+ settle(createPlaceholderSheet(placeholderSize), placeholderSize);
279
+ };
208
280
  }));
209
281
  const loadAudio = (src) => new Promise((resolve) => {
210
282
  const audio = new Audio(src);
@@ -281,9 +353,11 @@ const runEngine = async (props) => {
281
353
  };
282
354
  // A newer runEngine() call started while this one was still loading
283
355
  // resources (e.g. a spritesheet) — abandon this run instead of setting
284
- // up a second, orphaned render loop alongside the newer one.
356
+ // up a second, orphaned render loop alongside the newer one. The
357
+ // fullscreen functions are no-ops here since this run never gets far
358
+ // enough to own the canvas — the newer run's are the ones that matter.
285
359
  if (runId !== latestRunId) {
286
- return { sendEvent };
360
+ return { sendEvent, requestFullscreen: () => Promise.resolve(), exitFullscreen: () => Promise.resolve() };
287
361
  }
288
362
  context.imageSmoothingEnabled = false;
289
363
  // An offscreen 1x1 canvas used only to resolve a CSS color string (a
@@ -563,8 +637,20 @@ const runEngine = async (props) => {
563
637
  // the same conversion to line up.
564
638
  const getCanvasPosition = (ev) => {
565
639
  return {
566
- x: (ev.offsetX * canvas.width) / canvas.clientWidth,
567
- y: (ev.offsetY * canvas.height) / canvas.clientHeight,
640
+ x: (ev.offsetX * logicalWidth) / canvas.clientWidth,
641
+ y: (ev.offsetY * logicalHeight) / canvas.clientHeight,
642
+ };
643
+ };
644
+ // Touch's equivalent of getCanvasPosition — a Touch has no offsetX/Y
645
+ // (that's a MouseEvent-only convenience), so this gets there manually
646
+ // via getBoundingClientRect instead. clientX/Y and the rect are both
647
+ // viewport-relative, so their difference stays correct regardless of
648
+ // page scroll.
649
+ const getTouchPosition = (touch) => {
650
+ const rect = canvas.getBoundingClientRect();
651
+ return {
652
+ x: ((touch.clientX - rect.left) * logicalWidth) / canvas.clientWidth,
653
+ y: ((touch.clientY - rect.top) * logicalHeight) / canvas.clientHeight,
568
654
  };
569
655
  };
570
656
  const updateState = (updateFn) => {
@@ -573,8 +659,11 @@ const runEngine = async (props) => {
573
659
  const nextStateFns = Array.isArray(props.nextState) ? props.nextState : [props.nextState];
574
660
  let lastFrame = Date.now();
575
661
  let hoveredId = null;
576
- const handleClick = (ev) => {
577
- const mouse = getCanvasPosition(ev);
662
+ // Shared by the mouse "click" listener and the touch handlers below —
663
+ // firing a CLICK is the same "is whatever's currently hovered
664
+ // isClickable" check either way; only how `mouse` was determined
665
+ // differs (a real click event vs. a lifted finger).
666
+ const fireClick = (mouse) => {
578
667
  if (hoveredId === null)
579
668
  return;
580
669
  const { renderables, camera } = renderState(state);
@@ -583,6 +672,7 @@ const runEngine = async (props) => {
583
672
  events.push({ tag: "CLICK", id: hovered.id, mouse, worldMouse: toWorldPosition(mouse, camera) });
584
673
  }
585
674
  };
675
+ const handleClick = (ev) => fireClick(getCanvasPosition(ev));
586
676
  canvas.addEventListener("click", handleClick);
587
677
  const initialState = {
588
678
  keyboardState: createRecord(keyboardKeys, () => false),
@@ -612,8 +702,34 @@ const runEngine = async (props) => {
612
702
  }
613
703
  };
614
704
  canvas.addEventListener("keyup", handleKeyUp);
615
- const handleMouseMoveHover = (ev) => {
616
- const mouse = getCanvasPosition(ev);
705
+ // mouseButton state (see NextStateProps.mouseButton) — a double-buffer
706
+ // exactly like keyboardState above, just for the primary mouse button.
707
+ let previousMouseButtonState = { isPressed: false };
708
+ const currentMouseButtonState = { isPressed: false };
709
+ const handleMouseDown = (event) => {
710
+ // Left/primary button only — matching CLICK, which already only
711
+ // ever fires for it.
712
+ if (event.button === 0) {
713
+ currentMouseButtonState.isPressed = true;
714
+ }
715
+ };
716
+ canvas.addEventListener("mousedown", handleMouseDown);
717
+ const handleMouseUp = (event) => {
718
+ if (event.button === 0) {
719
+ currentMouseButtonState.isPressed = false;
720
+ }
721
+ };
722
+ // On window rather than the canvas — so releasing the button after
723
+ // having dragged off the canvas while still holding it down still
724
+ // clears isPressed, instead of leaving it stuck true forever.
725
+ window.addEventListener("mouseup", handleMouseUp);
726
+ // Shared by mousemove and the touch handlers below — updates hoveredId
727
+ // from a canvas position and fires HOVER_IN/HOVER_OUT as whatever's
728
+ // underneath it changes. Touch has no ambient hover the way a mouse
729
+ // does (nothing is "hovered" until a finger actually touches down), but
730
+ // feeding a touch's position through this the same as the mouse's is
731
+ // what lets an isHoverable renderable react to a tap/drag at all.
732
+ const updateHover = (mouse) => {
617
733
  const { renderables, camera } = renderState(state);
618
734
  const worldMouse = toWorldPosition(mouse, camera);
619
735
  const hovered = [...renderables]
@@ -630,9 +746,9 @@ const runEngine = async (props) => {
630
746
  }
631
747
  hoveredId = hovered === undefined ? null : hovered.id ?? null;
632
748
  };
749
+ const handleMouseMoveHover = (ev) => updateHover(getCanvasPosition(ev));
633
750
  canvas.addEventListener("mousemove", handleMouseMoveHover);
634
- const handleMouseMoveTracking = (ev) => {
635
- const mouse = getCanvasPosition(ev);
751
+ const updateTracking = (mouse) => {
636
752
  if (hoveredId === null) {
637
753
  return;
638
754
  }
@@ -642,13 +758,17 @@ const runEngine = async (props) => {
642
758
  events.push({ tag: "MOUSE_MOVE", mouse, worldMouse: toWorldPosition(mouse, camera), id: hovered.id });
643
759
  }
644
760
  };
761
+ const handleMouseMoveTracking = (ev) => updateTracking(getCanvasPosition(ev));
645
762
  canvas.addEventListener("mousemove", handleMouseMoveTracking);
646
763
  // No further mousemove fires once the mouse is off the canvas, so this
647
764
  // is also the only chance to report a HOVER_OUT for whatever was
648
765
  // hovered when it left — otherwise that hover would just dangle,
649
766
  // never explicitly ended.
650
- const handleMouseLeave = (ev) => {
651
- const mouse = getCanvasPosition(ev);
767
+ // Shared by mouseleave and the touch handlers below — clears whatever's
768
+ // hovered (firing HOVER_OUT for it) without a HOVER_IN taking its
769
+ // place. Returns worldMouse so callers that need it (MOUSE_LEAVE below)
770
+ // don't have to call renderState() a second time just to get it.
771
+ const clearHover = (mouse) => {
652
772
  const { renderables, camera } = renderState(state);
653
773
  const worldMouse = toWorldPosition(mouse, camera);
654
774
  if (hoveredId !== null) {
@@ -658,9 +778,79 @@ const runEngine = async (props) => {
658
778
  }
659
779
  }
660
780
  hoveredId = null;
781
+ return worldMouse;
782
+ };
783
+ const handleMouseLeave = (ev) => {
784
+ const mouse = getCanvasPosition(ev);
785
+ const worldMouse = clearHover(mouse);
661
786
  events.push({ tag: "MOUSE_LEAVE", mouse, worldMouse });
662
787
  };
663
788
  canvas.addEventListener("mouseleave", handleMouseLeave);
789
+ // Translates touch into the same HOVER_IN/HOVER_OUT/MOUSE_MOVE/CLICK
790
+ // events mouse input already produces (via updateHover/updateTracking/
791
+ // fireClick/clearHover above), so existing game code written against
792
+ // those events works on a touchscreen with no changes of its own.
793
+ // Single-touch only — touches[0]/changedTouches[0] — the same "one
794
+ // active pointer" model mouse input already assumes; a second finger is
795
+ // ignored rather than tracked as its own pointer.
796
+ //
797
+ // preventDefault on start/move keeps a drag/tap on the canvas from also
798
+ // scrolling, pinch-zooming, or triggering pull-to-refresh — the browser
799
+ // gestures a touchscreen normally reserves that space for; { passive:
800
+ // false } is what makes preventDefault actually take effect here.
801
+ const handleTouchStart = (ev) => {
802
+ ev.preventDefault();
803
+ const touch = ev.touches[0];
804
+ if (touch === undefined)
805
+ return;
806
+ currentMouseButtonState.isPressed = true;
807
+ const mouse = getTouchPosition(touch);
808
+ updateHover(mouse);
809
+ updateTracking(mouse);
810
+ };
811
+ canvas.addEventListener("touchstart", handleTouchStart, { passive: false });
812
+ const handleTouchMove = (ev) => {
813
+ ev.preventDefault();
814
+ const touch = ev.touches[0];
815
+ if (touch === undefined)
816
+ return;
817
+ const mouse = getTouchPosition(touch);
818
+ updateHover(mouse);
819
+ updateTracking(mouse);
820
+ };
821
+ canvas.addEventListener("touchmove", handleTouchMove, { passive: false });
822
+ // A lifted finger both releases (mirroring mouseup) and clicks
823
+ // (mirroring the browser's own click-after-mouseup) — touch has no
824
+ // separate "up" and "click" events of its own the way mouse does, so
825
+ // both happen here together, in that order. hover is updated once more
826
+ // first so a plain tap (touchstart immediately followed by touchend,
827
+ // with no touchmove between them to have already done this) still
828
+ // fires CLICK against whatever's actually under it; hover is then
829
+ // cleared, since nothing's left touching it once the finger lifts.
830
+ const handleTouchEnd = (ev) => {
831
+ ev.preventDefault();
832
+ currentMouseButtonState.isPressed = false;
833
+ const touch = ev.changedTouches[0];
834
+ if (touch === undefined)
835
+ return;
836
+ const mouse = getTouchPosition(touch);
837
+ updateHover(mouse);
838
+ fireClick(mouse);
839
+ clearHover(mouse);
840
+ };
841
+ canvas.addEventListener("touchend", handleTouchEnd, { passive: false });
842
+ // A cancelled touch (e.g. an incoming call interrupting the page, or
843
+ // the OS deciding it's a system gesture instead) never fires touchend —
844
+ // handled the same as lifting the finger, minus the click, since
845
+ // there's no tap to speak of once the touch itself has been cancelled.
846
+ const handleTouchCancel = (ev) => {
847
+ currentMouseButtonState.isPressed = false;
848
+ const touch = ev.changedTouches[0];
849
+ if (touch === undefined)
850
+ return;
851
+ clearHover(getTouchPosition(touch));
852
+ };
853
+ canvas.addEventListener("touchcancel", handleTouchCancel, { passive: false });
664
854
  // Tab switches are a document-level concern (visibilitychange), not
665
855
  // something that ever reaches the canvas itself the way mouse/keyboard
666
856
  // events do.
@@ -668,6 +858,17 @@ const runEngine = async (props) => {
668
858
  events.push({ tag: window.document.hidden ? "TAB_BLUR" : "TAB_FOCUS" });
669
859
  };
670
860
  window.document.addEventListener("visibilitychange", handleVisibilityChange);
861
+ // Fullscreen is a document-level concern too, and fires for every way
862
+ // fullscreen can change — the requestFullscreen()/exitFullscreen()
863
+ // above, but also things neither of those causes directly, like the
864
+ // user pressing Esc. Re-running applyResize() here (rather than relying
865
+ // solely on the "resize" listener) covers browsers that don't also fire
866
+ // a window resize when fullscreen is toggled.
867
+ const handleFullscreenChange = () => {
868
+ events.push({ tag: "FULLSCREEN_CHANGE", isFullscreen: window.document.fullscreenElement === canvas });
869
+ applyResize();
870
+ };
871
+ window.document.addEventListener("fullscreenchange", handleFullscreenChange);
671
872
  context.imageSmoothingEnabled = false;
672
873
  // Scale is a canvas transform around the renderable's anchor, applied
673
874
  // before its type-specific drawing runs below — everything drawn under
@@ -781,11 +982,17 @@ const runEngine = async (props) => {
781
982
  isJustReleased: !isPressed && previouslyPressed,
782
983
  };
783
984
  });
985
+ const mouseButton = {
986
+ isPressed: currentMouseButtonState.isPressed,
987
+ isJustPressed: currentMouseButtonState.isPressed && !previousMouseButtonState.isPressed,
988
+ isJustReleased: !currentMouseButtonState.isPressed && previousMouseButtonState.isPressed,
989
+ };
784
990
  for (const nextState of nextStateFns) {
785
991
  const result = nextState({
786
992
  state,
787
993
  event,
788
994
  keyboard,
995
+ mouseButton,
789
996
  playSound,
790
997
  playMusic,
791
998
  pauseMusic,
@@ -805,7 +1012,7 @@ const runEngine = async (props) => {
805
1012
  }
806
1013
  }
807
1014
  events.splice(0, events.length);
808
- context.clearRect(0, 0, canvas.width, canvas.height);
1015
+ context.clearRect(0, 0, logicalWidth, logicalHeight);
809
1016
  const { cursor, renderables } = renderState(state);
810
1017
  canvas.style.cursor = cursor ?? "default";
811
1018
  for (const renderable of renderables) {
@@ -838,12 +1045,11 @@ const runEngine = async (props) => {
838
1045
  continue;
839
1046
  }
840
1047
  if (renderable.type === "SPRITE") {
841
- const { opacity = 1, flipX = false, modulate, swapColors } = renderable;
1048
+ const { opacity = 1, flipX = false, frame: frameIndex = 0, modulate, swapColors } = renderable;
842
1049
  const resource = resourceById[renderable.resourceId];
843
1050
  const frame = {
844
- x: renderable.frame % resource.slices.horizontal,
845
- y: Math.floor(renderable.frame / resource.slices.horizontal) %
846
- resource.slices.vertical,
1051
+ x: frameIndex % resource.slices.horizontal,
1052
+ y: Math.floor(frameIndex / resource.slices.horizontal) % resource.slices.vertical,
847
1053
  };
848
1054
  const source = {
849
1055
  x: frame.x * resource.size.width,
@@ -863,7 +1069,7 @@ const runEngine = async (props) => {
863
1069
  let image = resource.image;
864
1070
  let imageSource = source;
865
1071
  if (swapColors !== undefined && swapColors.length > 0) {
866
- image = swappedSpriteFrame(renderable.resourceId, renderable.frame, image, imageSource, swapColors);
1072
+ image = swappedSpriteFrame(renderable.resourceId, frameIndex, image, imageSource, swapColors);
867
1073
  imageSource = { x: 0, y: 0, width: source.width, height: source.height };
868
1074
  }
869
1075
  if (modulate !== undefined) {
@@ -917,6 +1123,7 @@ const runEngine = async (props) => {
917
1123
  }
918
1124
  lastFrame = now;
919
1125
  previousState.keyboardState = { ...currentState.keyboardState };
1126
+ previousMouseButtonState = { ...currentMouseButtonState };
920
1127
  }, 0);
921
1128
  resetCanvas = () => {
922
1129
  clearInterval(intervalId);
@@ -940,16 +1147,70 @@ const runEngine = async (props) => {
940
1147
  canvas.removeEventListener("click", handleClick);
941
1148
  canvas.removeEventListener("keydown", handleKeyDown);
942
1149
  canvas.removeEventListener("keyup", handleKeyUp);
1150
+ canvas.removeEventListener("mousedown", handleMouseDown);
943
1151
  canvas.removeEventListener("mousemove", handleMouseMoveHover);
944
1152
  canvas.removeEventListener("mousemove", handleMouseMoveTracking);
945
1153
  canvas.removeEventListener("mouseleave", handleMouseLeave);
1154
+ canvas.removeEventListener("touchstart", handleTouchStart);
1155
+ canvas.removeEventListener("touchmove", handleTouchMove);
1156
+ canvas.removeEventListener("touchend", handleTouchEnd);
1157
+ canvas.removeEventListener("touchcancel", handleTouchCancel);
1158
+ // On window, not the canvas — see where it's added above for why.
1159
+ window.removeEventListener("mouseup", handleMouseUp);
946
1160
  // This one's on `document`, not the canvas — same reasoning as above,
947
1161
  // just doubly true since `document` isn't even scoped to this canvas.
948
1162
  window.document.removeEventListener("visibilitychange", handleVisibilityChange);
1163
+ window.document.removeEventListener("fullscreenchange", handleFullscreenChange);
1164
+ // Same pile-up risk as the canvas listeners above, but on `window`.
1165
+ window.removeEventListener("resize", applyResize);
949
1166
  };
950
- return { sendEvent };
1167
+ return { sendEvent, requestFullscreen, exitFullscreen };
951
1168
  };
952
1169
 
1170
+ // One factory per Renderable variant — literally just `{ type: "X",
1171
+ // ...props }`, so `sprite({...})` (or `Yuuna.sprite({...})` in the
1172
+ // browser bundle/playground) reads the same as writing the object
1173
+ // literal by hand, minus needing to get `type` right yourself.
1174
+ // Deliberately not a place defaults live: besides SpriteRenderable.frame
1175
+ // (optional at the type level, see types.ts — the engine itself defaults
1176
+ // it to 0), each factory still requires whatever its Renderable type
1177
+ // still requires.
1178
+ const rectangle = (props) => ({
1179
+ type: "RECTANGLE",
1180
+ ...props,
1181
+ });
1182
+ const circle = (props) => ({
1183
+ type: "CIRCLE",
1184
+ ...props,
1185
+ });
1186
+ const text = (props) => ({
1187
+ type: "TEXT",
1188
+ ...props,
1189
+ });
1190
+ const sprite = (props) => ({
1191
+ type: "SPRITE",
1192
+ ...props,
1193
+ });
1194
+ const animatedSprite = (props) => ({
1195
+ type: "ANIMATED_SPRITE",
1196
+ ...props,
1197
+ });
1198
+ const line = (props) => ({
1199
+ type: "LINE",
1200
+ ...props,
1201
+ });
1202
+ const group = (props) => ({
1203
+ type: "GROUP",
1204
+ ...props,
1205
+ });
1206
+
953
1207
  exports.STOP = STOP;
1208
+ exports.animatedSprite = animatedSprite;
1209
+ exports.circle = circle;
1210
+ exports.group = group;
1211
+ exports.line = line;
1212
+ exports.rectangle = rectangle;
954
1213
  exports.runEngine = runEngine;
1214
+ exports.sprite = sprite;
1215
+ exports.text = text;
955
1216
  //# sourceMappingURL=index.cjs.map
package/lib/index.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/lib/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { runEngine } from "./engine/runEngine";
2
2
  import { STOP } from "./engine/types";
3
- export { runEngine, STOP };
3
+ import { animatedSprite, circle, group, line, rectangle, sprite, text } from "./engine/renderables";
4
+ export { runEngine, STOP, rectangle, circle, text, sprite, animatedSprite, line, group };
4
5
  export type { NextStateFunction, NextStateProps, Renderable, GameEvent, CustomGameEvent, } from "./engine/types";