Fresh の islands 上で pixi.js を動かしたい

pixi.js を Fresh の islands 上で動かしたかったのでいろいろと試しました。
最終的には、preact での UI の更新と pixi.js の描画の同期が取れました。

数値、ボタンは preact で 回る ■ が増える減るの部分は、pixi.js です。

実装 1 比較的基本

動き無し インタラクション無し

routes/index.tsx
1
2
3
4
5
6
7
8
9
10
11
import PixiContainer from "../islands/PixiContainer.tsx";

export default function Home() {
return (
<>
<div class="p-4 mx-auto max-w-screen-md">
<PixiContainer/>
</div>
</>
);
}
islands/PixiContainer.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { useEffect, useRef } from "preact/hooks";
import { Application, Graphics } from "https://esm.sh/pixi.js";

export default function PixiContainer(props) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const app = new Application({
view: canvasRef.current,
width: 100,
height: 100,
resolution:1,
});

let obj = new Graphics();
obj.beginFill(0xff0000);
obj.drawRect(0, 0, 20, 20);
obj.x = app.screen.width / 2;
obj.y = app.screen.height / 2;
obj.pivot.x = 10
obj.pivot.y = 10

app.stage.addChild(obj);
}, []);

return (
<div class="flex raw w-full">
<canvas ref={canvasRef} />
</div>
);
}

以上の実装で表示できるのがこちら。

動き有り インタラクション無し

routes/index.tsx
1
2
3
4
5
6
7
8
9
10
11
import PixiContainer from "../islands/PixiContainer.tsx";

export default function Home() {
return (
<>
<div class="p-4 mx-auto max-w-screen-md">
<PixiContainer/>
</div>
</>
);
}
islands/PixiContainer.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import { useEffect, useRef } from "preact/hooks";
import { Application, Graphics } from "https://esm.sh/pixi.js";

export default function PixiContainer(props) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const reqIdRef = useRef(null);
useEffect(() => {
const app = new Application({
view: canvasRef.current,
width: 100,
height: 100,
resolution:1,
});

let obj = new Graphics();
obj.beginFill(0xff0000);
obj.drawRect(0, 0, 20, 20);
obj.x = app.screen.width / 2;
obj.y = app.screen.height / 2;
obj.pivot.x = 10
obj.pivot.y = 10

app.stage.addChild(obj);

const loop = (time) => {
obj.rotation = 0.001 * time;
reqIdRef.current = requestAnimationFrame(loop);
};

loop();
return () => cancelAnimationFrame(reqIdRef.current);
}, []);

return (
<div class="flex raw w-full">
<canvas ref={canvasRef} />
</div>
);
}

動き有り インタラクション無 親コンポーネントにステータスを反映

routes/index.tsx
1
2
3
4
5
6
7
8
9
10
11
import Parent from "../islands/Parent.tsx";

export default function Home() {
return (
<>
<div class="p-4 mx-auto max-w-screen-md">
<Parent/>
</div>
</>
);
}
islands/Parent.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { useState } from "preact/hooks";
import PixiContainer from "../islands/PixiContainer.tsx";

export default function Parent(props) {
const [rotationView, setRotationView] = useState(0);

return (
<div class="flex flex-col w-full">
<div>
<p>{rotationView}</p>
</div>
<div>
<PixiContainer setRotationView={setRotationView} />
</div>
</div>
);
}
islands/PixiContainer.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import { useEffect, useRef } from "preact/hooks";
import { Application, Graphics } from "https://esm.sh/pixi.js";

export default function PixiContainer(
props: { setRotationView: (rotation: number) => void },
) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const reqIdRef = useRef(null);
useEffect(() => {
const app = new Application({
view: canvasRef.current,
width: 100,
height: 100,
resolution: 1,
});

let obj = new Graphics();
obj.beginFill(0xff0000);
obj.drawRect(0, 0, 20, 20);
obj.x = app.screen.width / 2;
obj.y = app.screen.height / 2;
obj.pivot.x = 10;
obj.pivot.y = 10;

app.stage.addChild(obj);

const loop = (time) => {
obj.rotation = 0.001 * time;
props.setRotationView(obj.rotation);
reqIdRef.current = requestAnimationFrame(loop);
};

loop();
return () => cancelAnimationFrame(reqIdRef.current);
}, []);

