add system font loading
improve drawing implement staggered text begin refactor of drawing code
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user