add system font loading

improve drawing
implement staggered text
begin refactor of drawing code
This commit is contained in:
2023-05-24 00:24:16 +02:00
parent 8523e44029
commit 330fa6a7f0
28 changed files with 844 additions and 207 deletions

View File

@@ -5,15 +5,27 @@ import { useTimelineStore } from "stores/timeline.store";
import InitCanvasKit, { CanvasKit } from "canvaskit-wasm";
import { Surface } from "canvaskit-wasm";
import drawText from "drawers/text";
import drawBox from "drawers/box";
import drawRect from "drawers/rect";
import { Entities, EntityType } from "primitives/Entities";
import drawEllipse from "drawers/ellipse";
import { useRenderStateStore } from "stores/render-state.store";
import { useEntitiesStore } from "stores/entities.store";
import { AnimatedEntities } from "primitives/AnimatedEntities";
import drawStaggeredText, {
StaggeredTextCache,
calculateLetters,
} from "drawers/staggered-text";
import useMap from "hooks/useMap";
type CanvasProps = {};
function typedArrayToBuffer(array: Uint8Array): ArrayBuffer {
return array.buffer.slice(
array.byteOffset,
array.byteLength + array.byteOffset
);
}
const CanvasComponent: FC<CanvasProps> = () => {
const canvas = useRef<HTMLCanvasElement>(null);
@@ -21,15 +33,17 @@ const CanvasComponent: FC<CanvasProps> = () => {
const [canvasKit, setCanvasKit] = useState<CanvasKit>();
const [fontData, setFontData] = useState<ArrayBuffer>();
const surface = useRef<Surface>();
const staggeredTextCache = useMap<string, StaggeredTextCache>();
const isLocked = useRef<boolean>(false);
const renderState = useRenderStateStore((store) => store.renderState);
const { fps, size, duration } = useTimelineStore((store) => ({
fps: store.fps,
size: store.size,
duration: store.duration,
}));
const { entities } = useEntitiesStore((store) => ({
const { entities, updateEntityById } = useEntitiesStore((store) => ({
entities: store.entities,
updateEntityById: store.updateEntityById,
}));
useEffect(() => {
@@ -37,73 +51,121 @@ const CanvasComponent: FC<CanvasProps> = () => {
locateFile: (file) =>
"https://unpkg.com/canvaskit-wasm@latest/bin/" + file,
}).then((CanvasKit) => {
setLoading(false);
setCanvasKit(CanvasKit);
fetch("https://storage.googleapis.com/skia-cdn/misc/Roboto-Regular.ttf")
/* fetch("https://storage.googleapis.com/skia-cdn/misc/Roboto-Regular.ttf")
.then((response) => response.arrayBuffer())
.then((arrayBuffer) => {
setLoading(false);
setFontData(arrayBuffer);
});
}); */
if (canvas.current) {
const CSurface = CanvasKit.MakeWebGLCanvasSurface(canvas.current);
if (CSurface) {
surface.current = CSurface;
invoke("get_system_font", { fontName: "Helvetica-Bold" }).then((data) => {
console.log(data);
if (Array.isArray(data)) {
const u8 = new Uint8Array(data as any);
const buffer = typedArrayToBuffer(u8);
setFontData(buffer);
setLoading(false);
if (canvas.current) {
const CSurface = CanvasKit.MakeWebGLCanvasSurface(canvas.current);
if (CSurface) {
surface.current = CSurface;
}
}
}
}
});
});
}, []);
useEffect(() => {
console.time("calculation");
//console.time("calculation");
const parsedEntities = AnimatedEntities.parse(entities);
invoke("calculate_timeline_entities_at_frame", {
timeline: {
entities: parsedEntities,
render_state: renderState,
fps,
size,
duration,
},
}).then((data) => {
console.timeEnd("calculation");
// console.log(data);
if (!loading && !isLocked.current) {
isLocked.current = true;
invoke("calculate_timeline_entities_at_frame", {
timeline: {
entities: parsedEntities,
render_state: renderState,
fps,
size,
duration,
},
}).then((data) => {
const entitiesResult = Entities.safeParse(data);
const entitiesResult = Entities.safeParse(data);
console.time("draw");
if (canvasKit && canvas.current && surface.current && fontData) {
surface.current.flush();
if (canvasKit && canvas.current && surface.current && fontData) {
surface.current.flush();
surface.current.requestAnimationFrame((skCanvas) => {
skCanvas.clear(canvasKit.WHITE);
if (entitiesResult.success) {
const entities = entitiesResult.data;
surface.current.requestAnimationFrame((skCanvas) => {
skCanvas.clear(canvasKit.WHITE);
if (entitiesResult.success) {
const entities = entitiesResult.data;
entities.reverse().forEach((entity) => {
switch (entity.type) {
case EntityType.Enum.Box:
drawBox(canvasKit, skCanvas, entity);
break;
case EntityType.Enum.Ellipse:
drawEllipse(canvasKit, skCanvas, entity);
break;
case EntityType.Enum.Text:
drawText(canvasKit, skCanvas, entity, fontData);
break;
default:
break;
}
});
} else {
console.log(entitiesResult.error);
}
});
}
console.timeEnd("draw");
});
});
entities.reverse().forEach((entity) => {
switch (entity.type) {
case EntityType.Enum.Rect:
drawRect(canvasKit, skCanvas, entity);
break;
case EntityType.Enum.Ellipse:
drawEllipse(canvasKit, skCanvas, entity);
break;
case EntityType.Enum.Text:
drawText(canvasKit, skCanvas, entity, fontData);
break;
case EntityType.Enum.StaggeredText:
{
let cache: StaggeredTextCache;
if (!entity.cache.valid) {
const _cache = staggeredTextCache[0].get(entity.id);
if (_cache !== undefined) {
canvasKit.Free(_cache.glyphs);
}
cache = calculateLetters(canvasKit, entity, fontData);
staggeredTextCache[1].set(entity.id, cache);
updateEntityById(entity.id, { cache: { valid: true } });
} else {
const _cache = staggeredTextCache[0].get(entity.id);
if (_cache) {
cache = _cache;
} else {
cache = calculateLetters(canvasKit, entity, fontData);
}
}
drawStaggeredText(
canvasKit,
skCanvas,
entity,
cache.font,
cache.letterMeasures,
cache.metrics
);
}
break;
default:
break;
}
isLocked.current = false;
});
} else {
isLocked.current = false;
console.log(entitiesResult.error);
}
});
}
//console.timeEnd("draw");
});
}
}, [entities, loading, renderState.curr_frame]);
return (
<div>

View File

@@ -2,18 +2,22 @@ import { ease } from "@unom/style";
import { motion } from "framer-motion";
import {
AnimatedTextEntity,
AnimatedBoxEntity,
AnimatedRectEntity,
AnimatedStaggeredTextEntity,
AnimatedEllipseEntity,
} from "primitives/AnimatedEntities";
import { Paint, PaintStyle, PaintStyleType } from "primitives/Paint";
import { FC } from "react";
import { z } from "zod";
import { AnimatedVec2Properties } from "./Values";
import { AnimatedVec2Properties, ColorProperties } from "./Values";
import { PropertiesProps } from "./common";
type TextPropertiesProps = PropertiesProps<z.input<typeof AnimatedTextEntity>>;
type StaggeredTextPropertiesProps = PropertiesProps<
z.input<typeof AnimatedStaggeredTextEntity>
>;
type PaintPropertiesProps = PropertiesProps<z.input<typeof Paint>>;
type BoxPropertiesProps = PropertiesProps<z.input<typeof AnimatedBoxEntity>>;
type RectPropertiesProps = PropertiesProps<z.input<typeof AnimatedRectEntity>>;
type EllipsePropertiesProps = PropertiesProps<
z.input<typeof AnimatedEllipseEntity>
>;
@@ -45,6 +49,15 @@ export const PaintProperties: FC<PaintPropertiesProps> = ({
))}
</select>
</label>
{entity.style.color && (
<ColorProperties
label="Color"
onUpdate={(color) =>
onUpdate({ ...entity, style: { ...entity.style, color } })
}
entity={entity.style.color}
/>
)}
</div>
);
};
@@ -90,7 +103,75 @@ export const TextProperties: FC<TextPropertiesProps> = ({
);
};
export const BoxProperties: FC<BoxPropertiesProps> = ({ entity, onUpdate }) => {
export const StaggeredTextProperties: FC<StaggeredTextPropertiesProps> = ({
entity,
onUpdate,
}) => {
return (
<motion.div
variants={{ enter: { opacity: 1, y: 0 }, from: { opacity: 0, y: 50 } }}
animate="enter"
initial="from"
transition={ease.quint(0.9).out}
>
<label className="flex flex-col items-start">
<span className="label">Text</span>
<input
value={entity.text}
onChange={(e) =>
onUpdate({
...entity,
text: e.target.value,
cache: { valid: false },
})
}
/>
</label>
<label className="flex flex-col items-start">
<span className="label">Size</span>
<input
value={entity.letter.paint.size}
onChange={(e) =>
onUpdate({
...entity,
letter: {
...entity.letter,
paint: {
...entity.letter.paint,
size: Number(e.target.value),
},
},
})
}
></input>
</label>
<PaintProperties
entity={entity.letter.paint}
onUpdate={(paint) =>
onUpdate({
...entity,
letter: {
...entity.letter,
paint: { ...entity.letter.paint, ...paint },
},
})
}
/>
<AnimatedVec2Properties
onUpdate={(updatedEntity) =>
onUpdate({ ...entity, origin: updatedEntity })
}
label="Origin"
entity={entity.origin}
/>
</motion.div>
);
};
export const RectProperties: FC<RectPropertiesProps> = ({
entity,
onUpdate,
}) => {
return (
<div className="dark:text-white">
<PaintProperties

View File

@@ -2,7 +2,12 @@ import { FC, ReactNode } from "react";
import { useEntitiesStore } from "stores/entities.store";
import { shallow } from "zustand/shallow";
import { BoxProperties, EllipseProperties, TextProperties } from "./Primitives";
import {
RectProperties,
EllipseProperties,
TextProperties,
StaggeredTextProperties,
} from "./Primitives";
const PropertiesContainer: FC<{ children: ReactNode }> = ({ children }) => {
return (
@@ -26,6 +31,15 @@ const Properties = () => {
if (entity) {
switch (entity.type) {
case "StaggeredText":
return (
<StaggeredTextProperties
key={selectedEntity}
onUpdate={(entity) => updateEntity(selectedEntity, entity)}
entity={entity}
/>
);
case "Text":
return (
<TextProperties
@@ -35,9 +49,9 @@ const Properties = () => {
/>
);
case "Box":
case "Rect":
return (
<BoxProperties
<RectProperties
key={selectedEntity}
onUpdate={(entity) => updateEntity(selectedEntity, entity)}
entity={entity}

View File

@@ -7,7 +7,7 @@ import { shallow } from "zustand/shallow";
import { useEntitiesStore } from "stores/entities.store";
import { ease } from "@unom/style";
import Timestamp from "./Timestamp";
import { Keyframe, Keyframes } from "primitives/Keyframe";
import { Keyframe } from "primitives/Keyframe";
import { flattenedKeyframesByEntity } from "utils";
export type AnimationEntity = {
@@ -37,8 +37,8 @@ const KeyframeIndicator: FC<{
style={{
clipPath: "polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)",
}}
className="bg-indigo-300 absolute w-2 h-2 z-30 top-[39%] select-none"
></motion.div>
className="bg-indigo-300 absolute w-2 h-2 z-30 top-[39%] select-none pointer-events-none"
/>
);
};
@@ -55,7 +55,7 @@ const Track: FC<TrackProps> = ({ keyframes, animationData, index, name }) => {
);
return (
<div className="h-8 w-100 flex flex-row gap-1">
<div className="h-8 w-100 flex flex-row gap-1 select-none">
<div
onClick={() =>
selectedEntity !== undefined && selectedEntity === index
@@ -90,6 +90,7 @@ const Track: FC<TrackProps> = ({ keyframes, animationData, index, name }) => {
whileTap={{
scale: 0.9,
}}
onMouseDown={(e) => e.preventDefault()}
transition={ease.circ(0.6).out}
dragElastic={false}
dragConstraints={{ left: 0, right: 900 }}
@@ -113,9 +114,10 @@ const Track: FC<TrackProps> = ({ keyframes, animationData, index, name }) => {
},
});
}}
className="z-10 w-4 bg-slate-500 h-full absolute rounded-md select-none"
className="z-10 w-4 bg-slate-500 h-full absolute rounded-md select-none cursor-w-resize"
/>
<motion.div
onMouseDown={(e) => e.preventDefault()}
drag="x"
animate={{
x: (animationData.duration + animationData.offset) * 100 - 16,
@@ -142,7 +144,7 @@ const Track: FC<TrackProps> = ({ keyframes, animationData, index, name }) => {
},
});
}}
className="z-10 w-4 bg-slate-500 h-full absolute rounded-md select-none"
className="z-10 w-4 bg-slate-500 h-full absolute rounded-md select-none cursor-e-resize"
/>
<motion.div
drag="x"
@@ -156,16 +158,12 @@ const Track: FC<TrackProps> = ({ keyframes, animationData, index, name }) => {
left: 0,
right: 900,
}}
onMouseDown={(e) => e.preventDefault()}
transition={ease.circ(0.8).out}
onDragEnd={(e, info) => {
onDragEnd={(_e, info) => {
let offset = info.offset.x;
offset *= 0.01;
offset += animationData.offset;
console.log(offset);
updateEntity(index, {
animation_data: {
...animationData,
@@ -173,7 +171,7 @@ const Track: FC<TrackProps> = ({ keyframes, animationData, index, name }) => {
},
});
}}
className="z-5 h-full absolute rounded-md transition-colors bg-gray-700 hover:bg-gray-600 select-none"
className="z-5 h-full absolute rounded-md transition-colors bg-gray-700 hover:bg-gray-600 select-none cursor-grab"
></motion.div>
</div>
</div>