return (
<div class="flex raw w-full">
<canvas ref={canvasRef} />
</div>
);
}

動き有り インタラクション無 親コンポーネントにステータスを反映 親コンポーネントでの出し入れ有り

routes/index.tsx
1
2
3
4
5
6
7
8
9
10
11
import Parent from "../islands/Parent.tsx";

export default function Home() {
return (
<>
<div class="p-4 mx-auto max-w-screen-md">
<Parent/>
</div>
</>
);
}
islands/Parent.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { useState } from "preact/hooks";
import PixiContainer from "../islands/PixiContainer.tsx";

export default function Parent(props) {
const [viewSwitch, setViewSwitch] = useState(true);
const [rotationView, setRotationView] = useState(0);

return (
<div class="flex flex-col w-full">
<div>
<button class="border-2" onClick={() => setViewSwitch(!viewSwitch)}>
{viewSwitch ? "UnView" : "View"}
</button>
</div>

<div>
<p>{rotationView}</p>
</div>
<div>
{viewSwitch ? <PixiContainer setRotationView={setRotationView} /> : ""}
</div>
</div>
);
}
islands/PixiContainer.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import { useEffect, useRef } from "preact/hooks";
import { Application, Graphics } from "https://esm.sh/pixi.js";

export default function PixiContainer(
props: { setRotationView: (rotation: number) => void },
) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const reqIdRef = useRef(null);
useEffect(() => {
const app = new Application({
view: canvasRef.current,
width: 100,
height: 100,
resolution: 1,
});

let obj = new Graphics();
obj.beginFill(0xff0000);
obj.drawRect(0, 0, 20, 20);
obj.x = app.screen.width / 2;
obj.y = app.screen.height / 2;
obj.pivot.x = 10;
obj.pivot.y = 10;

app.stage.addChild(obj);

const loop = (time) => {
// 回転状況がtimeに依存しているので、再度開いたときも数値は途中から始まる
// obj.rotation += 0.001; などにすると開かれるたびに0から始まる
obj.rotation = 0.001 * time;
props.setRotationView(obj.rotation);
reqIdRef.current = requestAnimationFrame(loop);
};

loop();
return () => cancelAnimationFrame(reqIdRef.current);
}, []);

return (
<div class="flex raw w-full">
<canvas ref={canvasRef} />
</div>
);
}

動き有り インタラクション有り

参考:
PixiJS v7 - examples - events- Dragging

routes/index.tsx
1
2
3
4
5
6
7
8
9
10
11
import Parent from "../islands/Parent.tsx";

export default function Home() {
return (
<>
<div class="p-4 mx-auto max-w-screen-md">
<Parent/>
</div>
</>
);
}
islands/PixiContainer.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import { useEffect, useRef } from "preact/hooks";
import { Application, Graphics } from "https://esm.sh/pixi.js";

export default function PixiContainer(props) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const reqIdRef = useRef(null);
useEffect(() => {
const app = new Application({
view: canvasRef.current,
width: 100,
height: 100,
resolution:1,
});

let obj = new Graphics();
obj.beginFill(0xff0000);
obj.drawRect(0, 0, 20, 20);
obj.x = app.screen.width / 2;
obj.y = app.screen.height / 2;
obj.pivot.x = 10
obj.pivot.y = 10
obj.interactive = true;

obj.on(
"pointerdown",
onDragStart,
obj
);

let dragTarget: Graphics|null = null;
function onDragStart() {
this.alpha = 0.5;
dragTarget= this;
app.stage.on("pointermove", onDragMove);
}

const onDragMove = (event) => {
if (!dragTarget) return

dragTarget.parent.toLocal(event.global, null, dragTarget.position);
};

const onDragEnd = () => {
if (!dragTarget) return

app.stage.off("pointermove", onDragMove);
dragTarget.alpha = 1;
dragTarget = null;
};

app.stage.addChild(obj);
app.stage.interactive = true;
app.stage.hitArea = app.screen;
app.stage.on('pointerup', onDragEnd);
app.stage.on('pointerupoutside', onDragEnd);

const loop = (time) => {
obj.rotation = 0.001 * time;
reqIdRef.current = requestAnimationFrame(loop);
};

loop();
return () => cancelAnimationFrame(reqIdRef.current);
}, []);

