feat(effects): migrate battle effects to VFX

- add WebP frame-sequence assets for lightning, electric accent, fire, sunbeam, poison, rain, water, and rounded fog effects
- replace procedural lightning rendering with VFX frame playback
- align lightning impact to the click point
- remove the full-screen lightning blur/flash overlay
- add electric accent playback for lightning impact dispersion
- keep lightning scorch marks as the persistent ground trace after the action effect
- consolidate the lightning tool into a single public `Молния` action
- replace procedural fire with the VFX fire implementation
- remove the separate `Огонь 2` tool and keep the VFX fire under `Огонь`
- keep VFX fire brightness fixed at full intensity
- preserve fire as a field effect with brush placement and eraser support
- replace procedural sunbeam with the Pulse Discharge VFX implementation
- remove the separate `Луч света 2` tool and keep the VFX sunbeam under `Луч света`
- align sunbeam impact to the click point instead of the raw frame center
- remove the full-screen sunbeam flash overlay
- add Dust Burst VFX playback for poison cloud
- tint poison smoke green
- keep the skull glyph rendered above the poison smoke
- align poison smoke bottom to the click point
- remove the old procedural poison cloud implementation
- remove the separate `Яд 2` tool and keep the VFX poison under `Яд`
- replace procedural rain strokes with medium-rain VFX frame playback
- recolor rain frames to gray/light-gray so rain no longer renders black
- preserve rain as a field effect with brush placement and eraser support
- replace static water fill with generated animated water VFX frames
- render water VFX through the existing stroke mask so water keeps the painted brush shape
- preserve water as a field effect with brush placement and eraser support
- replace procedural fog texture generation with Smokey Atmosphere VFX frames
- generate rounded fog VFX frames with soft alpha falloff at the edges
- size each fog VFX stamp to the brush diameter
- remove per-stamp fog drift and rotation so fog elements stay fixed in place
- keep fog animation limited to internal frame playback
- preload VFX frame textures for smoother first use
- cache VFX textures per Pixi instance to avoid duplicate decoding
- filter expired action instances before node synchronization to stop old effects from flashing back during rapid casts
- add brush-stroke erasing for field effects using the same model as scene darkness reveal
- keep whole-instance erasing for action effects
- update effect hit tests for the consolidated VFX action effects
- update effect store pruning for renamed and consolidated action effects
- update control-panel buttons for consolidated fire, sunbeam, poison, and VFX-backed field effects
- update editor localization for removed and renamed effect tools
- add tests for field-effect brush erasing
- update eraser hit-test coverage for VFX-backed effects
- update effects store lifetime pruning tests for consolidated poison
- update control-panel tests for consolidated effect buttons
- include the field-effect eraser test in the default test script

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-07-03 21:20:43 +08:00
parent f5240d1623
commit 089da3cae0
334 changed files with 1203 additions and 671 deletions
+1 -1
View File
@@ -64,7 +64,7 @@ void test('pruneExpired: луч света удаляется после lifetim
assert.equal(store.getState().instances.length, 0); assert.equal(store.getState().instances.length, 0);
}); });
void test('pruneExpired: облако яда удаляется после lifetime', () => { void test('pruneExpired: яд удаляется после lifetime', () => {
const store = new EffectsStore(); const store = new EffectsStore();
store.dispatch({ store.dispatch({
kind: 'instance.add', kind: 'instance.add',
+19 -4
View File
@@ -1,5 +1,6 @@
import crypto from 'node:crypto'; import crypto from 'node:crypto';
import { applyFieldEffectEraserStroke } from '../../shared/fieldEffectEraser';
import type { EffectsEvent, EffectsState, EffectToolState } from '../../shared/types'; import type { EffectsEvent, EffectsState, EffectToolState } from '../../shared/types';
function nowMs(): number { function nowMs(): number {
@@ -35,7 +36,7 @@ export class EffectsStore {
dispatch(event: EffectsEvent): EffectsState { dispatch(event: EffectsEvent): EffectsState {
const s = this.state; const s = this.state;
const next: EffectsState = applyEvent(s, event); const next: EffectsState = applyEvent(s, event, (prefix) => this.makeId(prefix));
this.state = next; this.state = next;
return next; return next;
} }
@@ -47,13 +48,18 @@ export class EffectsStore {
const kept = this.state.instances.filter((i) => { const kept = this.state.instances.filter((i) => {
// Пятно льда не истекает по таймеру (только «очистить все» или ластик в UI). // Пятно льда не истекает по таймеру (только «очистить все» или ластик в UI).
if (i.type === 'ice' || i.type === 'shadow') return true; if (i.type === 'ice' || i.type === 'shadow') return true;
if (i.type === 'lightning' || i.type === 'sunbeam' || i.type === 'poisonCloud' || i.type === 'darkness') { if (
i.type === 'lightning' ||
i.type === 'sunbeam' ||
i.type === 'poisonCloud' ||
i.type === 'darkness'
) {
return now - i.createdAtMs < i.lifetimeMs; return now - i.createdAtMs < i.lifetimeMs;
} }
if (i.type === 'scorch') { if (i.type === 'scorch') {
return now - i.createdAtMs < i.lifetimeMs; return now - i.createdAtMs < i.lifetimeMs;
} }
if (i.type === 'fog' || i.type === 'water') { if (i.type === 'fog' || i.type === 'fire' || i.type === 'rain' || i.type === 'water') {
if (i.lifetimeMs === null) return true; if (i.lifetimeMs === null) return true;
return now - i.createdAtMs < i.lifetimeMs; return now - i.createdAtMs < i.lifetimeMs;
} }
@@ -74,7 +80,11 @@ export class EffectsStore {
} }
} }
function applyEvent(state: EffectsState, event: EffectsEvent): EffectsState { function applyEvent(
state: EffectsState,
event: EffectsEvent,
makeId: (prefix: string) => string,
): EffectsState {
const bump = (patch: Omit<EffectsState, 'revision' | 'serverNowMs'>): EffectsState => ({ const bump = (patch: Omit<EffectsState, 'revision' | 'serverNowMs'>): EffectsState => ({
...patch, ...patch,
revision: state.revision + 1, revision: state.revision + 1,
@@ -89,6 +99,11 @@ function applyEvent(state: EffectsState, event: EffectsEvent): EffectsState {
return bump({ ...state, instances: [...state.instances, event.instance] }); return bump({ ...state, instances: [...state.instances, event.instance] });
case 'instance.remove': case 'instance.remove':
return bump({ ...state, instances: state.instances.filter((i) => i.id !== event.id) }); return bump({ ...state, instances: state.instances.filter((i) => i.id !== event.id) });
case 'field.erase':
return bump({
...state,
instances: applyFieldEffectEraserStroke(state.instances, event.points, event.radiusN, makeId),
});
default: { default: {
// Exhaustiveness // Exhaustiveness
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
+53 -25
View File
@@ -19,8 +19,8 @@ import { getFreezeEffectLifeMs, playFreezeEffectSound } from './freezeSfx';
import { getPoisonCloudEffectLifeMs, playPoisonCloudEffectSound } from './poisonCloudSfx'; import { getPoisonCloudEffectLifeMs, playPoisonCloudEffectSound } from './poisonCloudSfx';
import { getSunbeamEffectLifeMs, playSunbeamEffectSound } from './sunbeamSfx'; import { getSunbeamEffectLifeMs, playSunbeamEffectSound } from './sunbeamSfx';
/** Длительность визуала молнии (мс). */ /** Длительность молнии: быстрый удар + акцент в точке попадания. */
const LIGHTNING_EFFECT_MS = 180; const LIGHTNING_EFFECT_MS = 840;
function formatTime(sec: number): string { function formatTime(sec: number): string {
if (!Number.isFinite(sec) || sec < 0) return '0:00'; if (!Number.isFinite(sec) || sec < 0) return '0:00';
@@ -154,8 +154,7 @@ export function ControlApp() {
const project = session?.project ?? null; const project = session?.project ?? null;
const currentGraphNodeId = project?.currentGraphNodeId ?? null; const currentGraphNodeId = project?.currentGraphNodeId ?? null;
const currentHistoryIdx = const currentHistoryIdx = currentGraphNodeId != null ? history.lastIndexOf(currentGraphNodeId) : -1;
currentGraphNodeId != null ? history.lastIndexOf(currentGraphNodeId) : -1;
const currentScene = const currentScene =
project && session?.currentSceneId ? project.scenes[session.currentSceneId] : undefined; project && session?.currentSceneId ? project.scenes[session.currentSceneId] : undefined;
const isVideoPreviewScene = currentScene?.previewAssetType === 'video'; const isVideoPreviewScene = currentScene?.previewAssetType === 'video';
@@ -660,6 +659,16 @@ export function ControlApp() {
return { x: Math.max(0, Math.min(1, x)), y: Math.max(0, Math.min(1, y)) }; return { x: Math.max(0, Math.min(1, x)), y: Math.max(0, Math.min(1, y)) };
} }
function dispatchFieldEraserPoints(points: { x: number; y: number; tMs: number }[]): void {
if (points.length === 0) return;
void fx.dispatch({ kind: 'field.erase', points, radiusN: toolRef.current.radiusN });
}
function tryEraseActionEffect(p: { x: number; y: number }): void {
const id = pickEraseTargetId(fxState?.instances ?? [], p, toolRef.current.radiusN);
if (id) void fx.dispatch({ kind: 'instance.remove', id });
}
async function commitStroke(): Promise<void> { async function commitStroke(): Promise<void> {
if (isVideoPreviewScene) { if (isVideoPreviewScene) {
brushRef.current = null; brushRef.current = null;
@@ -669,6 +678,10 @@ export function ControlApp() {
if (!fxState) return; if (!fxState) return;
const b = brushRef.current; const b = brushRef.current;
if (!b) return; if (!b) return;
if (b.tool === 'eraser') {
brushRef.current = null;
return;
}
const createdAtMs = Date.now(); const createdAtMs = Date.now();
const seed = Math.floor(Math.random() * 1_000_000_000); const seed = Math.floor(Math.random() * 1_000_000_000);
@@ -697,8 +710,7 @@ export function ControlApp() {
createdAtMs, createdAtMs,
points: b.points, points: b.points,
radiusN: tool.radiusN, radiusN: tool.radiusN,
// Огонь визуально ярче, но всё равно ограничиваемся безопасными пределами. opacity: 1,
opacity: Math.max(0.12, Math.min(0.95, tool.intensity)),
lifetimeMs: null, lifetimeMs: null,
}, },
}); });
@@ -790,8 +802,8 @@ export function ControlApp() {
createdAtMs, createdAtMs,
start, start,
end, end,
widthN: Math.max(0.01, tool.radiusN * 0.9), widthN: Math.max(0.014, tool.radiusN * 1.15),
intensity: Math.max(0.9, Math.min(1.2, tool.intensity * 1.35)), intensity: Math.max(1, Math.min(1.4, tool.intensity * 1.45)),
lifetimeMs: LIGHTNING_EFFECT_MS, lifetimeMs: LIGHTNING_EFFECT_MS,
}, },
}); });
@@ -800,12 +812,12 @@ export function ControlApp() {
instance: { instance: {
id: `sc_${String(createdAtMs)}_${String(seed)}`, id: `sc_${String(createdAtMs)}_${String(seed)}`,
type: 'scorch', type: 'scorch',
seed: seed ^ 0x5a5a5a, seed: seed ^ 0x7a7a7a,
createdAtMs, createdAtMs,
at: end, at: end,
radiusN: Math.max(0.03, tool.radiusN * 0.625), radiusN: Math.max(0.04, tool.radiusN * 0.82),
opacity: 0.92, opacity: 0.96,
lifetimeMs: 60_000, lifetimeMs: 90_000,
}, },
}); });
playLightningEffectSound(); playLightningEffectSound();
@@ -913,7 +925,7 @@ export function ControlApp() {
createdAtMs, createdAtMs,
points: b.points, points: b.points,
radiusN: tool.radiusN, radiusN: tool.radiusN,
opacity: Math.max(0.12, Math.min(0.75, tool.intensity * 0.85)), opacity: 1,
lifetimeMs: null, lifetimeMs: null,
}; };
} }
@@ -964,8 +976,8 @@ export function ControlApp() {
createdAtMs, createdAtMs,
start: { x: last.x, y: 0 }, start: { x: last.x, y: 0 },
end: { x: last.x, y: last.y }, end: { x: last.x, y: last.y },
widthN: Math.max(0.01, tool.radiusN * 0.9), widthN: Math.max(0.014, tool.radiusN * 1.15),
intensity: Math.max(0.9, Math.min(1.2, tool.intensity * 1.35)), intensity: Math.max(1, Math.min(1.4, tool.intensity * 1.45)),
lifetimeMs: LIGHTNING_EFFECT_MS, lifetimeMs: LIGHTNING_EFFECT_MS,
}; };
} }
@@ -1154,7 +1166,9 @@ export function ControlApp() {
iconOnly iconOnly
title={t('control.darkness')} title={t('control.darkness')}
ariaLabel={t('control.darkness')} ariaLabel={t('control.darkness')}
onClick={() => void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'darkness' } })} onClick={() =>
void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'darkness' } })
}
> >
<span className={styles.iconGlyph}>🌑</span> <span className={styles.iconGlyph}>🌑</span>
</Button> </Button>
@@ -1288,11 +1302,7 @@ export function ControlApp() {
} }
/> />
{previewContentRect ? ( {previewContentRect ? (
<SceneDarknessOverlay <SceneDarknessOverlay state={sdState} overlayAlpha={0.5} viewport={previewContentRect} />
state={sdState}
overlayAlpha={0.5}
viewport={previewContentRect}
/>
) : null} ) : null}
<div <div
ref={brushCursorElRef} ref={brushCursorElRef}
@@ -1319,8 +1329,10 @@ export function ControlApp() {
layoutBrushCursor(); layoutBrushCursor();
(e.currentTarget as HTMLDivElement).setPointerCapture(e.pointerId); (e.currentTarget as HTMLDivElement).setPointerCapture(e.pointerId);
if (tool.tool === 'eraser') { if (tool.tool === 'eraser') {
const id = pickEraseTargetId(fxState?.instances ?? [], p, tool.radiusN); const pt = { x: p.x, y: p.y, tMs: Date.now() };
if (id) void fx.dispatch({ kind: 'instance.remove', id }); brushRef.current = { tool: 'eraser', startN: p, points: [pt] };
dispatchFieldEraserPoints([pt]);
tryEraseActionEffect(p);
return; return;
} }
brushRef.current = { brushRef.current = {
@@ -1345,8 +1357,24 @@ export function ControlApp() {
cursorPosRef.current = p; cursorPosRef.current = p;
layoutBrushCursor(); layoutBrushCursor();
if (tool.tool === 'eraser' && (e.buttons & 1) !== 0) { if (tool.tool === 'eraser' && (e.buttons & 1) !== 0) {
const id = pickEraseTargetId(fxState?.instances ?? [], p, tool.radiusN); const b = brushRef.current;
if (id) void fx.dispatch({ kind: 'instance.remove', id }); const pt = { x: p.x, y: p.y, tMs: Date.now() };
if (b?.tool !== 'eraser' || !b.points) {
brushRef.current = { tool: 'eraser', startN: p, points: [pt] };
dispatchFieldEraserPoints([pt]);
} else {
const last = b.points[b.points.length - 1];
if (last) {
const dx = p.x - last.x;
const dy = p.y - last.y;
const minStep = Math.max(0.004, tool.radiusN * 0.25);
if (dx * dx + dy * dy >= minStep * minStep) {
b.points.push(pt);
dispatchFieldEraserPoints([last, pt]);
}
}
}
tryEraseActionEffect(p);
return; return;
} }
const b = brushRef.current; const b = brushRef.current;
@@ -18,6 +18,7 @@ void test('ControlApp: звук молнии (public/molniya.mp3)', () => {
const src = readControlApp(); const src = readControlApp();
assert.ok(src.includes('molniya.mp3')); assert.ok(src.includes('molniya.mp3'));
assert.ok(src.includes('playLightningEffectSound')); assert.ok(src.includes('playLightningEffectSound'));
assert.ok(src.includes('LIGHTNING_EFFECT_MS'));
}); });
void test('ControlApp: звук заморозки (public/zamorozka.mp3)', () => { void test('ControlApp: звук заморозки (public/zamorozka.mp3)', () => {
@@ -56,7 +57,9 @@ void test('ControlApp: эффекты в пульте, иконки с тулт
assert.ok(src.includes('SceneDarknessOverlay')); assert.ok(src.includes('SceneDarknessOverlay'));
assert.ok(src.includes('useSceneDarknessState')); assert.ok(src.includes('useSceneDarknessState'));
assert.ok(src.includes("t('control.sunbeam')")); assert.ok(src.includes("t('control.sunbeam')"));
assert.ok(src.includes("t('control.lightning')"));
assert.ok(src.includes("title={t('control.water')}")); assert.ok(src.includes("title={t('control.water')}"));
assert.ok(src.includes("title={t('control.fire')}"));
assert.ok(src.includes("title={t('control.darkness')}")); assert.ok(src.includes("title={t('control.darkness')}"));
assert.ok(src.includes("title={t('control.poisonCloud')}")); assert.ok(src.includes("title={t('control.poisonCloud')}"));
assert.ok(src.includes("title={t('control.fog')}")); assert.ok(src.includes("title={t('control.fog')}"));
+2 -2
View File
@@ -187,7 +187,7 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
'help.section.effects.title': 'Эффекты поля и действий', 'help.section.effects.title': 'Эффекты поля и действий',
'help.section.effects.body': 'help.section.effects.body':
'Эффекты работают на сценах с картинкой, не на видео. Рисуйте в «Предпросмотр экрана» — игроки увидят то же на презентации.\n\nВыберите инструмент слева:\n• Эффекты поля (туман, дождь, огонь, вода) — зажмите левую кнопку и ведите по карте.\n• Эффекты действий (молния, луч света, заморозка, тьма, облако яда) — короткий клик или штрих; у некоторых есть звук.\n\nЕсли у сцены в свойствах включено «Затемнить сцену», появится блок «Управление затемнением» с «Кистью Открытия» 🔦. Водите по предпросмотру — тьма снимается на обоих экранах сразу. У игроков нераскрытое остаётся чёрным, у вас на пульте — полузатемнённым. Уже открытые участки сохраняются, пока идёт показ и вы снова попадаете на ту же карточку сцены на карте. Это не то же самое, что эффект «Тьма» 🌑 в блоке действий.\n\nЛастик 🧹 — клик или проведение по эффекту, чтобы убрать. «Очистить эффекты» — снять всё сразу.\n\n«Радиус кисти» под панелью — чем больше число, тем шире мазок.', 'Эффекты работают на сценах с картинкой, не на видео. Рисуйте в «Предпросмотр экрана» — игроки увидят то же на презентации.\n\nВыберите инструмент слева:\n• Эффекты поля (туман, дождь, огонь, вода) — зажмите левую кнопку и ведите по карте.\n• Эффекты действий (молния, луч света, заморозка, тьма, облако яда) — короткий клик или штрих; у некоторых есть звук.\n\nЕсли у сцены в свойствах включено «Затемнить сцену», появится блок «Управление затемнением» с «Кистью Открытия» 🔦. Водите по предпросмотру — тьма снимается на обоих экранах сразу. У игроков нераскрытое остаётся чёрным, у вас на пульте — полузатемнённым. Уже открытые участки сохраняются, пока идёт показ и вы снова попадаете на ту же карточку сцены на карте. Это не то же самое, что эффект «Тьма» 🌑 в блоке действий.\n\nЛастик 🧹 — для эффектов поля (туман, дождь, огонь, вода) водите кистью, как «Кистью Открытия» для затемнения: стирается только пройденный участок. Эффекты действий (молния, луч и т.д.) убираются целиком при клике или проведении по ним. «Очистить эффекты» — снять всё сразу.\n\n«Радиус кисти» под панелью — чем больше число, тем шире мазок.',
'help.section.presentation.title': 'Экран презентации', 'help.section.presentation.title': 'Экран презентации',
'help.section.presentation.body': 'help.section.presentation.body':
@@ -509,7 +509,7 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
'help.section.effects.title': 'Field and action effects', 'help.section.effects.title': 'Field and action effects',
'help.section.effects.body': 'help.section.effects.body':
'Effects work on image scenes, not video. Paint in Screen preview — players see the same on presentation.\n\nPick a tool on the left:\n• Field effects (fog, rain, fire, water) — hold the left button and brush on the map.\n• Action effects (lightning, sunbeam, freeze, darkness, poison cloud) — click or short stroke; some include sound.\n\nIf Darken scene is enabled in scene properties, a Darkness control section appears with the Opening brush 🔦. Brush on the preview to clear darkness on both screens at once. Unrevealed areas stay fully black for players and half-dark on your preview. Revealed areas are remembered while the show runs and you return to the same graph card. This is not the same as the Darkness 🌑 action effect.\n\nEraser 🧹 — click or drag over an effect to remove it. Clear effects removes everything at once.\n\nBrush radius under the panel — higher values mean a wider stroke.', 'Effects work on image scenes, not video. Paint in Screen preview — players see the same on presentation.\n\nPick a tool on the left:\n• Field effects (fog, rain, fire, water) — hold the left button and brush on the map.\n• Action effects (lightning, sunbeam, freeze, darkness, poison cloud) — click or short stroke; some include sound.\n\nIf Darken scene is enabled in scene properties, a Darkness control section appears with the Opening brush 🔦. Brush on the preview to clear darkness on both screens at once. Unrevealed areas stay fully black for players and half-dark on your preview. Revealed areas are remembered while the show runs and you return to the same graph card. This is not the same as the Darkness 🌑 action effect.\n\nEraser 🧹 — for field effects (fog, rain, fire, water), brush like the Opening brush for darkness: only the stroke area is erased. Action effects (lightning, sunbeam, etc.) are removed whole when you click or drag over them. Clear effects removes everything at once.\n\nBrush radius under the panel — higher values mean a wider stroke.',
'help.section.presentation.title': 'Presentation screen', 'help.section.presentation.title': 'Presentation screen',
'help.section.presentation.body': 'help.section.presentation.body':
Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Some files were not shown because too many files have changed in this diff Show More