yuuna-engine 0.6.0 → 0.8.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 +177 -156
- package/lib/engine/renderables.d.ts +8 -0
- package/lib/engine/types.d.ts +33 -17
- package/lib/index.cjs +298 -41
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.ts +3 -2
- package/lib/index.js +292 -42
- 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,48 @@ 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 };
|
|
158
|
+
// Loads a single resources[id] entry into a ResourceEntry — shared by the
|
|
159
|
+
// startup resources (below) and addResource (returned from runEngine, at
|
|
160
|
+
// the bottom of this file) so a resource registered later loads exactly
|
|
161
|
+
// the same way, placeholder fallback included.
|
|
162
|
+
const loadResource = (value) => new Promise((resolve) => {
|
|
163
|
+
const image = new Image();
|
|
164
|
+
// A single, unsliced image (1x1) if unset — only an actual
|
|
165
|
+
// spritesheet needs this declared.
|
|
166
|
+
const slices = value.slices ?? { horizontal: 1, vertical: 1 };
|
|
167
|
+
// sheetSize is the whole loaded sheet's pixel dimensions — value's
|
|
168
|
+
// declared `size` if set, otherwise whatever the image actually
|
|
169
|
+
// measures once it's loaded (or DEFAULT_PLACEHOLDER_SIZE if even
|
|
170
|
+
// that isn't available, i.e. no `size` declared *and* the image
|
|
171
|
+
// failed to load too).
|
|
172
|
+
const settle = (loadedImage, sheetSize) => {
|
|
173
|
+
resolve({
|
|
174
|
+
image: loadedImage,
|
|
175
|
+
size: {
|
|
176
|
+
width: sheetSize.width / slices.horizontal,
|
|
177
|
+
height: sheetSize.height / slices.vertical,
|
|
178
|
+
},
|
|
179
|
+
slices,
|
|
180
|
+
animations: value.animations ?? {},
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
image.src = value.src;
|
|
184
|
+
image.onload = () => settle(image, value.size ?? { width: image.naturalWidth, height: image.naturalHeight });
|
|
185
|
+
// Missing/failed-to-load asset (see .gitignore's dist/resources/
|
|
186
|
+
// note) — a placeholder sheet, sized to match what this resource
|
|
187
|
+
// declared, keeps every frame/slice/animation index the caller
|
|
188
|
+
// already computes valid instead of drawing nothing or throwing.
|
|
189
|
+
image.onerror = () => {
|
|
190
|
+
const placeholderSize = value.size ?? DEFAULT_PLACEHOLDER_SIZE;
|
|
191
|
+
settle(createPlaceholderSheet(placeholderSize), placeholderSize);
|
|
192
|
+
};
|
|
193
|
+
});
|
|
152
194
|
const runEngine = async (props) => {
|
|
153
195
|
const runId = ++latestRunId;
|
|
154
196
|
resetCanvas?.();
|
|
@@ -172,9 +214,39 @@ const runEngine = async (props) => {
|
|
|
172
214
|
if (props.canvas?.backgroundColor !== undefined) {
|
|
173
215
|
canvas.style.backgroundColor = props.canvas.backgroundColor;
|
|
174
216
|
}
|
|
217
|
+
// The canvas's *logical* resolution — the fixed space every renderable
|
|
218
|
+
// position, and every mouse/touch coordinate, is expressed in. Captured
|
|
219
|
+
// here, before pixelRatio (below) scales the actual backing buffer past
|
|
220
|
+
// it, so both stay anchored to this regardless of what pixelRatio does.
|
|
221
|
+
const logicalWidth = canvas.width;
|
|
222
|
+
const logicalHeight = canvas.height;
|
|
223
|
+
// Scales the backing buffer beyond logicalWidth/logicalHeight so text
|
|
224
|
+
// and vector shapes (fillText, arc, ...) render crisply on a HiDPI
|
|
225
|
+
// screen — most phones — instead of the same logical-resolution buffer
|
|
226
|
+
// just being stretched larger by applyResize below. `true` follows the
|
|
227
|
+
// display's own devicePixelRatio; a number sets it explicitly; leaving
|
|
228
|
+
// this unset keeps today's 1x behavior. Sprites are unaffected either
|
|
229
|
+
// way — imageSmoothingEnabled stays false below regardless — so pixel
|
|
230
|
+
// art has no reason to turn this on.
|
|
231
|
+
const pixelRatio = props.canvas?.pixelRatio === true ? window.devicePixelRatio || 1 : props.canvas?.pixelRatio ?? 1;
|
|
232
|
+
if (pixelRatio !== 1) {
|
|
233
|
+
// Resizing the backing buffer resets the 2D context's transform (and
|
|
234
|
+
// everything else about its state), so context.scale below has to
|
|
235
|
+
// come after this, not before.
|
|
236
|
+
canvas.width = logicalWidth * pixelRatio;
|
|
237
|
+
canvas.height = logicalHeight * pixelRatio;
|
|
238
|
+
canvas.style.width = `${logicalWidth}px`;
|
|
239
|
+
canvas.style.height = `${logicalHeight}px`;
|
|
240
|
+
context.scale(pixelRatio, pixelRatio);
|
|
241
|
+
}
|
|
175
242
|
// Make the canvas focusable so keyboard input is scoped to it instead of
|
|
176
243
|
// leaking to the rest of the page (e.g. arrow keys scrolling the window).
|
|
177
244
|
canvas.tabIndex = 0;
|
|
245
|
+
// Stops the browser from treating a drag/pinch on the canvas as page
|
|
246
|
+
// scroll/zoom — backs up the touchstart/touchmove preventDefault calls
|
|
247
|
+
// below for gestures (e.g. a pinch starting on the canvas) preventDefault
|
|
248
|
+
// alone doesn't reliably stop.
|
|
249
|
+
canvas.style.touchAction = "none";
|
|
178
250
|
// Resizes the *display* size only (CSS width/height) — canvas.width/
|
|
179
251
|
// height above stays the fixed logical resolution every renderable's
|
|
180
252
|
// position is already expressed in, so this never needs to touch any
|
|
@@ -210,27 +282,14 @@ const runEngine = async (props) => {
|
|
|
210
282
|
events.push({ tag: "CUSTOM", event });
|
|
211
283
|
};
|
|
212
284
|
const resources = props.resources ?? {};
|
|
213
|
-
const resourceById = await iterateRecordAsync(resources,
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
},
|
|
222
|
-
slices: value.slices,
|
|
223
|
-
animations: value.animations ?? {},
|
|
224
|
-
});
|
|
225
|
-
};
|
|
226
|
-
image.src = value.src;
|
|
227
|
-
image.onload = () => settle(image);
|
|
228
|
-
// Missing/failed-to-load asset (see .gitignore's dist/resources/
|
|
229
|
-
// note) — a placeholder sheet, sized to match what this resource
|
|
230
|
-
// declared, keeps every frame/slice/animation index the example
|
|
231
|
-
// already computes valid instead of drawing nothing or throwing.
|
|
232
|
-
image.onerror = () => settle(createPlaceholderSheet(value.size));
|
|
233
|
-
}));
|
|
285
|
+
const resourceById = await iterateRecordAsync(resources, ({ value }) => loadResource(value));
|
|
286
|
+
// Registers a resource after startup — loads it exactly like the
|
|
287
|
+
// resources above (loadResource is the same function), then adds it to
|
|
288
|
+
// the same resourceById map every SPRITE/ANIMATED_SPRITE lookup already
|
|
289
|
+
// reads from, so it's usable by `resourceId` as soon as this resolves.
|
|
290
|
+
const addResource = async (id, resource) => {
|
|
291
|
+
resourceById[id] = await loadResource(resource);
|
|
292
|
+
};
|
|
234
293
|
const loadAudio = (src) => new Promise((resolve) => {
|
|
235
294
|
const audio = new Audio(src);
|
|
236
295
|
audio.oncanplaythrough = function () {
|
|
@@ -310,7 +369,12 @@ const runEngine = async (props) => {
|
|
|
310
369
|
// fullscreen functions are no-ops here since this run never gets far
|
|
311
370
|
// enough to own the canvas — the newer run's are the ones that matter.
|
|
312
371
|
if (runId !== latestRunId) {
|
|
313
|
-
return {
|
|
372
|
+
return {
|
|
373
|
+
sendEvent,
|
|
374
|
+
requestFullscreen: () => Promise.resolve(),
|
|
375
|
+
exitFullscreen: () => Promise.resolve(),
|
|
376
|
+
addResource,
|
|
377
|
+
};
|
|
314
378
|
}
|
|
315
379
|
context.imageSmoothingEnabled = false;
|
|
316
380
|
// An offscreen 1x1 canvas used only to resolve a CSS color string (a
|
|
@@ -398,7 +462,12 @@ const runEngine = async (props) => {
|
|
|
398
462
|
seenAnimationIds.add(renderable.id);
|
|
399
463
|
const resource = resourceById[renderable.resourceId];
|
|
400
464
|
const animation = resource.animations[renderable.animation];
|
|
401
|
-
|
|
465
|
+
// Composed with RunEngineProps.timeScale (currentTimeScale, set once
|
|
466
|
+
// per tick below) — this renderable's own speed multiplied by
|
|
467
|
+
// whatever the whole game's currently running at, so a global 2x/4x/
|
|
468
|
+
// stop control speeds up (or freezes) every animation right along
|
|
469
|
+
// with the rest of the simulation, not just movement/timers.
|
|
470
|
+
const timeScale = (renderable.timeScale ?? 1) * currentTimeScale;
|
|
402
471
|
const paused = renderable.paused ?? false;
|
|
403
472
|
const now = Date.now();
|
|
404
473
|
const tracked = animationStateById.get(renderable.id);
|
|
@@ -590,8 +659,20 @@ const runEngine = async (props) => {
|
|
|
590
659
|
// the same conversion to line up.
|
|
591
660
|
const getCanvasPosition = (ev) => {
|
|
592
661
|
return {
|
|
593
|
-
x: (ev.offsetX *
|
|
594
|
-
y: (ev.offsetY *
|
|
662
|
+
x: (ev.offsetX * logicalWidth) / canvas.clientWidth,
|
|
663
|
+
y: (ev.offsetY * logicalHeight) / canvas.clientHeight,
|
|
664
|
+
};
|
|
665
|
+
};
|
|
666
|
+
// Touch's equivalent of getCanvasPosition — a Touch has no offsetX/Y
|
|
667
|
+
// (that's a MouseEvent-only convenience), so this gets there manually
|
|
668
|
+
// via getBoundingClientRect instead. clientX/Y and the rect are both
|
|
669
|
+
// viewport-relative, so their difference stays correct regardless of
|
|
670
|
+
// page scroll.
|
|
671
|
+
const getTouchPosition = (touch) => {
|
|
672
|
+
const rect = canvas.getBoundingClientRect();
|
|
673
|
+
return {
|
|
674
|
+
x: ((touch.clientX - rect.left) * logicalWidth) / canvas.clientWidth,
|
|
675
|
+
y: ((touch.clientY - rect.top) * logicalHeight) / canvas.clientHeight,
|
|
595
676
|
};
|
|
596
677
|
};
|
|
597
678
|
const updateState = (updateFn) => {
|
|
@@ -600,8 +681,19 @@ const runEngine = async (props) => {
|
|
|
600
681
|
const nextStateFns = Array.isArray(props.nextState) ? props.nextState : [props.nextState];
|
|
601
682
|
let lastFrame = Date.now();
|
|
602
683
|
let hoveredId = null;
|
|
603
|
-
|
|
604
|
-
|
|
684
|
+
// How much this tick's simulated time is scaled by (see
|
|
685
|
+
// RunEngineProps.timeScale) — set once per tick, right before that
|
|
686
|
+
// tick's own TIME event delta is computed, and read again later the
|
|
687
|
+
// same tick by resolveAnimatedSprite (below) so a single number
|
|
688
|
+
// governs both game logic and animation playback consistently within
|
|
689
|
+
// one tick rather than each recomputing props.timeScale(state)
|
|
690
|
+
// separately (state may itself have just changed this same tick).
|
|
691
|
+
let currentTimeScale = 1;
|
|
692
|
+
// Shared by the mouse "click" listener and the touch handlers below —
|
|
693
|
+
// firing a CLICK is the same "is whatever's currently hovered
|
|
694
|
+
// isClickable" check either way; only how `mouse` was determined
|
|
695
|
+
// differs (a real click event vs. a lifted finger).
|
|
696
|
+
const fireClick = (mouse) => {
|
|
605
697
|
if (hoveredId === null)
|
|
606
698
|
return;
|
|
607
699
|
const { renderables, camera } = renderState(state);
|
|
@@ -610,6 +702,7 @@ const runEngine = async (props) => {
|
|
|
610
702
|
events.push({ tag: "CLICK", id: hovered.id, mouse, worldMouse: toWorldPosition(mouse, camera) });
|
|
611
703
|
}
|
|
612
704
|
};
|
|
705
|
+
const handleClick = (ev) => fireClick(getCanvasPosition(ev));
|
|
613
706
|
canvas.addEventListener("click", handleClick);
|
|
614
707
|
const initialState = {
|
|
615
708
|
keyboardState: createRecord(keyboardKeys, () => false),
|
|
@@ -639,8 +732,34 @@ const runEngine = async (props) => {
|
|
|
639
732
|
}
|
|
640
733
|
};
|
|
641
734
|
canvas.addEventListener("keyup", handleKeyUp);
|
|
642
|
-
|
|
643
|
-
|
|
735
|
+
// mouseButton state (see NextStateProps.mouseButton) — a double-buffer
|
|
736
|
+
// exactly like keyboardState above, just for the primary mouse button.
|
|
737
|
+
let previousMouseButtonState = { isPressed: false };
|
|
738
|
+
const currentMouseButtonState = { isPressed: false };
|
|
739
|
+
const handleMouseDown = (event) => {
|
|
740
|
+
// Left/primary button only — matching CLICK, which already only
|
|
741
|
+
// ever fires for it.
|
|
742
|
+
if (event.button === 0) {
|
|
743
|
+
currentMouseButtonState.isPressed = true;
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
canvas.addEventListener("mousedown", handleMouseDown);
|
|
747
|
+
const handleMouseUp = (event) => {
|
|
748
|
+
if (event.button === 0) {
|
|
749
|
+
currentMouseButtonState.isPressed = false;
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
// On window rather than the canvas — so releasing the button after
|
|
753
|
+
// having dragged off the canvas while still holding it down still
|
|
754
|
+
// clears isPressed, instead of leaving it stuck true forever.
|
|
755
|
+
window.addEventListener("mouseup", handleMouseUp);
|
|
756
|
+
// Shared by mousemove and the touch handlers below — updates hoveredId
|
|
757
|
+
// from a canvas position and fires HOVER_IN/HOVER_OUT as whatever's
|
|
758
|
+
// underneath it changes. Touch has no ambient hover the way a mouse
|
|
759
|
+
// does (nothing is "hovered" until a finger actually touches down), but
|
|
760
|
+
// feeding a touch's position through this the same as the mouse's is
|
|
761
|
+
// what lets an isHoverable renderable react to a tap/drag at all.
|
|
762
|
+
const updateHover = (mouse) => {
|
|
644
763
|
const { renderables, camera } = renderState(state);
|
|
645
764
|
const worldMouse = toWorldPosition(mouse, camera);
|
|
646
765
|
const hovered = [...renderables]
|
|
@@ -657,9 +776,9 @@ const runEngine = async (props) => {
|
|
|
657
776
|
}
|
|
658
777
|
hoveredId = hovered === undefined ? null : hovered.id ?? null;
|
|
659
778
|
};
|
|
779
|
+
const handleMouseMoveHover = (ev) => updateHover(getCanvasPosition(ev));
|
|
660
780
|
canvas.addEventListener("mousemove", handleMouseMoveHover);
|
|
661
|
-
const
|
|
662
|
-
const mouse = getCanvasPosition(ev);
|
|
781
|
+
const updateTracking = (mouse) => {
|
|
663
782
|
if (hoveredId === null) {
|
|
664
783
|
return;
|
|
665
784
|
}
|
|
@@ -669,13 +788,17 @@ const runEngine = async (props) => {
|
|
|
669
788
|
events.push({ tag: "MOUSE_MOVE", mouse, worldMouse: toWorldPosition(mouse, camera), id: hovered.id });
|
|
670
789
|
}
|
|
671
790
|
};
|
|
791
|
+
const handleMouseMoveTracking = (ev) => updateTracking(getCanvasPosition(ev));
|
|
672
792
|
canvas.addEventListener("mousemove", handleMouseMoveTracking);
|
|
673
793
|
// No further mousemove fires once the mouse is off the canvas, so this
|
|
674
794
|
// is also the only chance to report a HOVER_OUT for whatever was
|
|
675
795
|
// hovered when it left — otherwise that hover would just dangle,
|
|
676
796
|
// never explicitly ended.
|
|
677
|
-
|
|
678
|
-
|
|
797
|
+
// Shared by mouseleave and the touch handlers below — clears whatever's
|
|
798
|
+
// hovered (firing HOVER_OUT for it) without a HOVER_IN taking its
|
|
799
|
+
// place. Returns worldMouse so callers that need it (MOUSE_LEAVE below)
|
|
800
|
+
// don't have to call renderState() a second time just to get it.
|
|
801
|
+
const clearHover = (mouse) => {
|
|
679
802
|
const { renderables, camera } = renderState(state);
|
|
680
803
|
const worldMouse = toWorldPosition(mouse, camera);
|
|
681
804
|
if (hoveredId !== null) {
|
|
@@ -685,9 +808,79 @@ const runEngine = async (props) => {
|
|
|
685
808
|
}
|
|
686
809
|
}
|
|
687
810
|
hoveredId = null;
|
|
811
|
+
return worldMouse;
|
|
812
|
+
};
|
|
813
|
+
const handleMouseLeave = (ev) => {
|
|
814
|
+
const mouse = getCanvasPosition(ev);
|
|
815
|
+
const worldMouse = clearHover(mouse);
|
|
688
816
|
events.push({ tag: "MOUSE_LEAVE", mouse, worldMouse });
|
|
689
817
|
};
|
|
690
818
|
canvas.addEventListener("mouseleave", handleMouseLeave);
|
|
819
|
+
// Translates touch into the same HOVER_IN/HOVER_OUT/MOUSE_MOVE/CLICK
|
|
820
|
+
// events mouse input already produces (via updateHover/updateTracking/
|
|
821
|
+
// fireClick/clearHover above), so existing game code written against
|
|
822
|
+
// those events works on a touchscreen with no changes of its own.
|
|
823
|
+
// Single-touch only — touches[0]/changedTouches[0] — the same "one
|
|
824
|
+
// active pointer" model mouse input already assumes; a second finger is
|
|
825
|
+
// ignored rather than tracked as its own pointer.
|
|
826
|
+
//
|
|
827
|
+
// preventDefault on start/move keeps a drag/tap on the canvas from also
|
|
828
|
+
// scrolling, pinch-zooming, or triggering pull-to-refresh — the browser
|
|
829
|
+
// gestures a touchscreen normally reserves that space for; { passive:
|
|
830
|
+
// false } is what makes preventDefault actually take effect here.
|
|
831
|
+
const handleTouchStart = (ev) => {
|
|
832
|
+
ev.preventDefault();
|
|
833
|
+
const touch = ev.touches[0];
|
|
834
|
+
if (touch === undefined)
|
|
835
|
+
return;
|
|
836
|
+
currentMouseButtonState.isPressed = true;
|
|
837
|
+
const mouse = getTouchPosition(touch);
|
|
838
|
+
updateHover(mouse);
|
|
839
|
+
updateTracking(mouse);
|
|
840
|
+
};
|
|
841
|
+
canvas.addEventListener("touchstart", handleTouchStart, { passive: false });
|
|
842
|
+
const handleTouchMove = (ev) => {
|
|
843
|
+
ev.preventDefault();
|
|
844
|
+
const touch = ev.touches[0];
|
|
845
|
+
if (touch === undefined)
|
|
846
|
+
return;
|
|
847
|
+
const mouse = getTouchPosition(touch);
|
|
848
|
+
updateHover(mouse);
|
|
849
|
+
updateTracking(mouse);
|
|
850
|
+
};
|
|
851
|
+
canvas.addEventListener("touchmove", handleTouchMove, { passive: false });
|
|
852
|
+
// A lifted finger both releases (mirroring mouseup) and clicks
|
|
853
|
+
// (mirroring the browser's own click-after-mouseup) — touch has no
|
|
854
|
+
// separate "up" and "click" events of its own the way mouse does, so
|
|
855
|
+
// both happen here together, in that order. hover is updated once more
|
|
856
|
+
// first so a plain tap (touchstart immediately followed by touchend,
|
|
857
|
+
// with no touchmove between them to have already done this) still
|
|
858
|
+
// fires CLICK against whatever's actually under it; hover is then
|
|
859
|
+
// cleared, since nothing's left touching it once the finger lifts.
|
|
860
|
+
const handleTouchEnd = (ev) => {
|
|
861
|
+
ev.preventDefault();
|
|
862
|
+
currentMouseButtonState.isPressed = false;
|
|
863
|
+
const touch = ev.changedTouches[0];
|
|
864
|
+
if (touch === undefined)
|
|
865
|
+
return;
|
|
866
|
+
const mouse = getTouchPosition(touch);
|
|
867
|
+
updateHover(mouse);
|
|
868
|
+
fireClick(mouse);
|
|
869
|
+
clearHover(mouse);
|
|
870
|
+
};
|
|
871
|
+
canvas.addEventListener("touchend", handleTouchEnd, { passive: false });
|
|
872
|
+
// A cancelled touch (e.g. an incoming call interrupting the page, or
|
|
873
|
+
// the OS deciding it's a system gesture instead) never fires touchend —
|
|
874
|
+
// handled the same as lifting the finger, minus the click, since
|
|
875
|
+
// there's no tap to speak of once the touch itself has been cancelled.
|
|
876
|
+
const handleTouchCancel = (ev) => {
|
|
877
|
+
currentMouseButtonState.isPressed = false;
|
|
878
|
+
const touch = ev.changedTouches[0];
|
|
879
|
+
if (touch === undefined)
|
|
880
|
+
return;
|
|
881
|
+
clearHover(getTouchPosition(touch));
|
|
882
|
+
};
|
|
883
|
+
canvas.addEventListener("touchcancel", handleTouchCancel, { passive: false });
|
|
691
884
|
// Tab switches are a document-level concern (visibilitychange), not
|
|
692
885
|
// something that ever reaches the canvas itself the way mouse/keyboard
|
|
693
886
|
// events do.
|
|
@@ -808,7 +1001,14 @@ const runEngine = async (props) => {
|
|
|
808
1001
|
};
|
|
809
1002
|
const intervalId = setInterval(() => {
|
|
810
1003
|
const now = Date.now();
|
|
811
|
-
const
|
|
1004
|
+
const rawDelta = now - lastFrame;
|
|
1005
|
+
// Read from state as this tick starts (i.e. still last tick's own
|
|
1006
|
+
// result) — resolveAnimatedSprite (below, during this same tick's
|
|
1007
|
+
// render pass) reads this same currentTimeScale rather than calling
|
|
1008
|
+
// props.timeScale itself, so a single value governs both the TIME
|
|
1009
|
+
// event about to fire and every animation's playback consistently.
|
|
1010
|
+
currentTimeScale = props.timeScale?.(state) ?? 1;
|
|
1011
|
+
const delta = rawDelta * currentTimeScale;
|
|
812
1012
|
events.push({ tag: "TIME", delta: delta });
|
|
813
1013
|
for (const event of events) {
|
|
814
1014
|
const keyboard = iterateRecord(previousState.keyboardState, ({ key, value: previouslyPressed }) => {
|
|
@@ -819,11 +1019,17 @@ const runEngine = async (props) => {
|
|
|
819
1019
|
isJustReleased: !isPressed && previouslyPressed,
|
|
820
1020
|
};
|
|
821
1021
|
});
|
|
1022
|
+
const mouseButton = {
|
|
1023
|
+
isPressed: currentMouseButtonState.isPressed,
|
|
1024
|
+
isJustPressed: currentMouseButtonState.isPressed && !previousMouseButtonState.isPressed,
|
|
1025
|
+
isJustReleased: !currentMouseButtonState.isPressed && previousMouseButtonState.isPressed,
|
|
1026
|
+
};
|
|
822
1027
|
for (const nextState of nextStateFns) {
|
|
823
1028
|
const result = nextState({
|
|
824
1029
|
state,
|
|
825
1030
|
event,
|
|
826
1031
|
keyboard,
|
|
1032
|
+
mouseButton,
|
|
827
1033
|
playSound,
|
|
828
1034
|
playMusic,
|
|
829
1035
|
pauseMusic,
|
|
@@ -843,7 +1049,7 @@ const runEngine = async (props) => {
|
|
|
843
1049
|
}
|
|
844
1050
|
}
|
|
845
1051
|
events.splice(0, events.length);
|
|
846
|
-
context.clearRect(0, 0,
|
|
1052
|
+
context.clearRect(0, 0, logicalWidth, logicalHeight);
|
|
847
1053
|
const { cursor, renderables } = renderState(state);
|
|
848
1054
|
canvas.style.cursor = cursor ?? "default";
|
|
849
1055
|
for (const renderable of renderables) {
|
|
@@ -876,12 +1082,11 @@ const runEngine = async (props) => {
|
|
|
876
1082
|
continue;
|
|
877
1083
|
}
|
|
878
1084
|
if (renderable.type === "SPRITE") {
|
|
879
|
-
const { opacity = 1, flipX = false, modulate, swapColors } = renderable;
|
|
1085
|
+
const { opacity = 1, flipX = false, frame: frameIndex = 0, modulate, swapColors } = renderable;
|
|
880
1086
|
const resource = resourceById[renderable.resourceId];
|
|
881
1087
|
const frame = {
|
|
882
|
-
x:
|
|
883
|
-
y: Math.floor(
|
|
884
|
-
resource.slices.vertical,
|
|
1088
|
+
x: frameIndex % resource.slices.horizontal,
|
|
1089
|
+
y: Math.floor(frameIndex / resource.slices.horizontal) % resource.slices.vertical,
|
|
885
1090
|
};
|
|
886
1091
|
const source = {
|
|
887
1092
|
x: frame.x * resource.size.width,
|
|
@@ -901,7 +1106,7 @@ const runEngine = async (props) => {
|
|
|
901
1106
|
let image = resource.image;
|
|
902
1107
|
let imageSource = source;
|
|
903
1108
|
if (swapColors !== undefined && swapColors.length > 0) {
|
|
904
|
-
image = swappedSpriteFrame(renderable.resourceId,
|
|
1109
|
+
image = swappedSpriteFrame(renderable.resourceId, frameIndex, image, imageSource, swapColors);
|
|
905
1110
|
imageSource = { x: 0, y: 0, width: source.width, height: source.height };
|
|
906
1111
|
}
|
|
907
1112
|
if (modulate !== undefined) {
|
|
@@ -955,6 +1160,7 @@ const runEngine = async (props) => {
|
|
|
955
1160
|
}
|
|
956
1161
|
lastFrame = now;
|
|
957
1162
|
previousState.keyboardState = { ...currentState.keyboardState };
|
|
1163
|
+
previousMouseButtonState = { ...currentMouseButtonState };
|
|
958
1164
|
}, 0);
|
|
959
1165
|
resetCanvas = () => {
|
|
960
1166
|
clearInterval(intervalId);
|
|
@@ -978,9 +1184,16 @@ const runEngine = async (props) => {
|
|
|
978
1184
|
canvas.removeEventListener("click", handleClick);
|
|
979
1185
|
canvas.removeEventListener("keydown", handleKeyDown);
|
|
980
1186
|
canvas.removeEventListener("keyup", handleKeyUp);
|
|
1187
|
+
canvas.removeEventListener("mousedown", handleMouseDown);
|
|
981
1188
|
canvas.removeEventListener("mousemove", handleMouseMoveHover);
|
|
982
1189
|
canvas.removeEventListener("mousemove", handleMouseMoveTracking);
|
|
983
1190
|
canvas.removeEventListener("mouseleave", handleMouseLeave);
|
|
1191
|
+
canvas.removeEventListener("touchstart", handleTouchStart);
|
|
1192
|
+
canvas.removeEventListener("touchmove", handleTouchMove);
|
|
1193
|
+
canvas.removeEventListener("touchend", handleTouchEnd);
|
|
1194
|
+
canvas.removeEventListener("touchcancel", handleTouchCancel);
|
|
1195
|
+
// On window, not the canvas — see where it's added above for why.
|
|
1196
|
+
window.removeEventListener("mouseup", handleMouseUp);
|
|
984
1197
|
// This one's on `document`, not the canvas — same reasoning as above,
|
|
985
1198
|
// just doubly true since `document` isn't even scoped to this canvas.
|
|
986
1199
|
window.document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
@@ -988,9 +1201,53 @@ const runEngine = async (props) => {
|
|
|
988
1201
|
// Same pile-up risk as the canvas listeners above, but on `window`.
|
|
989
1202
|
window.removeEventListener("resize", applyResize);
|
|
990
1203
|
};
|
|
991
|
-
return { sendEvent, requestFullscreen, exitFullscreen };
|
|
1204
|
+
return { sendEvent, requestFullscreen, exitFullscreen, addResource };
|
|
992
1205
|
};
|
|
993
1206
|
|
|
1207
|
+
// One factory per Renderable variant — literally just `{ type: "X",
|
|
1208
|
+
// ...props }`, so `sprite({...})` (or `Yuuna.sprite({...})` in the
|
|
1209
|
+
// browser bundle/playground) reads the same as writing the object
|
|
1210
|
+
// literal by hand, minus needing to get `type` right yourself.
|
|
1211
|
+
// Deliberately not a place defaults live: besides SpriteRenderable.frame
|
|
1212
|
+
// (optional at the type level, see types.ts — the engine itself defaults
|
|
1213
|
+
// it to 0), each factory still requires whatever its Renderable type
|
|
1214
|
+
// still requires.
|
|
1215
|
+
const rectangle = (props) => ({
|
|
1216
|
+
type: "RECTANGLE",
|
|
1217
|
+
...props,
|
|
1218
|
+
});
|
|
1219
|
+
const circle = (props) => ({
|
|
1220
|
+
type: "CIRCLE",
|
|
1221
|
+
...props,
|
|
1222
|
+
});
|
|
1223
|
+
const text = (props) => ({
|
|
1224
|
+
type: "TEXT",
|
|
1225
|
+
...props,
|
|
1226
|
+
});
|
|
1227
|
+
const sprite = (props) => ({
|
|
1228
|
+
type: "SPRITE",
|
|
1229
|
+
...props,
|
|
1230
|
+
});
|
|
1231
|
+
const animatedSprite = (props) => ({
|
|
1232
|
+
type: "ANIMATED_SPRITE",
|
|
1233
|
+
...props,
|
|
1234
|
+
});
|
|
1235
|
+
const line = (props) => ({
|
|
1236
|
+
type: "LINE",
|
|
1237
|
+
...props,
|
|
1238
|
+
});
|
|
1239
|
+
const group = (props) => ({
|
|
1240
|
+
type: "GROUP",
|
|
1241
|
+
...props,
|
|
1242
|
+
});
|
|
1243
|
+
|
|
994
1244
|
exports.STOP = STOP;
|
|
1245
|
+
exports.animatedSprite = animatedSprite;
|
|
1246
|
+
exports.circle = circle;
|
|
1247
|
+
exports.group = group;
|
|
1248
|
+
exports.line = line;
|
|
1249
|
+
exports.rectangle = rectangle;
|
|
995
1250
|
exports.runEngine = runEngine;
|
|
1251
|
+
exports.sprite = sprite;
|
|
1252
|
+
exports.text = text;
|
|
996
1253
|
//# 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
|
-
|
|
4
|
-
export
|
|
3
|
+
import { animatedSprite, circle, group, line, rectangle, sprite, text } from "./engine/renderables";
|
|
4
|
+
export { runEngine, STOP, rectangle, circle, text, sprite, animatedSprite, line, group };
|
|
5
|
+
export type { NextStateFunction, NextStateProps, Renderable, ResourceConfig, GameEvent, CustomGameEvent, } from "./engine/types";
|