return (
<div class="flex raw w-full">
<canvas ref={canvasRef} />
</div>
);
}

動き有り インタラクション有り 親コンポーネントにステータスを反映

routes/index.tsx
1
2
3
4
5
6
7
8
9
10
11
import Parent from "../islands/Parent.tsx";

export default function Home() {
return (
<>
<div class="p-4 mx-auto max-w-screen-md">
<Parent/>
</div>
</>
);
}
islands/Parent.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { useState } from "preact/hooks";
import PixiContainer from "../islands/PixiContainer.tsx";

export default function Parent(props) {
const [rotationView, setRotationView] = useState(0);
const [positionView, setPositionView] = useState({ x: 0, y: 0 });

return (
<div class="flex flex-col w-full">
<div>
<p>rotation = {rotationView}</p>
</div>
<div>
<p>position.x = {positionView.x}</p>
<p>position.y = {positionView.y}</p>
</div>
<div>
<PixiContainer
setRotationView={setRotationView}
setPositionView={setPositionView}
/>
</div>
</div>
);
}
islands/PixiContainer.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import { useEffect, useRef } from "preact/hooks";
import { Application, Graphics } from "https://esm.sh/pixi.js";

export default function PixiContainer(props: {
setRotationView: (rotation: number) => void;
setPositionView: (position: { x: number; y: number }) => void;
}) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const reqIdRef = useRef(null);
useEffect(() => {
const app = new Application({
view: canvasRef.current,
width: 100,
height: 100,
resolution: 1,
});

let obj = new Graphics();
obj.beginFill(0xff0000);
obj.drawRect(0, 0, 20, 20);
obj.x = app.screen.width / 2;
obj.y = app.screen.height / 2;
obj.pivot.x = 10;
obj.pivot.y = 10;
obj.interactive = true;

obj.on(
"pointerdown",
onDragStart,
obj,
);

let dragTarget: Graphics | null = null;
function onDragStart() {
this.alpha = 0.5;
dragTarget = this;
app.stage.on("pointermove", onDragMove);
}

const onDragMove = (event) => {
if (!dragTarget) return;
dragTarget.parent.toLocal(event.global, null, dragTarget.position);
props.setPositionView({
x: dragTarget.position.x,
y: dragTarget.position.y,
});
};

const onDragEnd = () => {
if (!dragTarget) return;

app.stage.off("pointermove", onDragMove);
dragTarget.alpha = 1;
dragTarget = null;
};

app.stage.addChild(obj);
app.stage.interactive = true;
app.stage.hitArea = app.screen;
app.stage.on("pointerup", onDragEnd);
app.stage.on("pointerupoutside", onDragEnd);

const loop = (time) => {
obj.rotation = 0.001 * time;
props.setRotationView(obj.rotation);
reqIdRef.current = requestAnimationFrame(loop);
};

loop();
return () => cancelAnimationFrame(reqIdRef.current);
}, []);

return (
<div class="flex raw w-full">
<canvas ref={canvasRef} />
</div>
);
}

動き有り インタラクション有り 親コンポーネントにステータスを反映 親コンポーネントでの出し入れ有り

routes/index.tsx
1
2
3
4
5
6
7
8
9
10
11
import Parent from "../islands/Parent.tsx";

export default function Home() {
return (
<>
<div class="p-4 mx-auto max-w-screen-md">
<Parent/>
</div>
</>
);
}
islands/Parent.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { useState } from "preact/hooks";
import PixiContainer from "../islands/PixiContainer.tsx";

export default function Parent(props) {
const [viewSwitch, setViewSwitch] = useState(true);
const [rotationView, setRotationView] = useState(0);
const [positionView, setPositionView] = useState({ x: 0, y: 0 });

return (
<div class="flex flex-col w-full">
<div>
<button class="border-2" onClick={() => setViewSwitch(!viewSwitch)}>
{viewSwitch ? "UnView" : "View"}
</button>
</div>
<div>
<p>rotation = {rotationView}</p>
</div>
<div>
<p>position.x = {positionView.x}</p>
<p>position.y = {positionView.y}</p>
</div>
<div>
{viewSwitch ?
<PixiContainer
setRotationView={setRotationView}
setPositionView={setPositionView}
/>
: ""}
</div>
</div>
);
}
islands/PixiContainer.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import { useEffect, useRef } from "preact/hooks";
import { Application, Graphics } from "https://esm.sh/pixi.js";

