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/README.md +160 -152
- package/lib/engine/renderables.d.ts +8 -0
- package/lib/engine/types.d.ts +24 -4
- package/lib/index.cjs +286 -25
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.ts +2 -1
- package/lib/index.js +280 -26
- package/lib/index.js.map +1 -1
- package/package.json +4 -2
- package/lib/utils/constants.d.ts +0 -6
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
|
-
|
|
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:
|
|
195
|
-
height:
|
|
263
|
+
width: sheetSize.width / slices.horizontal,
|
|
264
|
+
height: sheetSize.height / slices.vertical,
|
|
196
265
|
},
|
|
197
|
-
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 = () =>
|
|
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 *
|
|
567
|
-
y: (ev.offsetY *
|
|
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
|
-
|
|
577
|
-
|
|
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
|
-
|
|
616
|
-
|
|
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
|
|
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
|
-
|
|
651
|
-
|
|
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,
|
|
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:
|
|
845
|
-
y: Math.floor(
|
|
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,
|
|
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
|
-
|
|
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";
|