plushies 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +195 -0
- package/dist/bundled.d.ts +22 -0
- package/dist/index.d.ts +224 -0
- package/dist/index.js +2259 -0
- package/dist/plushies.bundled.js +4354 -0
- package/dist/plushies.bundled.js.map +14 -0
- package/dist/plushies.cjs +2289 -0
- package/dist/plushies.global.js +4354 -0
- package/dist/plushies.global.js.map +15 -0
- package/dist/viewer.d.ts +70 -0
- package/dist/viewer.js +393 -0
- package/package.json +58 -0
package/dist/viewer.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A drop-in viewer: one plushie on its own transparent canvas, with camera,
|
|
3
|
+
* resize handling, a render loop that only runs while something changes, a
|
|
4
|
+
* few tweens and an optional idle loop.
|
|
5
|
+
*
|
|
6
|
+
* All viewers on a page draw through one hidden WebGL context (per three
|
|
7
|
+
* module) and copy their pixels onto their own 2D canvas. Browsers cap live
|
|
8
|
+
* WebGL contexts at around 16, so this keeps a page of plushies working, and
|
|
9
|
+
* shaders and the environment map are built only once.
|
|
10
|
+
*
|
|
11
|
+
* import * as THREE from 'three';
|
|
12
|
+
* import {mountPlushie} from 'plushies/viewer';
|
|
13
|
+
* const view = mountPlushie(document.querySelector('#hero'), THREE, {kind: 'heart', color: '#f47c9a', idle: true});
|
|
14
|
+
* await view.hop();
|
|
15
|
+
*/
|
|
16
|
+
import { type Plushie, type PlushieOptions, type PlushiePose, type ThreeModule } from './index.js';
|
|
17
|
+
/** The look a viewer builds its plushie from (the viewer brings its own renderer). */
|
|
18
|
+
export type ViewerLook = Omit<PlushieOptions, 'renderer'>;
|
|
19
|
+
export interface ViewerOptions extends ViewerLook, Partial<Omit<PlushiePose, 'width' | 'height' | 'pixelRatio'>> {
|
|
20
|
+
/** Blink, breathe and glance around on its own. (default: false) */
|
|
21
|
+
idle?: boolean;
|
|
22
|
+
/** Eyes follow the mouse pointer anywhere on the page. (default: false) */
|
|
23
|
+
followPointer?: boolean;
|
|
24
|
+
/** Cap for the device pixel ratio. (default: 2) */
|
|
25
|
+
maxPixelRatio?: number;
|
|
26
|
+
}
|
|
27
|
+
export type Easing = (t: number) => number;
|
|
28
|
+
export declare const easeInOut: Easing;
|
|
29
|
+
export declare const easeOut: Easing;
|
|
30
|
+
export declare const easeIn: Easing;
|
|
31
|
+
type Numeric = Exclude<keyof PlushiePose, 'color' | 'width' | 'height' | 'pixelRatio'>;
|
|
32
|
+
export interface PlushieViewer {
|
|
33
|
+
/** The current plushie (replaced by `restyle`). */
|
|
34
|
+
readonly plushie: Plushie;
|
|
35
|
+
readonly canvas: HTMLCanvasElement;
|
|
36
|
+
/** The current look: the options given at mount, merged with every `restyle`. */
|
|
37
|
+
readonly options: Readonly<ViewerLook>;
|
|
38
|
+
/** Set pose values immediately. */
|
|
39
|
+
set(pose: Partial<PlushiePose>): void;
|
|
40
|
+
/** Tween numeric pose values. Resolves when done; a newer tween of the same key takes over. */
|
|
41
|
+
to(pose: Partial<Pick<PlushiePose, Numeric>>, seconds?: number, ease?: Easing): Promise<void>;
|
|
42
|
+
/** Point the eyes at x, y in −1..1 (y down). */
|
|
43
|
+
look(x: number, y?: number, seconds?: number): Promise<void>;
|
|
44
|
+
blink(seconds?: number): Promise<void>;
|
|
45
|
+
/** Anticipation squash, jump, landing squash, settle. `height` in pixels defaults to a fifth of the canvas height, at most 90. */
|
|
46
|
+
hop(height?: number, seconds?: number): Promise<void>;
|
|
47
|
+
/** Squeeze and spring back. */
|
|
48
|
+
squish(amount?: number, seconds?: number): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Change the look (build-time options) in place, keeping the pose. `options`
|
|
51
|
+
* is merged into the current look; pass `undefined` for a key to reset it to
|
|
52
|
+
* its default. Fur length resets to the new fabric's unless given.
|
|
53
|
+
*/
|
|
54
|
+
restyle(options: ViewerLook & Partial<Pick<PlushiePose, 'color' | 'fur'>>): void;
|
|
55
|
+
/**
|
|
56
|
+
* Halt everything where it is: running tweens, `hop`/`squish`/`blink`
|
|
57
|
+
* sequences and the current idle gesture. Their promises resolve; the idle
|
|
58
|
+
* loop (if on) carries on after its pause.
|
|
59
|
+
*/
|
|
60
|
+
stop(): void;
|
|
61
|
+
/** Start or stop the idle loop (blink, breathe, glance). */
|
|
62
|
+
setIdle(on: boolean): void;
|
|
63
|
+
/** Start or stop following the pointer with the eyes. */
|
|
64
|
+
setFollowPointer(on: boolean): void;
|
|
65
|
+
/** Remove the canvas and free GPU resources. Pending promises resolve. Safe to call twice. */
|
|
66
|
+
dispose(): void;
|
|
67
|
+
}
|
|
68
|
+
/** Mount a plushie filling `container` (give the container a size). */
|
|
69
|
+
export declare function mountPlushie(container: HTMLElement, three: ThreeModule, options?: ViewerOptions): PlushieViewer;
|
|
70
|
+
export {};
|
package/dist/viewer.js
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
// src/viewer.ts
|
|
2
|
+
import {
|
|
3
|
+
createPlushie,
|
|
4
|
+
resetEnvironment
|
|
5
|
+
} from "./index.js";
|
|
6
|
+
var easeInOut = (t) => t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
|
|
7
|
+
var easeOut = (t) => 1 - (1 - t) ** 3;
|
|
8
|
+
var easeIn = (t) => t * t;
|
|
9
|
+
var SHARED = new WeakMap;
|
|
10
|
+
function acquireRenderer(three) {
|
|
11
|
+
let shared = SHARED.get(three);
|
|
12
|
+
if (!shared) {
|
|
13
|
+
const renderer = new three.WebGLRenderer({ antialias: true, alpha: true, premultipliedAlpha: true });
|
|
14
|
+
renderer.setClearColor(0, 0);
|
|
15
|
+
renderer.setPixelRatio(1);
|
|
16
|
+
renderer.setSize(1, 1, false);
|
|
17
|
+
renderer.setScissorTest(true);
|
|
18
|
+
const created = { renderer, users: new Map, width: 1, height: 1 };
|
|
19
|
+
renderer.domElement.addEventListener("webglcontextlost", (event) => event.preventDefault());
|
|
20
|
+
renderer.domElement.addEventListener("webglcontextrestored", () => {
|
|
21
|
+
for (const user of created.users.values())
|
|
22
|
+
user.restore();
|
|
23
|
+
});
|
|
24
|
+
shared = created;
|
|
25
|
+
SHARED.set(three, shared);
|
|
26
|
+
}
|
|
27
|
+
return shared;
|
|
28
|
+
}
|
|
29
|
+
function releaseRenderer(three, shared, user) {
|
|
30
|
+
if (!shared.users.delete(user))
|
|
31
|
+
return;
|
|
32
|
+
if (shared.users.size > 0) {
|
|
33
|
+
fitBuffer(shared);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
SHARED.delete(three);
|
|
37
|
+
shared.renderer.dispose();
|
|
38
|
+
shared.renderer.forceContextLoss();
|
|
39
|
+
}
|
|
40
|
+
function fitBuffer(shared) {
|
|
41
|
+
let width = 1;
|
|
42
|
+
let height = 1;
|
|
43
|
+
for (const user of shared.users.values()) {
|
|
44
|
+
width = Math.max(width, user.width);
|
|
45
|
+
height = Math.max(height, user.height);
|
|
46
|
+
}
|
|
47
|
+
if (width * height < shared.width * shared.height * 0.5) {
|
|
48
|
+
shared.width = width;
|
|
49
|
+
shared.height = height;
|
|
50
|
+
shared.renderer.setSize(width, height, false);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function paint(shared, scene, camera, target, w, h) {
|
|
54
|
+
const { renderer } = shared;
|
|
55
|
+
if (w > shared.width || h > shared.height) {
|
|
56
|
+
shared.width = Math.max(w, shared.width);
|
|
57
|
+
shared.height = Math.max(h, shared.height);
|
|
58
|
+
renderer.setSize(shared.width, shared.height, false);
|
|
59
|
+
}
|
|
60
|
+
renderer.setViewport(0, 0, w, h);
|
|
61
|
+
renderer.setScissor(0, 0, w, h);
|
|
62
|
+
renderer.render(scene, camera);
|
|
63
|
+
target.clearRect(0, 0, w, h);
|
|
64
|
+
target.drawImage(renderer.domElement, 0, shared.height - h, w, h, 0, 0, w, h);
|
|
65
|
+
}
|
|
66
|
+
var NUMERIC = ["lookX", "lookY", "blink", "squash", "hop", "lean", "turn", "float", "fur"];
|
|
67
|
+
var POSE_KEYS = [...NUMERIC, "color", "width", "height", "pixelRatio"];
|
|
68
|
+
function mountPlushie(container, three, options = {}) {
|
|
69
|
+
const { idle = false, followPointer = false, maxPixelRatio = 2, ...rest } = options;
|
|
70
|
+
const look = { ...rest };
|
|
71
|
+
for (const key of POSE_KEYS)
|
|
72
|
+
delete look[key];
|
|
73
|
+
delete look.renderer;
|
|
74
|
+
const shared = acquireRenderer(three);
|
|
75
|
+
const { renderer } = shared;
|
|
76
|
+
const canvas = document.createElement("canvas");
|
|
77
|
+
const context = canvas.getContext("2d");
|
|
78
|
+
canvas.style.display = "block";
|
|
79
|
+
canvas.style.width = "100%";
|
|
80
|
+
canvas.style.height = "100%";
|
|
81
|
+
container.appendChild(canvas);
|
|
82
|
+
const user = { width: 1, height: 1, restore: () => restore() };
|
|
83
|
+
shared.users.set(user, user);
|
|
84
|
+
const scene = new three.Scene;
|
|
85
|
+
const world = new three.Group;
|
|
86
|
+
world.scale.setScalar(1 / 100);
|
|
87
|
+
scene.add(world);
|
|
88
|
+
const camera = new three.PerspectiveCamera;
|
|
89
|
+
const DISTANCE = 1600;
|
|
90
|
+
let plushie;
|
|
91
|
+
try {
|
|
92
|
+
plushie = createPlushie(three, { ...rest, renderer });
|
|
93
|
+
} catch (error) {
|
|
94
|
+
releaseRenderer(three, shared, user);
|
|
95
|
+
canvas.remove();
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
world.add(plushie.object);
|
|
99
|
+
let width = 1;
|
|
100
|
+
let height = 1;
|
|
101
|
+
const resize = () => {
|
|
102
|
+
if (disposed)
|
|
103
|
+
return;
|
|
104
|
+
width = Math.max(1, container.clientWidth);
|
|
105
|
+
height = Math.max(1, container.clientHeight);
|
|
106
|
+
const ratio = Math.min(maxPixelRatio, window.devicePixelRatio || 1);
|
|
107
|
+
const w = Math.round(width * ratio);
|
|
108
|
+
const h = Math.round(height * ratio);
|
|
109
|
+
const pose = plushie.pose;
|
|
110
|
+
if (w === canvas.width && h === canvas.height && pose.pixelRatio === ratio && pose.width === width && pose.height === height)
|
|
111
|
+
return;
|
|
112
|
+
canvas.width = w;
|
|
113
|
+
canvas.height = h;
|
|
114
|
+
user.width = canvas.width;
|
|
115
|
+
user.height = canvas.height;
|
|
116
|
+
fitBuffer(shared);
|
|
117
|
+
camera.fov = 2 * Math.atan(height / 2 / DISTANCE) * 180 / Math.PI;
|
|
118
|
+
camera.aspect = width / height;
|
|
119
|
+
camera.near = 0.08;
|
|
120
|
+
camera.far = 1700;
|
|
121
|
+
camera.position.set(0, 0, DISTANCE / 100);
|
|
122
|
+
camera.lookAt(0, 0, 0);
|
|
123
|
+
camera.updateProjectionMatrix();
|
|
124
|
+
plushie.set({ width, height, pixelRatio: ratio });
|
|
125
|
+
invalidate();
|
|
126
|
+
};
|
|
127
|
+
const tweens = new Map;
|
|
128
|
+
let frame = 0;
|
|
129
|
+
let disposed = false;
|
|
130
|
+
let visible = true;
|
|
131
|
+
let stale = false;
|
|
132
|
+
let chainFrom = 0;
|
|
133
|
+
let chainLimit = 50;
|
|
134
|
+
let chaining = false;
|
|
135
|
+
let lastTick = 0;
|
|
136
|
+
let frameGap = 0;
|
|
137
|
+
let generation = 0;
|
|
138
|
+
const invalidate = () => {
|
|
139
|
+
if (!frame && !disposed)
|
|
140
|
+
frame = requestAnimationFrame(tick);
|
|
141
|
+
};
|
|
142
|
+
const tick = (now) => {
|
|
143
|
+
frame = 0;
|
|
144
|
+
const gap = lastTick ? now - lastTick : 0;
|
|
145
|
+
const next = {};
|
|
146
|
+
const finished = [];
|
|
147
|
+
for (const [key, t] of tweens) {
|
|
148
|
+
const u = t.seconds <= 0 ? 1 : Math.min(1, Math.max(0, (now - t.start) / 1000 / t.seconds));
|
|
149
|
+
next[key] = t.from + (t.to - t.from) * t.ease(u);
|
|
150
|
+
if (u >= 1) {
|
|
151
|
+
tweens.delete(key);
|
|
152
|
+
finished.push(t);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
plushie.set(next);
|
|
156
|
+
if (visible)
|
|
157
|
+
paint(shared, scene, camera, context, canvas.width, canvas.height);
|
|
158
|
+
else
|
|
159
|
+
stale = true;
|
|
160
|
+
if (finished.length) {
|
|
161
|
+
chainFrom = Math.max(...finished.map((t) => t.start + t.seconds * 1000));
|
|
162
|
+
chainLimit = Math.min(500, Math.max(50, 2 * frameGap));
|
|
163
|
+
chaining = true;
|
|
164
|
+
setTimeout(() => chaining = false);
|
|
165
|
+
for (const t of finished)
|
|
166
|
+
t.done();
|
|
167
|
+
}
|
|
168
|
+
frameGap = tweens.size ? gap : 0;
|
|
169
|
+
lastTick = tweens.size ? now : 0;
|
|
170
|
+
if (tweens.size)
|
|
171
|
+
invalidate();
|
|
172
|
+
};
|
|
173
|
+
const settle = (key) => {
|
|
174
|
+
for (const [k, t] of tweens) {
|
|
175
|
+
if (key !== undefined && k !== key)
|
|
176
|
+
continue;
|
|
177
|
+
tweens.delete(k);
|
|
178
|
+
t.done();
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
const to = (pose, seconds = 0.4, ease = easeInOut) => {
|
|
182
|
+
if (disposed)
|
|
183
|
+
return Promise.resolve();
|
|
184
|
+
const now = performance.now();
|
|
185
|
+
const start = chaining && now - chainFrom <= chainLimit ? chainFrom : now;
|
|
186
|
+
const keys = Object.keys(pose).filter((k) => NUMERIC.includes(k) && Number.isFinite(pose[k]));
|
|
187
|
+
const done = Promise.all(keys.map((key) => new Promise((resolve) => {
|
|
188
|
+
settle(key);
|
|
189
|
+
tweens.set(key, {
|
|
190
|
+
from: plushie.pose[key],
|
|
191
|
+
to: pose[key],
|
|
192
|
+
start,
|
|
193
|
+
seconds: Number.isFinite(seconds) ? seconds : 0,
|
|
194
|
+
ease,
|
|
195
|
+
done: resolve
|
|
196
|
+
});
|
|
197
|
+
}))).then(() => {
|
|
198
|
+
return;
|
|
199
|
+
});
|
|
200
|
+
invalidate();
|
|
201
|
+
return done;
|
|
202
|
+
};
|
|
203
|
+
const wait = (seconds) => new Promise((r) => setTimeout(r, seconds * 1000));
|
|
204
|
+
const perform = async (steps) => {
|
|
205
|
+
const g = generation;
|
|
206
|
+
for (const step of steps) {
|
|
207
|
+
if (disposed || generation !== g)
|
|
208
|
+
return;
|
|
209
|
+
await step();
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
let idling = false;
|
|
213
|
+
let idleRun = 0;
|
|
214
|
+
const idleLoop = async (run) => {
|
|
215
|
+
const glances = [[-0.8, 0], [0.7, -0.3], [0, 0.2], [0.4, 0.5], [0, 0]];
|
|
216
|
+
const alive = () => !disposed && idleRun === run;
|
|
217
|
+
for (let i = 0;alive(); i++) {
|
|
218
|
+
const g = generation;
|
|
219
|
+
const going = () => alive() && generation === g;
|
|
220
|
+
await viewer.blink();
|
|
221
|
+
if (going())
|
|
222
|
+
await to({ squash: 0.08, float: 1.12 }, 1.1);
|
|
223
|
+
if (going())
|
|
224
|
+
await to({ squash: 0, float: 1 }, 1.1);
|
|
225
|
+
if (going() && performance.now() - pointerAt > 2000) {
|
|
226
|
+
const [x, y] = glances[i % glances.length];
|
|
227
|
+
await viewer.look(x, y, 0.4);
|
|
228
|
+
}
|
|
229
|
+
if (!alive())
|
|
230
|
+
break;
|
|
231
|
+
await wait(0.6 + i * 7 % 5 * 0.25);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
const rebuild = () => {
|
|
235
|
+
const { fur, ...pose } = plushie.pose;
|
|
236
|
+
plushie.dispose();
|
|
237
|
+
plushie = createPlushie(three, { ...look, ...pose, fur, renderer });
|
|
238
|
+
world.add(plushie.object);
|
|
239
|
+
invalidate();
|
|
240
|
+
};
|
|
241
|
+
const restore = () => {
|
|
242
|
+
resetEnvironment(renderer);
|
|
243
|
+
rebuild();
|
|
244
|
+
};
|
|
245
|
+
const viewer = {
|
|
246
|
+
get plushie() {
|
|
247
|
+
return plushie;
|
|
248
|
+
},
|
|
249
|
+
canvas,
|
|
250
|
+
get options() {
|
|
251
|
+
return { ...look };
|
|
252
|
+
},
|
|
253
|
+
set(pose) {
|
|
254
|
+
if (disposed)
|
|
255
|
+
return;
|
|
256
|
+
for (const key of Object.keys(pose))
|
|
257
|
+
settle(key);
|
|
258
|
+
plushie.set(pose);
|
|
259
|
+
invalidate();
|
|
260
|
+
},
|
|
261
|
+
to,
|
|
262
|
+
look: (x, y = 0, seconds = 0.35) => to({ lookX: x, lookY: y }, seconds),
|
|
263
|
+
blink: (seconds = 0.22) => perform([() => to({ blink: 1 }, seconds * 0.45, easeIn), () => to({ blink: 0 }, seconds * 0.55, easeOut)]),
|
|
264
|
+
hop(jump, seconds = 1.1) {
|
|
265
|
+
const height2 = jump ?? Math.min(90, plushie.pose.height * 0.2);
|
|
266
|
+
return perform([
|
|
267
|
+
() => to({ squash: 0.45 }, seconds * 0.2, easeOut),
|
|
268
|
+
() => {
|
|
269
|
+
to({ squash: -0.35 }, seconds * 0.15, easeOut);
|
|
270
|
+
return to({ hop: height2 }, seconds * 0.3, easeOut);
|
|
271
|
+
},
|
|
272
|
+
() => {
|
|
273
|
+
to({ squash: 0 }, seconds * 0.2);
|
|
274
|
+
return to({ hop: 0 }, seconds * 0.25, easeIn);
|
|
275
|
+
},
|
|
276
|
+
() => to({ squash: 0.4 }, seconds * 0.08, easeOut),
|
|
277
|
+
() => to({ squash: 0 }, seconds * 0.22, easeOut)
|
|
278
|
+
]);
|
|
279
|
+
},
|
|
280
|
+
squish: (amount = 0.6, seconds = 1) => perform([
|
|
281
|
+
() => to({ squash: amount, float: 1.25 }, seconds * 0.35, easeOut),
|
|
282
|
+
() => to({ squash: -amount * 0.3, float: 0.9 }, seconds * 0.25, easeInOut),
|
|
283
|
+
() => to({ squash: 0, float: 1 }, seconds * 0.4, easeOut)
|
|
284
|
+
]),
|
|
285
|
+
restyle(change) {
|
|
286
|
+
if (disposed)
|
|
287
|
+
return;
|
|
288
|
+
const { fur, ...rest2 } = change;
|
|
289
|
+
delete rest2.renderer;
|
|
290
|
+
const posed = {};
|
|
291
|
+
for (const [key, value] of Object.entries(rest2)) {
|
|
292
|
+
if (POSE_KEYS.includes(key)) {
|
|
293
|
+
if (value !== undefined)
|
|
294
|
+
posed[key] = value;
|
|
295
|
+
} else if (value === undefined)
|
|
296
|
+
delete look[key];
|
|
297
|
+
else
|
|
298
|
+
look[key] = value;
|
|
299
|
+
}
|
|
300
|
+
for (const key of Object.keys(posed))
|
|
301
|
+
settle(key);
|
|
302
|
+
if (fur !== undefined || "fabric" in rest2)
|
|
303
|
+
settle("fur");
|
|
304
|
+
const { fur: _fur, ...pose } = plushie.pose;
|
|
305
|
+
plushie.dispose();
|
|
306
|
+
plushie = createPlushie(three, { ...look, ...pose, ...posed, ...fur !== undefined && { fur }, renderer });
|
|
307
|
+
world.add(plushie.object);
|
|
308
|
+
invalidate();
|
|
309
|
+
},
|
|
310
|
+
stop() {
|
|
311
|
+
generation++;
|
|
312
|
+
settle();
|
|
313
|
+
},
|
|
314
|
+
setIdle(on) {
|
|
315
|
+
if (disposed || on === idling)
|
|
316
|
+
return;
|
|
317
|
+
idling = on;
|
|
318
|
+
idleRun++;
|
|
319
|
+
if (on)
|
|
320
|
+
idleLoop(idleRun);
|
|
321
|
+
},
|
|
322
|
+
setFollowPointer(on) {
|
|
323
|
+
window.removeEventListener("pointermove", onPointer);
|
|
324
|
+
if (on && !disposed)
|
|
325
|
+
window.addEventListener("pointermove", onPointer);
|
|
326
|
+
},
|
|
327
|
+
dispose() {
|
|
328
|
+
if (disposed)
|
|
329
|
+
return;
|
|
330
|
+
disposed = true;
|
|
331
|
+
idling = false;
|
|
332
|
+
idleRun++;
|
|
333
|
+
if (frame)
|
|
334
|
+
cancelAnimationFrame(frame);
|
|
335
|
+
frame = 0;
|
|
336
|
+
observer.disconnect();
|
|
337
|
+
visibility?.disconnect();
|
|
338
|
+
unwatchRatio();
|
|
339
|
+
window.removeEventListener("pointermove", onPointer);
|
|
340
|
+
settle();
|
|
341
|
+
plushie.dispose();
|
|
342
|
+
releaseRenderer(three, shared, user);
|
|
343
|
+
canvas.remove();
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
const observer = new ResizeObserver(resize);
|
|
347
|
+
observer.observe(container);
|
|
348
|
+
resize();
|
|
349
|
+
if (container.clientWidth > 0 && container.clientHeight === 0 && getComputedStyle(container).display !== "none") {
|
|
350
|
+
console.warn("plushies: mountPlushie container has no height; give it one (the canvas fills it)");
|
|
351
|
+
}
|
|
352
|
+
const visibility = typeof IntersectionObserver === "undefined" ? null : new IntersectionObserver((entries) => {
|
|
353
|
+
visible = entries[entries.length - 1].isIntersecting;
|
|
354
|
+
if (visible && stale) {
|
|
355
|
+
stale = false;
|
|
356
|
+
invalidate();
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
visibility?.observe(canvas);
|
|
360
|
+
let ratioQuery = null;
|
|
361
|
+
const watchRatio = () => {
|
|
362
|
+
ratioQuery?.removeEventListener("change", onRatio);
|
|
363
|
+
ratioQuery = typeof matchMedia === "undefined" ? null : matchMedia(`(resolution: ${window.devicePixelRatio || 1}dppx)`);
|
|
364
|
+
ratioQuery?.addEventListener("change", onRatio);
|
|
365
|
+
};
|
|
366
|
+
const onRatio = () => {
|
|
367
|
+
resize();
|
|
368
|
+
watchRatio();
|
|
369
|
+
};
|
|
370
|
+
const unwatchRatio = () => ratioQuery?.removeEventListener("change", onRatio);
|
|
371
|
+
watchRatio();
|
|
372
|
+
let pointerAt = 0;
|
|
373
|
+
const onPointer = (event) => {
|
|
374
|
+
if (!visible)
|
|
375
|
+
return;
|
|
376
|
+
const box = canvas.getBoundingClientRect();
|
|
377
|
+
const x = (event.clientX - (box.left + box.width / 2)) / (window.innerWidth / 2);
|
|
378
|
+
const y = (event.clientY - (box.top + box.height / 2)) / (window.innerHeight / 2);
|
|
379
|
+
pointerAt = performance.now();
|
|
380
|
+
to({ lookX: Math.max(-1, Math.min(1, x)), lookY: Math.max(-1, Math.min(1, y)) }, 0.25, easeOut);
|
|
381
|
+
};
|
|
382
|
+
if (followPointer)
|
|
383
|
+
viewer.setFollowPointer(true);
|
|
384
|
+
if (idle)
|
|
385
|
+
viewer.setIdle(true);
|
|
386
|
+
return viewer;
|
|
387
|
+
}
|
|
388
|
+
export {
|
|
389
|
+
mountPlushie,
|
|
390
|
+
easeOut,
|
|
391
|
+
easeInOut,
|
|
392
|
+
easeIn
|
|
393
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "plushies",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "Soft 3D plush characters for three.js — 17 silhouettes, shell-textured fur, floating eyes, glasses and hats, squash & stretch.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Patrick Rathje",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/prathje/plushies.git",
|
|
10
|
+
"directory": "packages/plushies"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/prathje/plushies/tree/main/packages/plushies#readme",
|
|
13
|
+
"bugs": "https://github.com/prathje/plushies/issues",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "./dist/plushies.cjs",
|
|
16
|
+
"module": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"unpkg": "./dist/plushies.global.js",
|
|
19
|
+
"jsdelivr": "./dist/plushies.global.js",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js",
|
|
24
|
+
"require": "./dist/plushies.cjs"
|
|
25
|
+
},
|
|
26
|
+
"./viewer": {
|
|
27
|
+
"types": "./dist/viewer.d.ts",
|
|
28
|
+
"import": "./dist/viewer.js"
|
|
29
|
+
},
|
|
30
|
+
"./bundled": {
|
|
31
|
+
"types": "./dist/bundled.d.ts",
|
|
32
|
+
"import": "./dist/plushies.bundled.js"
|
|
33
|
+
},
|
|
34
|
+
"./package.json": "./package.json"
|
|
35
|
+
},
|
|
36
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
37
|
+
"sideEffects": ["./site/*.ts", "./src/global.ts"],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "bun run build.ts",
|
|
40
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
41
|
+
"test": "bun test test/unit",
|
|
42
|
+
"test:e2e": "bun test test/e2e",
|
|
43
|
+
"site:dev": "bun --port 4517 site/index.html",
|
|
44
|
+
"site:build": "bun run site/build.ts",
|
|
45
|
+
"check": "bun run typecheck && bun run test && bun run build && bun run site:build"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"three": ">=0.160.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/bun": "^1.3.2",
|
|
52
|
+
"@types/three": "^0.186.0",
|
|
53
|
+
"playwright": "^1.55.0",
|
|
54
|
+
"three": "^0.186.0",
|
|
55
|
+
"typescript": "^5.9.3"
|
|
56
|
+
},
|
|
57
|
+
"keywords": ["three", "threejs", "character", "plush", "fur", "mascot", "3d", "webgl"]
|
|
58
|
+
}
|