export default function PixiContainer(props: {
setRotationView: (rotation: number) => void;
setPositionView: (position: { x: number; y: number }) => void;
}) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const reqIdRef = useRef(null);
useEffect(() => {
const app = new Application({
view: canvasRef.current,
width: 100,
height: 100,
resolution: 1,
});

let obj = new Graphics();
obj.beginFill(0xff0000);
obj.drawRect(0, 0, 20, 20);
obj.x = app.screen.width / 2;
obj.y = app.screen.height / 2;
obj.pivot.x = 10;
obj.pivot.y = 10;
obj.interactive = true;

obj.on(
"pointerdown",
onDragStart,
obj,
);

let dragTarget: Graphics | null = null;
function onDragStart() {
this.alpha = 0.5;
dragTarget = this;
app.stage.on("pointermove", onDragMove);
}

const onDragMove = (event) => {
if (!dragTarget) return;
dragTarget.parent.toLocal(event.global, null, dragTarget.position);
props.setPositionView({
x: dragTarget.position.x,
y: dragTarget.position.y,
});
};

const onDragEnd = () => {
if (!dragTarget) return;

app.stage.off("pointermove", onDragMove);
dragTarget.alpha = 1;
dragTarget = null;
};

app.stage.addChild(obj);
app.stage.interactive = true;
app.stage.hitArea = app.screen;
app.stage.on("pointerup", onDragEnd);
app.stage.on("pointerupoutside", onDragEnd);

const loop = (time) => {
obj.rotation = 0.001 * time;
props.setRotationView(obj.rotation);
reqIdRef.current = requestAnimationFrame(loop);
};

loop();
return () => cancelAnimationFrame(reqIdRef.current);
}, []);

return (
<div class="flex raw w-full">
<canvas ref={canvasRef} />
</div>
);
}

実装 2 オブジェクト追加/削除操作

親コンポーネントでオブジェクト追加

routes/index.tsx
1
2
3
4
5
6
7
8
9
10
11
import Parent from "../islands/Parent.tsx";

export default function Home() {
return (
<>
<div class="p-4 mx-auto max-w-screen-md">
<Parent/>
</div>
</>
);
}
islands/Parent.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import { useState, useRef } from "preact/hooks";
import PixiContainer from "../islands/PixiContainer.tsx";

export default function Parent(props) {
const [viewSwitch, setViewSwitch] = useState(true);
const [positionView, setPositionView] = useState({ x: 0, y: 0 });
const addFunctionRef = useRef()

const setterAddFunction =(addFunction)=>{
// 子コンポーネントから呼び出してもらい、子コンポーネントの関数をもらって来る
addFunctionRef.current = addFunction
}
const callAddFunction =()=>{
// 子コンポーネントのメソッドを呼び出す
addFunctionRef.current()
}

return (
<div class="flex flex-col w-full">
<div>
<button class="border-2" onClick={() => setViewSwitch(!viewSwitch)}>
{viewSwitch ? "UnView" : "View"}
</button>
</div>
<div>
<p>position.x = {positionView.x}</p>
<p>position.y = {positionView.y}</p>
</div>

{viewSwitch ?
<div>
<button class="border-2" onClick={() => callAddFunction()} >
Add Object
</button>
<PixiContainer
setPositionView={setPositionView}
setterAddFunction={setterAddFunction}
/>
</div>
: ""}
</div>
);
}
islands/PixiContainer.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import { useEffect, useRef } from "preact/hooks";
import { Application, Graphics } from "https://esm.sh/pixi.js";

export default function PixiContainer(props: {
setPositionView: (position: { x: number; y: number }) => void;
setterAddFunction: (addFunction: () => void) =>void
}) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const reqIdRef = useRef(null);
useEffect(() => {
const app = new Application({
view: canvasRef.current,
width: 100,
height: 100,
resolution: 1,
});

const objects: Graphics[] = [];

const addObject = ()=>{
let obj = new Graphics();
obj.beginFill(0xff0000);
obj.drawRect(0, 0, 20, 20);
obj.x = app.screen.width / 2;
obj.y = app.screen.height / 2;
obj.pivot.x = 10;
obj.pivot.y = 10;
obj.interactive = true;
obj.on(
"pointerdown",
onDragStart,
obj,
);
app.stage.addChild(obj);
objects.push(obj)
}

// 親コンポーネントに、関数の引き渡し
props.setterAddFunction(addObject)


let dragTarget: Graphics | null = null;
function onDragStart() {
this.alpha = 0.5;
dragTarget = this;
app.stage.on("pointermove", onDragMove);
}

const onDragMove = (event) => {
if (!dragTarget) return;
dragTarget.parent.toLocal(event.global, null, dragTarget.position);
props.setPositionView({
x: dragTarget.position.x,
y: dragTarget.position.y,
});
};

const onDragEnd = () => {
if (!dragTarget) return;

app.stage.off("pointermove", onDragMove);
dragTarget.alpha = 1;
dragTarget = null;
};

app.stage.interactive = true;
app.stage.hitArea = app.screen;
app.stage.on("pointerup", onDragEnd);
app.stage.on("pointerupoutside", onDragEnd);

const loop = (time) => {
objects.forEach((obj)=>{
obj.rotation = 0.001 * time;
})
reqIdRef.current = requestAnimationFrame(loop);
};

loop();
return () => cancelAnimationFrame(reqIdRef.current);
}, []);

return (
<div class="flex raw w-full">
<canvas ref={canvasRef} />
</div>
);
}

親コンポーネントでオブジェクト追加 開きなおしたとき前の状態を復元

routes/index.tsx
1
2
3
4
5
6
7
8
9
10
11
import Parent from "../islands/Parent.tsx";

export default function Home() {
return (
<>
<div class="p-4 mx-auto max-w-screen-md">
<Parent/>
</div>
</>
);
}
islands/Parent.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import { useRef, useState } from "preact/hooks";
import PixiContainer from "../islands/PixiContainer.tsx";
import { Graphics } from "https://esm.sh/pixi.js";

export default function Parent(props) {
const [viewSwitch, setViewSwitch] = useState(true);
const [positionView, setPositionView] = useState({ x: 0, y: 0 });
const [objects, setObjects] = useState([]);
const addFunctionRef = useRef();

const setterAddFunction = (addFunction) => {
// 子コンポーネントから呼び出してもらい、子コンポーネントの関数をもらって来る
addFunctionRef.current = addFunction;
};
const callAddFunction = () => {
// 子コンポーネントのメソッドを呼び出す
addFunctionRef.current();
};

// 子コンポーネントから呼び出され、オブジェクトの内容を登録
const setterObjects = (objects: Graphics[]) => {
const objectData = objects.map((object) => {
return {
x: object.x,
y: object.y,
};
});

setObjects(objectData);
};

return (
<div class="flex flex-col w-full">
<div>
<button class="border-2" onClick={() => setViewSwitch(!viewSwitch)}>
{viewSwitch ? "UnView" : "View"}
</button>
</div>
<div>
<p>position.x = {positionView.x}</p>
<p>position.y = {positionView.y}</p>
</div>

<div>
<p>Objects</p>
{objects.map((object) => (
<p>object.x = {object.x}, object.y = {object.y}</p>
))}
</div>

{viewSwitch
? (
<div>
<button class="border-2" onClick={() => callAddFunction()}>
Add Object
</button>
<PixiContainer
setPositionView={setPositionView}
setterAddFunction={setterAddFunction}
setterObjects={setterObjects}
initObjects={objects}
/>
</div>
)
: ""}
</div>
);
}
islands/PixiContainer.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import { useEffect, useRef } from "preact/hooks";
import { Application, Graphics } from "https://esm.sh/pixi.js";

export default function PixiContainer(props: {
setPositionView: (position: { x: number; y: number }) => void;
setterAddFunction: (addFunction: () => void) => void;
setterObjects: (objects: (objects: Graphics[]) => void) => void;
objects: { x: number; y: number }[];
}) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const reqIdRef = useRef(null);
useEffect(() => {
const app = new Application({
view: canvasRef.current,
width: 100,
height: 100,
resolution: 1,
});

const objects: Graphics[] = [];

const addObject = (x: undefined | number, y: undefined | number) => {
let obj = new Graphics();
obj.beginFill(0xff0000);
obj.drawRect(0, 0, 20, 20);
obj.x = x || app.screen.width / 2;
obj.y = y || app.screen.height / 2;
obj.pivot.x = 10;
obj.pivot.y = 10;
obj.interactive = true;
obj.on(
"pointerdown",
onDragStart,
obj,
);
app.stage.addChild(obj);
objects.push(obj);
};

// 親コンポーネントに、関数の引き渡し
props.setterAddFunction(addObject);

// 親コンポーネントのパラメータから、オブジェクトを復元
props.initObjects.forEach((initObject) => {
addObject(initObject.x, initObject.y);
});

let dragTarget: Graphics | null = null;
function onDragStart() {
this.alpha = 0.5;
dragTarget = this;
app.stage.on("pointermove", onDragMove);
}

const onDragMove = (event) => {
if (!dragTarget) return;
dragTarget.parent.toLocal(event.global, null, dragTarget.position);
props.setPositionView({
x: dragTarget.position.x,
y: dragTarget.position.y,
});
};

const onDragEnd = () => {
if (!dragTarget) return;

app.stage.off("pointermove", onDragMove);
dragTarget.alpha = 1;
dragTarget = null;
};

app.stage.interactive = true;
app.stage.hitArea = app.screen;
app.stage.on("pointerup", onDragEnd);
app.stage.on("pointerupoutside", onDragEnd);

const loop = (time) => {
objects.forEach((obj) => {
obj.rotation = 0.001 * time;
});
// オブジェクトの内容を登録 ループで呼ぶのはやりすぎとも思うけど
props.setterObjects(objects);
reqIdRef.current = requestAnimationFrame(loop);
};

loop();
return () => cancelAnimationFrame(reqIdRef.current);
}, []);

return (
<div class="flex raw w-full">
<canvas ref={canvasRef} />
</div>
);
}

親コンポーネントでオブジェクト追加 開きなおしたとき前の状態を復元 削除モード

routes/index.tsx
1
2
3
4
5
6
7
8
9
10
11
import Parent from "../islands/Parent.tsx";

export default function Home() {
return (
<>
<div class="p-4 mx-auto max-w-screen-md">
<Parent/>
</div>
</>
);
}
islands/Parent.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import { useRef, useState } from "preact/hooks";
import PixiContainer from "../islands/PixiContainer.tsx";
import { Graphics } from "https://esm.sh/pixi.js";

export default function Parent(props) {
const [viewSwitch, setViewSwitch] = useState(true);
const [positionView, setPositionView] = useState({ x: 0, y: 0 });
const [objects, setObjects] = useState([]);
const addFunctionRef = useRef();
const removeModeFunctionRef = useRef();
const [isRemoveMode, setIsRemoveMode] = useState(false);

const setterAddFunction = (addFunction) => {
addFunctionRef.current = addFunction;
};
const callAddFunction = () => {
addFunctionRef.current();
};

const setterRemoveModeFunction = (addFunction) => {
removeModeFunctionRef.current = addFunction;
};
const callRemoveModeFunction = () => {
removeModeFunctionRef.current(!isRemoveMode);
};

const updateRemoveMode = (mode: boolean) => {
setIsRemoveMode(mode);
};

// 子コンポーネントから呼び出され、オブジェクトの内容を登録
const setterObjects = (objects: Graphics[]) => {
const objectData = objects.map((object) => {
return {
x: object.x,
y: object.y,
};
});

setObjects(objectData);
};

return (
<div class="flex flex-col w-full">
<div>
<button class="border-2" onClick={() => setViewSwitch(!viewSwitch)}>
{viewSwitch ? "UnView" : "View"}
</button>
</div>
<div>
<p>position.x = {positionView.x}</p>
<p>position.y = {positionView.y}</p>
</div>

<div>
<p>Objects</p>
{objects.map((object) => (
<p>object.x = {object.x}, object.y = {object.y}</p>
))}
</div>

{viewSwitch
? (
<div>
<button class="border-2" onClick={() => callAddFunction()}>
Add Object
</button>
<button class="border-2 ml-2" onClick={() => callRemoveModeFunction()}>
{isRemoveMode ? "lift Remove Mode" : "Do Remove Mode"}
</button>
<PixiContainer
setPositionView={setPositionView}
setterAddFunction={setterAddFunction}
setterRemoveModeFunction={setterRemoveModeFunction}
setterObjects={setterObjects}
removeModeFunctionRef
initObjects={objects}
initRemoveMode={isRemoveMode}
updateRemoveMode={updateRemoveMode}
/>
</div>
)
: ""}
</div>
);
}
islands/PixiContainer.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
import { useEffect, useRef } from "preact/hooks";
import { Application, Graphics } from "https://esm.sh/pixi.js";

export default function PixiContainer(props: {
setPositionView: (position: { x: number; y: number }) => void;
setterAddFunction: (addFunction: () => void) => void;
setterRemoveModeFunction: (removeModeFunction: () => void) => void;
setterObjects: (objects: (objects: Graphics[]) => void) => void;
objects: { x: number; y: number }[];
initRemoveMode: boolean;
updateRemoveMode: (isRemoveMode: boolean) => void;
}) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const reqIdRef = useRef(null);
useEffect(() => {
const app = new Application({
view: canvasRef.current,
width: 100,
height: 100,
resolution: 1,
});


// 削除モード設定
let removeMode = props.initRemoveMode;
const setRemoveMode = (mode: boolean) => {
removeMode = mode;
props.updateRemoveMode(mode);
};

props.setterRemoveModeFunction(setRemoveMode);

let objects: Graphics[] = [];

const addObject = (x: undefined | number, y: undefined | number) => {
let obj = new Graphics();
obj.beginFill(0xff0000);
obj.drawRect(0, 0, 20, 20);
obj.x = x || app.screen.width / 2;
obj.y = y || app.screen.height / 2;
obj.pivot.x = 10;
obj.pivot.y = 10;
obj.interactive = true;
obj.on(
"pointerdown",
onPointerDown,
obj,
);
app.stage.addChild(obj);
objects.push(obj);
};

// 親コンポーネントに、関数の引き渡し
props.setterAddFunction(addObject);

// 親コンポーネントのパラメータから、オブジェクトを復元
props.initObjects.forEach((initObject) => {
addObject(initObject.x, initObject.y);
});

let dragTarget: Graphics | null = null;
function onPointerDown() {
if (removeMode) {
// 削除モードが有効の時、
// objects から、対象オブジェクトを除外して再割り当て
objects = objects.filter((object) => object !== this);
// 対象オブジェクトを削除
this.destroy();
// 削除モードを終了
return;
}
this.alpha = 0.5;
dragTarget = this;
app.stage.on("pointermove", onDragMove);
}

const onDragMove = (event) => {
if (!dragTarget) return;
dragTarget.parent.toLocal(event.global, null, dragTarget.position);
props.setPositionView({
x: dragTarget.position.x,
y: dragTarget.position.y,
});
};

const onDragEnd = () => {
if (!dragTarget) return;

app.stage.off("pointermove", onDragMove);
dragTarget.alpha = 1;
dragTarget = null;
};

app.stage.interactive = true;
app.stage.hitArea = app.screen;
app.stage.on("pointerup", onDragEnd);
app.stage.on("pointerupoutside", onDragEnd);

const loop = (time) => {
objects.forEach((obj) => {
obj.rotation = 0.001 * time;
});
props.setterObjects(objects);
reqIdRef.current = requestAnimationFrame(loop);
};

loop();
return () => cancelAnimationFrame(reqIdRef.current);
}, []);

return (
<div class="flex raw w-full">
<canvas ref={canvasRef} />
</div>
);
}


実装と動作だけ列挙しました。

pixi.js と Fresh でしばらくとあるものを作りたいので、進めていきます。

では。


2023年04月16日追記

esm.sh のビルド結果が変わったからなのか、ブラウザに送られてくるjsに Deno.build.os == hoge というコードが混ざってくるようになってしまった。
ブラウザには、Deno.hoge なAPIは無いので当然エラーになる。

調べてみると、pixi.jsが自前でesmビルドした結果が公開されていたのでそちらに読み込み先を切り替えるとちゃんと動作した

1
2
- import { Application, Graphics } from "https://esm.sh/pixi.js";
+ import { Application, Graphics } from "https://pixijs.download/dev/pixi.mjs";

github - pixijs/pixijs - Import PixiJS as ESM without bundlers